Skip to content
This repository has been archived by the owner on Jul 16, 2023. It is now read-only.

feat: introduce new command check-unnecessary-nullable #874

Merged
merged 13 commits into from Jun 20, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

* test: added test case in [`prefer-const-border-radius`](https://dartcodemetrics.dev/docs/rules/flutter/prefer-const-border-radius) rule.
* chore: restrict `analyzer` version to `>=2.4.0 <4.2.0`.
* feat: introduce new command [`check-unnecessary-nullable`](https://dartcodemetrics.dev/docs/cli/check-unnecessary-nullable).
* fix: normalize file paths after extraction from analyzed folder.
* fix: improve context root included files calculation.
* fix: resolve package with imported analysis options.
Expand Down
1 change: 1 addition & 0 deletions analysis_options.yaml
Expand Up @@ -6,6 +6,7 @@ analyzer:
- test/resources/unused_files_analyzer/**
- test/resources/unused_l10n_analyzer/**
- test/resources/unused_code_analyzer/generated/**
- test/resources/unnecessary_nullable_analyzer/generated/**
- test/resources/file_path_folder/**
- test/**/examples/**
language:
Expand Down
@@ -0,0 +1,74 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/nullability_suffix.dart';

import '../../utils/node_utils.dart';

typedef DeclarationUsages = Map<Element, Iterable<FormalParameter>>;

class DeclarationsVisitor extends RecursiveAstVisitor<void> {
final DeclarationUsages declarations = {};

@override
void visitMethodDeclaration(MethodDeclaration node) {
super.visitMethodDeclaration(node);

final parameters = node.parameters?.parameters;
if (parameters == null || !_hasNullableParameters(parameters)) {
return;
}

_getDeclarationElement(node, parameters);
}

@override
void visitFunctionDeclaration(FunctionDeclaration node) {
super.visitFunctionDeclaration(node);

final parameters = node.functionExpression.parameters?.parameters;
if (isEntrypoint(node.name.name, node.metadata) ||
(parameters == null || !_hasNullableParameters(parameters))) {
return;
}

_getDeclarationElement(node, parameters);
}

@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
super.visitConstructorDeclaration(node);

final parameters = node.parameters.parameters;
if (!_hasNullableParameters(parameters)) {
return;
}

_getDeclarationElement(node, parameters);
}

bool _hasNullableParameters(Iterable<FormalParameter> parameters) =>
parameters.any((parameter) {
final type = parameter.declaredElement?.type;

return type != null &&
(type.nullabilitySuffix == NullabilitySuffix.question &&
(!parameter.isOptional ||
parameter.isOptional && parameter.isRequired)) ||
(parameter is DefaultFormalParameter &&
parameter.defaultValue == null);
});

void _getDeclarationElement(
Declaration node,
Iterable<FormalParameter> parameters,
) {
final element = node.declaredElement;

if (element != null) {
declarations[element] = parameters;
}
}
}
@@ -0,0 +1,56 @@
// ignore_for_file: public_member_api_docs

import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart';

import 'models/invocations_usage.dart';

class InvocationsVisitor extends RecursiveAstVisitor<void> {
final invocationsUsages = InvocationsUsage();

@override
void visitExportDirective(ExportDirective node) {
super.visitExportDirective(node);

final path = node.uriSource?.fullName;
if (path != null) {
invocationsUsages.exports.add(path);
}
}

@override
void visitMethodInvocation(MethodInvocation node) {
super.visitMethodInvocation(node);

_recordUsedElement(node.methodName.staticElement, node.argumentList);
}

@override
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
super.visitFunctionExpressionInvocation(node);

_recordUsedElement(node.staticElement, node.argumentList);
}

@override
void visitInstanceCreationExpression(InstanceCreationExpression node) {
super.visitInstanceCreationExpression(node);

_recordUsedElement(node.constructorName.staticElement, node.argumentList);
}

/// Records use of a not prefixed [element].
void _recordUsedElement(Element? element, ArgumentList arguments) {
if (element == null) {
return;
}
// // Ignore if an unknown library.
incendial marked this conversation as resolved.
Show resolved Hide resolved
final containingLibrary = element.library;
if (containingLibrary == null) {
return;
}
// Remember the element.
invocationsUsages.addElementUsage(element, {arguments});
}
}
@@ -0,0 +1,24 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/element/element.dart';

/// A container with information about used imports prefixes and used imported
/// elements.
class InvocationsUsage {
/// The set of referenced top-level elements.
final Map<Element, Set<ArgumentList>> elements = {};

final Set<String> exports = {};

void addElementUsage(Element element, Set<ArgumentList> expressions) {
elements.update(
element,
(value) => value..addAll(expressions),
ifAbsent: () => expressions,
);
}

void merge(InvocationsUsage other) {
other.elements.forEach(addElementUsage);
exports.addAll(other.exports);
}
}
@@ -0,0 +1,22 @@
import '../../../reporters/models/file_report.dart';
import 'unnecessary_nullable_issue.dart';

/// Represents unused code report collected for a file.
class UnnecessaryNullableFileReport implements FileReport {
/// The path to the target file.
@override
final String path;

/// The path to the target file relative to the package root.
@override
final String relativePath;

/// The issues detected in the target file.
final Iterable<UnnecessaryNullableIssue> issues;

const UnnecessaryNullableFileReport({
required this.path,
required this.relativePath,
required this.issues,
});
}
@@ -0,0 +1,26 @@
import 'package:source_span/source_span.dart';

/// Represents an issue detected by the unused code check.
class UnnecessaryNullableIssue {
/// The name of the unused declaration.
final String declarationName;

/// The type of the unused declaration.
final String declarationType;

final Iterable<String> parameters;

/// The source location associated with this issue.
final SourceLocation location;

/// Initialize a newly created [UnnecessaryNullableIssue].
///
/// The issue is associated with the given [location]. Used for
/// creating an unused code report.
const UnnecessaryNullableIssue({
required this.declarationName,
required this.declarationType,
required this.parameters,
required this.location,
});
}
@@ -0,0 +1,30 @@
import 'dart:io';

import '../../../reporters/models/console_reporter.dart';
import '../../../reporters/models/json_reporter.dart';
import '../../../reporters/models/reporter.dart';
import '../models/unnecessary_nullable_file_report.dart';
import 'reporters_list/console/unnecessary_nullable_console_reporter.dart';
import 'reporters_list/json/unnecessary_nullable_json_reporter.dart';
import 'unnecessary_nullable_report_params.dart';

final _implementedReports = <
String,
Reporter<UnnecessaryNullableFileReport, void,
UnnecessaryNullableReportParams>
Function(
IOSink output,
)>{
ConsoleReporter.id: (output) => UnnecessaryNullableConsoleReporter(output),
JsonReporter.id: (output) => UnnecessaryNullableJsonReporter(output),
};

Reporter<UnnecessaryNullableFileReport, void, UnnecessaryNullableReportParams>?
reporter({
required String name,
required IOSink output,
}) {
final constructor = _implementedReports[name];

return constructor != null ? constructor(output) : null;
}
@@ -0,0 +1,62 @@
import 'dart:io';

import '../../../../../reporters/models/console_reporter.dart';
import '../../../models/unnecessary_nullable_file_report.dart';
import '../../unnecessary_nullable_report_params.dart';

/// Unnecessary nullable console reporter.
///
/// Use it to create reports in console format.
class UnnecessaryNullableConsoleReporter extends ConsoleReporter<
UnnecessaryNullableFileReport, void, UnnecessaryNullableReportParams> {
UnnecessaryNullableConsoleReporter(IOSink output) : super(output);

@override
Future<void> report(
Iterable<UnnecessaryNullableFileReport> records, {
Iterable<void> summary = const [],
UnnecessaryNullableReportParams? additionalParams,
}) async {
if (records.isEmpty) {
if (additionalParams?.congratulate ?? true) {
output
.writeln('${okPen('✔')} no unnecessary nullable parameters found!');
}

return;
}

final sortedRecords = records.toList()
..sort((a, b) => a.relativePath.compareTo(b.relativePath));

var warnings = 0;

for (final analysisRecord in sortedRecords) {
output.writeln('${analysisRecord.relativePath}:');

for (final issue in analysisRecord.issues) {
final line = issue.location.line;
final column = issue.location.column;
final path = analysisRecord.relativePath;

final offset = ''.padRight(3);
final pathOffset = offset.padRight(5);

output
..writeln(
'$offset ${warningPen('⚠')} ${issue.declarationType} ${issue.declarationName} has unnecessary nullable parameters',
)
..writeln('$pathOffset ${issue.parameters}')
..writeln('$pathOffset at $path:$line:$column');
}

warnings += analysisRecord.issues.length;

output.writeln('');
}

output.writeln(
'${alarmPen('✖')} total declarations (functions, methods and constructors) with unnecessary nullable parameters - ${alarmPen(warnings)}',
);
}
}
@@ -0,0 +1,52 @@
import 'dart:convert';
import 'dart:io';

import '../../../../../reporters/models/json_reporter.dart';
import '../../../models/unnecessary_nullable_file_report.dart';
import '../../../models/unnecessary_nullable_issue.dart';
import '../../unnecessary_nullable_report_params.dart';

/// Unnecessary nullable JSON reporter.
///
/// Use it to create reports in JSON format.
class UnnecessaryNullableJsonReporter extends JsonReporter<
UnnecessaryNullableFileReport, void, UnnecessaryNullableReportParams> {
const UnnecessaryNullableJsonReporter(IOSink output) : super(output, 2);

@override
Future<void> report(
Iterable<UnnecessaryNullableFileReport> records, {
Iterable<void> summary = const [],
UnnecessaryNullableReportParams? additionalParams,
}) async {
if (records.isEmpty) {
return;
}

final encodedReport = json.encode({
'formatVersion': formatVersion,
'timestamp': getTimestamp(),
'unnecessaryNullable':
records.map(_unnecessaryNullableFileReportToJson).toList(),
});

output.write(encodedReport);
}

Map<String, Object> _unnecessaryNullableFileReportToJson(
UnnecessaryNullableFileReport report,
) =>
{
'path': report.relativePath,
'issues': report.issues.map(_issueToJson).toList(),
};

Map<String, Object> _issueToJson(UnnecessaryNullableIssue issue) => {
'declarationType': issue.declarationType,
'declarationName': issue.declarationName,
'parameters': issue.parameters,
'column': issue.location.column,
'line': issue.location.line,
'offset': issue.location.offset,
};
}
@@ -0,0 +1,6 @@
/// Represents additional unnecessary nullable reporter params.
class UnnecessaryNullableReportParams {
final bool congratulate;

const UnnecessaryNullableReportParams({required this.congratulate});
}
@@ -0,0 +1,12 @@
import 'package:glob/glob.dart';

/// Represents converted unused code config which contains parsed entities.
class UnnecessaryNullableAnalysisConfig {
final Iterable<Glob> globalExcludes;
final Iterable<Glob> analyzerExcludedPatterns;

const UnnecessaryNullableAnalysisConfig(
this.globalExcludes,
this.analyzerExcludedPatterns,
);
}