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

Commit

Permalink
feat: introduce new command check-unnecessary-nullable (#874)
Browse files Browse the repository at this point in the history
* feat: introduce new command check-unnecessary-nullable

* chore: update changelog

* fix: add support for property access

* fix: add support for index expressions

* fix: add support for all expressions as arguments

* fix: add null literal check

* fix: support dynamic type for arguments

* chore: downgrade react version

* chore: fix comments
  • Loading branch information
incendial committed Jun 20, 2022
1 parent 10c3162 commit b4bc57b
Show file tree
Hide file tree
Showing 34 changed files with 1,390 additions and 89 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,7 @@

## Unreleased

* feat: introduce new command [`check-unnecessary-nullable`](https://dartcodemetrics.dev/docs/cli/check-unnecessary-nullable).
* feat: add [`avoid-banned-imports`](https://dartcodemetrics.dev/docs/rules/common/avoid-banned-imports) rule.
* feat: add configuration to [`prefer-extracting-callbacks`](https://dartcodemetrics.dev/docs/rules/flutter/prefer-extracting-callbacks).
* feat: improve [`checkstyle`](https://dartcodemetrics.dev/docs/cli/analyze#checkstyle) report, added metrics entries.
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.
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,
);
}

0 comments on commit b4bc57b

Please sign in to comment.