diff --git a/CHANGELOG.md b/CHANGELOG.md index cee4767220..f78ec05bd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/analysis_options.yaml b/analysis_options.yaml index e921b9aaea..b9a9bea720 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -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: diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/declarations_visitor.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/declarations_visitor.dart new file mode 100644 index 0000000000..3c3a29d610 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/declarations_visitor.dart @@ -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>; + +class DeclarationsVisitor extends RecursiveAstVisitor { + 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 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 parameters, + ) { + final element = node.declaredElement; + + if (element != null) { + declarations[element] = parameters; + } + } +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/invocations_visitor.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/invocations_visitor.dart new file mode 100644 index 0000000000..4df2dc68b9 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/invocations_visitor.dart @@ -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 { + 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}); + } +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/models/invocations_usage.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/models/invocations_usage.dart new file mode 100644 index 0000000000..d6b475fbd6 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/models/invocations_usage.dart @@ -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> elements = {}; + + final Set exports = {}; + + void addElementUsage(Element element, Set expressions) { + elements.update( + element, + (value) => value..addAll(expressions), + ifAbsent: () => expressions, + ); + } + + void merge(InvocationsUsage other) { + other.elements.forEach(addElementUsage); + exports.addAll(other.exports); + } +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_file_report.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_file_report.dart new file mode 100644 index 0000000000..b09e51ab9d --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_file_report.dart @@ -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 issues; + + const UnnecessaryNullableFileReport({ + required this.path, + required this.relativePath, + required this.issues, + }); +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_issue.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_issue.dart new file mode 100644 index 0000000000..0f69722ebe --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_issue.dart @@ -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 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, + }); +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporter_factory.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporter_factory.dart new file mode 100644 index 0000000000..c0166ced37 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporter_factory.dart @@ -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 + Function( + IOSink output, +)>{ + ConsoleReporter.id: (output) => UnnecessaryNullableConsoleReporter(output), + JsonReporter.id: (output) => UnnecessaryNullableJsonReporter(output), +}; + +Reporter? + reporter({ + required String name, + required IOSink output, +}) { + final constructor = _implementedReports[name]; + + return constructor != null ? constructor(output) : null; +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/console/unnecessary_nullable_console_reporter.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/console/unnecessary_nullable_console_reporter.dart new file mode 100644 index 0000000000..a9b2db7ef4 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/console/unnecessary_nullable_console_reporter.dart @@ -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 report( + Iterable records, { + Iterable 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)}', + ); + } +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/json/unnecessary_nullable_json_reporter.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/json/unnecessary_nullable_json_reporter.dart new file mode 100644 index 0000000000..c6ba407cde --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/json/unnecessary_nullable_json_reporter.dart @@ -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 report( + Iterable records, { + Iterable 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 _unnecessaryNullableFileReportToJson( + UnnecessaryNullableFileReport report, + ) => + { + 'path': report.relativePath, + 'issues': report.issues.map(_issueToJson).toList(), + }; + + Map _issueToJson(UnnecessaryNullableIssue issue) => { + 'declarationType': issue.declarationType, + 'declarationName': issue.declarationName, + 'parameters': issue.parameters, + 'column': issue.location.column, + 'line': issue.location.line, + 'offset': issue.location.offset, + }; +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/unnecessary_nullable_report_params.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/unnecessary_nullable_report_params.dart new file mode 100644 index 0000000000..ef1084ca8c --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/reporters/unnecessary_nullable_report_params.dart @@ -0,0 +1,6 @@ +/// Represents additional unnecessary nullable reporter params. +class UnnecessaryNullableReportParams { + final bool congratulate; + + const UnnecessaryNullableReportParams({required this.congratulate}); +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analysis_config.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analysis_config.dart new file mode 100644 index 0000000000..9f49506874 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analysis_config.dart @@ -0,0 +1,12 @@ +import 'package:glob/glob.dart'; + +/// Represents converted unused code config which contains parsed entities. +class UnnecessaryNullableAnalysisConfig { + final Iterable globalExcludes; + final Iterable analyzerExcludedPatterns; + + const UnnecessaryNullableAnalysisConfig( + this.globalExcludes, + this.analyzerExcludedPatterns, + ); +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer.dart new file mode 100644 index 0000000000..b0562a8669 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer.dart @@ -0,0 +1,292 @@ +import 'dart:io'; + +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +// ignore: implementation_imports +import 'package:analyzer/src/dart/element/element.dart'; +import 'package:collection/collection.dart'; +import 'package:path/path.dart'; +import 'package:source_span/source_span.dart'; + +import '../../config_builder/config_builder.dart'; +import '../../config_builder/models/analysis_options.dart'; +import '../../reporters/models/reporter.dart'; +import '../../utils/analyzer_utils.dart'; +import 'declarations_visitor.dart'; +import 'invocations_visitor.dart'; +import 'models/invocations_usage.dart'; +import 'models/unnecessary_nullable_file_report.dart'; +import 'models/unnecessary_nullable_issue.dart'; +import 'reporters/reporter_factory.dart'; +import 'reporters/unnecessary_nullable_report_params.dart'; +import 'unnecessary_nullable_analysis_config.dart'; +import 'unnecessary_nullable_config.dart'; + +/// The analyzer responsible for collecting unnecessary nullable parameters reports. +class UnnecessaryNullableAnalyzer { + const UnnecessaryNullableAnalyzer(); + + /// Returns a reporter for the given [name]. Use the reporter + /// to convert analysis reports to console, JSON or other supported format. + Reporter? getReporter({ + required String name, + required IOSink output, + }) => + reporter( + name: name, + output: output, + ); + + /// Returns a list of unnecessary nullable parameters reports + /// for analyzing all files in the given [folders]. + /// The analysis is configured with the [config]. + Future> runCliAnalysis( + Iterable folders, + String rootFolder, + UnnecessaryNullableConfig config, { + String? sdkPath, + }) async { + final collection = + createAnalysisContextCollection(folders, rootFolder, sdkPath); + + final invocationsUsages = InvocationsUsage(); + final declarationsUsages = {}; + + for (final context in collection.contexts) { + final unnecessaryNullableAnalysisConfig = + await _getAnalysisConfig(context, rootFolder, config); + + final filePaths = getFilePaths( + folders, + context, + rootFolder, + unnecessaryNullableAnalysisConfig.globalExcludes, + ); + + final analyzedFiles = + filePaths.intersection(context.contextRoot.analyzedFiles().toSet()); + for (final filePath in analyzedFiles) { + final unit = await context.currentSession.getResolvedUnit(filePath); + + final invocationsUsage = _analyzeInvocationsUsage(unit); + if (invocationsUsage != null) { + invocationsUsages.merge(invocationsUsage); + } + + declarationsUsages[filePath] = _analyzeDeclarationsUsage(unit); + } + + final notAnalyzedFiles = filePaths.difference(analyzedFiles); + for (final filePath in notAnalyzedFiles) { + if (unnecessaryNullableAnalysisConfig.analyzerExcludedPatterns + .any((pattern) => pattern.matches(filePath))) { + final unit = await resolveFile2(path: filePath); + + final invocationsUsage = _analyzeInvocationsUsage(unit); + if (invocationsUsage != null) { + invocationsUsages.merge(invocationsUsage); + } + } + } + } + + if (!config.isMonorepo) { + invocationsUsages.exports.forEach(declarationsUsages.remove); + } + + return _getReports(invocationsUsages, declarationsUsages, rootFolder); + } + + Future _getAnalysisConfig( + AnalysisContext context, + String rootFolder, + UnnecessaryNullableConfig config, + ) async { + final analysisOptions = analysisOptionsFromContext(context) ?? + analysisOptionsFromFilePath(rootFolder, context); + + final contextConfig = + ConfigBuilder.getUnnecessaryNullableConfigFromOption(analysisOptions) + .merge(config); + + return ConfigBuilder.getUnnecessaryNullableConfig( + contextConfig, + rootFolder, + ); + } + + InvocationsUsage? _analyzeInvocationsUsage(SomeResolvedUnitResult unit) { + if (unit is ResolvedUnitResult) { + final visitor = InvocationsVisitor(); + unit.unit.visitChildren(visitor); + + return visitor.invocationsUsages; + } + + return null; + } + + DeclarationUsages _analyzeDeclarationsUsage(SomeResolvedUnitResult unit) { + if (unit is ResolvedUnitResult) { + final visitor = DeclarationsVisitor(); + unit.unit.visitChildren(visitor); + + return visitor.declarations; + } + + return {}; + } + + Iterable _getReports( + InvocationsUsage invocationsUsage, + Map declarationsByPath, + String rootFolder, + ) { + final unnecessaryNullableReports = []; + + declarationsByPath.forEach((path, elements) { + final issues = []; + + for (final entry in elements.entries) { + final issue = _getUnnecessaryNullableIssues( + invocationsUsage, + entry.key, + entry.value, + ); + if (issue != null) { + issues.add(issue); + } + } + + final relativePath = relative(path, from: rootFolder); + + if (issues.isNotEmpty) { + unnecessaryNullableReports.add(UnnecessaryNullableFileReport( + path: path, + relativePath: relativePath, + issues: issues, + )); + } + }); + + return unnecessaryNullableReports; + } + + UnnecessaryNullableIssue? _getUnnecessaryNullableIssues( + InvocationsUsage invocationsUsage, + Element element, + Iterable parameters, + ) { + final usages = invocationsUsage.elements[element]; + final unit = element.thisOrAncestorOfType(); + if (usages == null || unit == null) { + return null; + } + + final markedParameters = {}; + + final notNamedParameters = + parameters.where((parameter) => !parameter.isNamed).toList(); + + for (final usage in usages) { + final namedArguments = + usage.arguments.whereType().toList(); + final notNamedArguments = + usage.arguments.whereNot((arg) => arg is NamedExpression).toList(); + + for (final parameter in parameters) { + final relatedArgument = _findRelatedArgument( + parameter, + namedArguments, + notNamedArguments, + notNamedParameters, + ); + + if (relatedArgument == null || + _shouldMarkParameterAsNullable(relatedArgument)) { + markedParameters.add(parameter); + } + } + } + + final unnecessaryNullable = parameters.where( + (parameter) => + !markedParameters.contains(parameter) && + parameter.declaredElement?.type.nullabilitySuffix == + NullabilitySuffix.question, + ); + + if (unnecessaryNullable.isEmpty) { + return null; + } + + return _createUnnecessaryNullableIssue( + element as ElementImpl, + unit, + unnecessaryNullable, + ); + } + + Expression? _findRelatedArgument( + FormalParameter parameter, + List namedArguments, + List notNamedArguments, + List notNamedParameters, + ) { + if (parameter.isNamed) { + return namedArguments.firstWhereOrNull((arg) => + arg is NamedExpression && + arg.name.label.name == parameter.identifier?.name); + } + + final parameterIndex = notNamedParameters.indexOf(parameter); + + return notNamedArguments + .whereNot((element) => element is NamedExpression) + .firstWhereIndexedOrNull((index, _) => index == parameterIndex); + } + + bool _shouldMarkParameterAsNullable(Expression argument) { + if (argument is NamedExpression) { + return _shouldMarkParameterAsNullable(argument.expression); + } + + final staticType = argument.staticType; + + final isNullable = argument is NullLiteral || + (staticType != null && + (staticType.isDynamic || + staticType.nullabilitySuffix == NullabilitySuffix.question)); + + return isNullable; + } + + UnnecessaryNullableIssue _createUnnecessaryNullableIssue( + ElementImpl element, + CompilationUnitElement unit, + Iterable parameters, + ) { + final offset = element.codeOffset!; + final lineInfo = unit.lineInfo; + final offsetLocation = lineInfo.getLocation(offset); + + final sourceUrl = element.source!.uri; + + return UnnecessaryNullableIssue( + declarationName: element.displayName, + declarationType: element.kind.displayName, + parameters: parameters.map((parameter) => parameter.toString()), + location: SourceLocation( + offset, + sourceUrl: sourceUrl, + line: offsetLocation.lineNumber, + column: offsetLocation.columnNumber, + ), + ); + } +} diff --git a/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_config.dart b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_config.dart new file mode 100644 index 0000000000..2981deed68 --- /dev/null +++ b/lib/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_config.dart @@ -0,0 +1,50 @@ +import '../../config_builder/models/analysis_options.dart'; + +/// Represents raw unused code config which can be merged with other raw configs. +class UnnecessaryNullableConfig { + final Iterable excludePatterns; + final Iterable analyzerExcludePatterns; + final bool isMonorepo; + + const UnnecessaryNullableConfig({ + required this.excludePatterns, + required this.analyzerExcludePatterns, + required this.isMonorepo, + }); + + /// Creates the config from analysis [options]. + factory UnnecessaryNullableConfig.fromAnalysisOptions( + AnalysisOptions options, + ) => + UnnecessaryNullableConfig( + excludePatterns: const [], + analyzerExcludePatterns: + options.readIterableOfString(['analyzer', 'exclude']), + isMonorepo: false, + ); + + /// Creates the config from cli args. + factory UnnecessaryNullableConfig.fromArgs( + Iterable excludePatterns, { + required bool isMonorepo, + }) => + UnnecessaryNullableConfig( + excludePatterns: excludePatterns, + analyzerExcludePatterns: const [], + isMonorepo: isMonorepo, + ); + + /// Merges two configs into a single one. + /// + /// Config coming from [overrides] has a higher priority + /// and overrides conflicting entries. + UnnecessaryNullableConfig merge(UnnecessaryNullableConfig overrides) => + UnnecessaryNullableConfig( + excludePatterns: {...excludePatterns, ...overrides.excludePatterns}, + analyzerExcludePatterns: { + ...analyzerExcludePatterns, + ...overrides.analyzerExcludePatterns, + }, + isMonorepo: isMonorepo || overrides.isMonorepo, + ); +} diff --git a/lib/src/analyzers/unused_code_analyzer/unused_code_analyzer.dart b/lib/src/analyzers/unused_code_analyzer/unused_code_analyzer.dart index a9d0183f9c..3d21f9f59a 100644 --- a/lib/src/analyzers/unused_code_analyzer/unused_code_analyzer.dart +++ b/lib/src/analyzers/unused_code_analyzer/unused_code_analyzer.dart @@ -217,9 +217,7 @@ class UnusedCodeAnalyzer { CompilationUnitElement unit, ) { final offset = element.codeOffset!; - - // ignore: unnecessary_non_null_assertion - final lineInfo = unit.lineInfo!; + final lineInfo = unit.lineInfo; final offsetLocation = lineInfo.getLocation(offset); final sourceUrl = element.source!.uri; diff --git a/lib/src/analyzers/unused_code_analyzer/used_code_visitor.dart b/lib/src/analyzers/unused_code_analyzer/used_code_visitor.dart index e2eeb05d36..36bed08c63 100644 --- a/lib/src/analyzers/unused_code_analyzer/used_code_visitor.dart +++ b/lib/src/analyzers/unused_code_analyzer/used_code_visitor.dart @@ -130,7 +130,7 @@ class UsedCodeVisitor extends RecursiveAstVisitor { /// Records use of a not prefixed [element]. void _recordUsedElement(Element element) { - // // Ignore if an unknown library. + // Ignore if an unknown library. final containingLibrary = element.library; if (containingLibrary == null) { return; diff --git a/lib/src/analyzers/unused_l10n_analyzer/unused_l10n_analyzer.dart b/lib/src/analyzers/unused_l10n_analyzer/unused_l10n_analyzer.dart index 4361b00c91..38c767fceb 100644 --- a/lib/src/analyzers/unused_l10n_analyzer/unused_l10n_analyzer.dart +++ b/lib/src/analyzers/unused_l10n_analyzer/unused_l10n_analyzer.dart @@ -232,9 +232,7 @@ class UnusedL10nAnalyzer { CompilationUnitElement unit, ) { final offset = element.codeOffset!; - - // ignore: unnecessary_non_null_assertion - final lineInfo = unit.lineInfo!; + final lineInfo = unit.lineInfo; final offsetLocation = lineInfo.getLocation(offset); final sourceUrl = element.source!.uri; diff --git a/lib/src/cli/cli_runner.dart b/lib/src/cli/cli_runner.dart index 6bc57cec12..ec94d028f4 100644 --- a/lib/src/cli/cli_runner.dart +++ b/lib/src/cli/cli_runner.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import '../version.dart'; import 'commands/analyze_command.dart'; +import 'commands/check_unnecessary_nullable_command.dart'; import 'commands/check_unused_code_command.dart'; import 'commands/check_unused_files_command.dart'; import 'commands/check_unused_l10n_command.dart'; @@ -17,6 +18,7 @@ class CliRunner extends CommandRunner { CheckUnusedFilesCommand(), CheckUnusedL10nCommand(), CheckUnusedCodeCommand(), + CheckUnnecessaryNullableCommand(), ]; final IOSink _output; diff --git a/lib/src/cli/commands/check_unnecessary_nullable_command.dart b/lib/src/cli/commands/check_unnecessary_nullable_command.dart new file mode 100644 index 0000000000..3fe3fd90fe --- /dev/null +++ b/lib/src/cli/commands/check_unnecessary_nullable_command.dart @@ -0,0 +1,110 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:io'; + +import '../../analyzers/unnecessary_nullable_analyzer/reporters/unnecessary_nullable_report_params.dart'; +import '../../analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer.dart'; +import '../../config_builder/config_builder.dart'; +import '../models/flag_names.dart'; +import 'base_command.dart'; + +class CheckUnnecessaryNullableCommand extends BaseCommand { + static const _analyzer = UnnecessaryNullableAnalyzer(); + + @override + String get name => 'check-unnecessary-nullable'; + + @override + String get description => + 'Check unnecessary nullable parameters in functions, methods, constructors.'; + + @override + String get invocation => + '${runner?.executableName} $name [arguments] '; + + CheckUnnecessaryNullableCommand() { + _addFlags(); + } + + @override + Future runCommand() async { + final rootFolder = argResults[FlagNames.rootFolder] as String; + final folders = argResults.rest; + final excludePath = argResults[FlagNames.exclude] as String; + final reporterName = argResults[FlagNames.reporter] as String; + final isMonorepo = argResults[FlagNames.isMonorepo] as bool; + final noCongratulate = argResults[FlagNames.noCongratulate] as bool; + + final config = ConfigBuilder.getUnnecessaryNullableConfigFromArgs( + [excludePath], + isMonorepo: isMonorepo, + ); + + final unusedCodeResult = await _analyzer.runCliAnalysis( + folders, + rootFolder, + config, + sdkPath: findSdkPath(), + ); + + await _analyzer + .getReporter( + name: reporterName, + output: stdout, + ) + ?.report( + unusedCodeResult, + additionalParams: + UnnecessaryNullableReportParams(congratulate: !noCongratulate), + ); + + if (unusedCodeResult.isNotEmpty && + (argResults[FlagNames.fatalOnFound] as bool)) { + exit(1); + } + } + + void _addFlags() { + _usesReporterOption(); + addCommonFlags(); + _usesIsMonorepoOption(); + _usesExitOption(); + } + + void _usesReporterOption() { + argParser + ..addSeparator('') + ..addOption( + FlagNames.reporter, + abbr: 'r', + help: 'The format of the output of the analysis.', + valueHelp: FlagNames.consoleReporter, + allowed: [ + FlagNames.consoleReporter, + FlagNames.jsonReporter, + ], + defaultsTo: FlagNames.consoleReporter, + ); + } + + void _usesIsMonorepoOption() { + argParser + ..addSeparator('') + ..addFlag( + FlagNames.isMonorepo, + help: + 'Treats all exported code with parameters as non-nullable by default.', + ); + } + + void _usesExitOption() { + argParser + ..addSeparator('') + ..addFlag( + FlagNames.fatalOnFound, + help: 'Treat found unnecessary nullable parameters as fatal.', +// TODO(dkrutrkikh): activate on next major version +// defaultsTo: true, + ); + } +} diff --git a/lib/src/cli/models/flag_names.dart b/lib/src/cli/models/flag_names.dart index 3101f88ad3..9645aa2a72 100644 --- a/lib/src/cli/models/flag_names.dart +++ b/lib/src/cli/models/flag_names.dart @@ -33,5 +33,6 @@ class FlagNames { static const l10nClassPattern = 'class-pattern'; static const fatalOnUnused = 'fatal-unused'; + static const fatalOnFound = 'fatal-found'; static const deleteFiles = 'delete-files'; } diff --git a/lib/src/config_builder/config_builder.dart b/lib/src/config_builder/config_builder.dart index 1596cb16bc..92466dcabe 100644 --- a/lib/src/config_builder/config_builder.dart +++ b/lib/src/config_builder/config_builder.dart @@ -5,6 +5,8 @@ import '../analyzers/lint_analyzer/metrics/metrics_factory.dart'; import '../analyzers/lint_analyzer/metrics/models/metric.dart'; import '../analyzers/lint_analyzer/models/entity_type.dart'; import '../analyzers/lint_analyzer/rules/rules_factory.dart'; +import '../analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analysis_config.dart'; +import '../analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_config.dart'; import '../analyzers/unused_code_analyzer/unused_code_analysis_config.dart'; import '../analyzers/unused_code_analyzer/unused_code_config.dart'; import '../analyzers/unused_files_analyzer/unused_files_analysis_config.dart'; @@ -137,4 +139,30 @@ class ConfigBuilder { prepareExcludes(config.analyzerExcludePatterns, rootPath), config.classPattern, ); + + /// Creates a raw unnecessary nullable config from given [excludePatterns]. + static UnnecessaryNullableConfig getUnnecessaryNullableConfigFromArgs( + Iterable excludePatterns, { + required bool isMonorepo, + }) => + UnnecessaryNullableConfig.fromArgs( + excludePatterns, + isMonorepo: isMonorepo, + ); + + /// Creates a raw unnecessary nullable config from given [options]. + static UnnecessaryNullableConfig getUnnecessaryNullableConfigFromOption( + AnalysisOptions options, + ) => + UnnecessaryNullableConfig.fromAnalysisOptions(options); + + /// Creates an unnecessary nullable config from given raw [config]. + static UnnecessaryNullableAnalysisConfig getUnnecessaryNullableConfig( + UnnecessaryNullableConfig config, + String rootPath, + ) => + UnnecessaryNullableAnalysisConfig( + prepareExcludes(config.excludePatterns, rootPath), + prepareExcludes(config.analyzerExcludePatterns, rootPath), + ); } diff --git a/test/resources/unnecessary_nullable_analyzer/generated/some_file.freezed.dart b/test/resources/unnecessary_nullable_analyzer/generated/some_file.freezed.dart new file mode 100644 index 0000000000..1c55e9d4b2 --- /dev/null +++ b/test/resources/unnecessary_nullable_analyzer/generated/some_file.freezed.dart @@ -0,0 +1,3 @@ +void freeze() { + print('brr'); +} diff --git a/test/resources/unnecessary_nullable_analyzer/nullable_class_parameters.dart b/test/resources/unnecessary_nullable_analyzer/nullable_class_parameters.dart new file mode 100644 index 0000000000..821930aab3 --- /dev/null +++ b/test/resources/unnecessary_nullable_analyzer/nullable_class_parameters.dart @@ -0,0 +1,25 @@ +class NullableClassParameters { + final String? value; + + const NullableClassParameters(this.value); +} + +// LINT +class AlwaysUsedAsNonNullable { + final String? anotherValue; + + const AlwaysUsedAsNonNullable(this.anotherValue); +} + +class DefaultNonNullable { + final String value; + + const DefaultNonNullable({this.value = '123'}); +} + +// LINT +class NamedNonNullable { + final String? value; + + const NamedNonNullable({this.value}); +} diff --git a/test/resources/unnecessary_nullable_analyzer/nullable_function_parameters.dart b/test/resources/unnecessary_nullable_analyzer/nullable_function_parameters.dart new file mode 100644 index 0000000000..1d3cd9af30 --- /dev/null +++ b/test/resources/unnecessary_nullable_analyzer/nullable_function_parameters.dart @@ -0,0 +1,30 @@ +// ignore_for_file: avoid-unused-parameters, no-empty-block + +void doSomething(String? value) {} + +// LINT +void alwaysNonNullableDoSomething(String? anotherValue) {} + +void multipleParametersUsed( + String value, + int anotherValue, { + required String? name, + String? secondName, + String? thirdName, +}) {} + +// LINT +void multipleParametersWithNamed( + String? value, + int anotherValue, { + required String? name, + String? secondName, +}) {} + +// LINT +void multipleParametersWithOptional( + String? value, + int anotherValue, [ + String? name, + String? secondName, +]) {} diff --git a/test/resources/unnecessary_nullable_analyzer/nullable_method_parameters.dart b/test/resources/unnecessary_nullable_analyzer/nullable_method_parameters.dart new file mode 100644 index 0000000000..6c0993fe1d --- /dev/null +++ b/test/resources/unnecessary_nullable_analyzer/nullable_method_parameters.dart @@ -0,0 +1,22 @@ +// ignore_for_file: avoid-unused-parameters, no-empty-block + +class ClassWithMethods { + void someMethod(String? value) {} + + // LINT + void alwaysNonNullable(String? anotherValue) {} + + void multipleParametersUsed( + String value, + int anotherValue, { + required String? name, + }) {} + + // LINT + void multipleParametersWithNamed( + String? value, + int anotherValue, { + required String? name, + String? secondName, + }) {} +} diff --git a/test/resources/unnecessary_nullable_analyzer/unnecessary_nullabe_example.dart b/test/resources/unnecessary_nullable_analyzer/unnecessary_nullabe_example.dart new file mode 100644 index 0000000000..07b18aad7f --- /dev/null +++ b/test/resources/unnecessary_nullable_analyzer/unnecessary_nullabe_example.dart @@ -0,0 +1,47 @@ +// ignore_for_file: cascade_invocations, unused_local_variable, prefer_const_declarations + +import 'nullable_class_parameters.dart'; +import 'nullable_function_parameters.dart'; +import 'nullable_method_parameters.dart'; + +void main() { + final withMethods = ClassWithMethods(); + final nullableWrapper = _Test(); + final map = {'123': '321'}; + + withMethods + ..someMethod(null) + ..someMethod('value'); + + withMethods.alwaysNonNullable('anotherValue'); + + withMethods.multipleParametersUsed('123', 1, name: 'null'); + withMethods.multipleParametersUsed('123', 1, name: null); + + withMethods.multipleParametersWithNamed('value', 1, name: 'name'); + + final nonNullableConstructor = const AlwaysUsedAsNonNullable('anotherValue'); + final nullableConstructor = const NullableClassParameters(null); + final defaultNonNullable = const DefaultNonNullable(value: '321'); + final namedNonNullable = const NamedNonNullable(value: '123'); + + doSomething(_getNullableString()); + doSomething(map['321']); + doSomething('value'); + + alwaysNonNullableDoSomething('anotherValue'); + + multipleParametersUsed('str', 1, name: nullableWrapper.level.uri); + multipleParametersUsed('str', 1, name: 'name', secondName: 'secondName'); + multipleParametersUsed('str', 1, name: 'name', thirdName: 'thirdName'); + + multipleParametersWithNamed('name', 1, name: 'secondName'); + multipleParametersWithOptional('name', 1, 'secondName'); +} + +class _Test { + _Test get level => _Test(); + String? get uri => null; +} + +String? _getNullableString() => null; diff --git a/test/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer_test.dart b/test/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer_test.dart new file mode 100644 index 0000000000..9cdd17c774 --- /dev/null +++ b/test/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer_test.dart @@ -0,0 +1,143 @@ +import 'dart:io'; + +import 'package:dart_code_metrics/src/analyzers/unnecessary_nullable_analyzer/models/unnecessary_nullable_file_report.dart'; +import 'package:dart_code_metrics/src/analyzers/unnecessary_nullable_analyzer/reporters/reporters_list/console/unnecessary_nullable_console_reporter.dart'; +import 'package:dart_code_metrics/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_analyzer.dart'; +import 'package:dart_code_metrics/src/analyzers/unnecessary_nullable_analyzer/unnecessary_nullable_config.dart'; +import 'package:path/path.dart'; +import 'package:test/test.dart'; + +void main() { + group( + 'UnnecessaryNullableAnalyzer', + () { + const analyzer = UnnecessaryNullableAnalyzer(); + const rootDirectory = ''; + const analyzerExcludes = [ + 'test/resources/unnecessary_nullable_analyzer/generated/**', + ]; + final folders = [ + normalize( + File('test/resources/unnecessary_nullable_analyzer').absolute.path, + ), + ]; + + group('run analysis', () { + late final Iterable result; + + setUpAll(() async { + final config = + _createConfig(analyzerExcludePatterns: analyzerExcludes); + + result = await analyzer.runCliAnalysis( + folders, + rootDirectory, + config, + ); + }); + + test('should report 3 files and not report excluded file', () { + expect(result, hasLength(3)); + }); + + test('should analyze nullable class parameters', () async { + final report = result.firstWhere((report) => + report.path.endsWith('nullable_class_parameters.dart')); + + expect(report.issues, hasLength(2)); + + final firstIssue = report.issues.first; + expect(firstIssue.declarationName, 'AlwaysUsedAsNonNullable'); + expect(firstIssue.declarationType, 'constructor'); + expect(firstIssue.parameters.toString(), '(this.anotherValue)'); + expect(firstIssue.location.line, 11); + expect(firstIssue.location.column, 3); + + final secondIssue = report.issues.last; + expect(secondIssue.declarationName, 'NamedNonNullable'); + expect(secondIssue.declarationType, 'constructor'); + expect(secondIssue.parameters.toString(), '(this.value)'); + expect(secondIssue.location.line, 24); + expect(secondIssue.location.column, 3); + }); + + test('should analyze nullable method parameters', () async { + final report = result.firstWhere( + (report) => report.path.endsWith('nullable_method_parameters.dart'), + ); + + expect(report.issues, hasLength(2)); + + final firstIssue = report.issues.first; + expect(firstIssue.declarationName, 'alwaysNonNullable'); + expect(firstIssue.declarationType, 'method'); + expect(firstIssue.parameters.toString(), '(String? anotherValue)'); + expect(firstIssue.location.line, 7); + expect(firstIssue.location.column, 3); + + final secondIssue = report.issues.last; + expect(secondIssue.declarationName, 'multipleParametersWithNamed'); + expect(secondIssue.declarationType, 'method'); + expect( + secondIssue.parameters.toString(), + '(String? value, required String? name)', + ); + expect(secondIssue.location.line, 16); + expect(secondIssue.location.column, 3); + }); + + test('should analyze nullable function parameters', () async { + final report = result.firstWhere( + (report) => + report.path.endsWith('nullable_function_parameters.dart'), + ); + + expect(report.issues, hasLength(3)); + + final firstIssue = report.issues.first; + expect(firstIssue.declarationName, 'alwaysNonNullableDoSomething'); + expect(firstIssue.declarationType, 'function'); + expect(firstIssue.parameters.toString(), '(String? anotherValue)'); + expect(firstIssue.location.line, 6); + expect(firstIssue.location.column, 1); + + final secondIssue = report.issues.elementAt(1); + expect(secondIssue.declarationName, 'multipleParametersWithNamed'); + expect(secondIssue.declarationType, 'function'); + expect( + secondIssue.parameters.toString(), + '(String? value, required String? name)', + ); + expect(secondIssue.location.line, 17); + expect(secondIssue.location.column, 1); + + final thirdIssue = report.issues.elementAt(2); + expect(thirdIssue.declarationName, 'multipleParametersWithOptional'); + expect(thirdIssue.declarationType, 'function'); + expect( + thirdIssue.parameters.toString(), + '(String? value, String? name)', + ); + expect(thirdIssue.location.line, 25); + expect(thirdIssue.location.column, 1); + }); + }); + + test('should return a reporter', () { + final reporter = analyzer.getReporter(name: 'console', output: stdout); + + expect(reporter, isA()); + }); + }, + testOn: 'posix', + ); +} + +UnnecessaryNullableConfig _createConfig({ + Iterable analyzerExcludePatterns = const [], +}) => + UnnecessaryNullableConfig( + excludePatterns: const [], + analyzerExcludePatterns: analyzerExcludePatterns, + isMonorepo: false, + ); diff --git a/test/src/cli/commands/check_unnecessary_nullable_command_test.dart b/test/src/cli/commands/check_unnecessary_nullable_command_test.dart new file mode 100644 index 0000000000..361908b084 --- /dev/null +++ b/test/src/cli/commands/check_unnecessary_nullable_command_test.dart @@ -0,0 +1,65 @@ +import 'package:dart_code_metrics/src/cli/cli_runner.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +const _usage = + 'Check unnecessary nullable parameters in functions, methods, constructors.\n' + '\n' + 'Usage: metrics check-unnecessary-nullable [arguments] \n' + '-h, --help Print this usage information.\n' + '\n' + '\n' + '-r, --reporter= The format of the output of the analysis.\n' + ' [console (default), json]\n' + '\n' + '\n' + ' --root-folder=<./> Root folder.\n' + ' (defaults to current directory)\n' + ' --sdk-path= Dart SDK directory path. Should be provided only when you run the application as compiled executable(https://dart.dev/tools/dart-compile#exe) and automatic Dart SDK path detection fails.\n' + ' --exclude=<{/**.g.dart,/**.template.dart}> File paths in Glob syntax to be exclude.\n' + ' (defaults to "{/**.g.dart,/**.template.dart}")\n' + '\n' + '\n' + " --no-congratulate Don't show output even when there are no issues.\n" + '\n' + '\n' + ' --[no-]monorepo Treats all exported code with parameters as non-nullable by default.\n' + '\n' + '\n' + ' --[no-]fatal-found Treat found unnecessary nullable parameters as fatal.\n' + '\n' + 'Run "metrics help" to see global options.'; + +void main() { + group('CheckUnnecessaryNullableCommand', () { + final runner = CliRunner(); + final command = runner.commands['check-unnecessary-nullable']; + + test('should have correct name', () { + expect(command?.name, equals('check-unnecessary-nullable')); + }); + + test('should have correct description', () { + expect( + command?.description, + equals( + 'Check unnecessary nullable parameters in functions, methods, constructors.', + ), + ); + }); + + test('should have correct invocation', () { + expect( + command?.invocation, + equals('metrics check-unnecessary-nullable [arguments] '), + ); + }); + + test('should have correct usage', () { + expect( + command?.usage.replaceAll('"${p.current}"', 'current directory'), + equals(_usage), + ); + }); + }); +} diff --git a/website/docs/cli/check-unnecessary-nullable.md b/website/docs/cli/check-unnecessary-nullable.md new file mode 100644 index 0000000000..acef2b032e --- /dev/null +++ b/website/docs/cli/check-unnecessary-nullable.md @@ -0,0 +1,113 @@ +# Check unnecessary nullable parameters + +Check unnecessary nullable parameters in functions, methods, constructors. Removing unnecessary nullables can help reduce amount of checks in the code. + +To execute the command, run + +```sh +$ dart run dart_code_metrics:metrics check-unnecessary-nullable lib + +# or for a Flutter package +$ flutter pub run dart_code_metrics:metrics check-unnecessary-nullable lib +``` + +Full command description: + +```text +Usage: metrics check-unnecessary-nullable [arguments] +-h, --help Print this usage information. + + +-r, --reporter= The format of the output of the analysis. + [console (default), json] + + + --root-folder=<./> Root folder. + (defaults to current directory) + --sdk-path= Dart SDK directory path. Should be provided only when you run the application as compiled executable(https://dart.dev/tools/dart-compile#exe) and automatic Dart SDK path detection fails. + --exclude=<{/**.g.dart,/**.template.dart}> File paths in Glob syntax to be exclude. + (defaults to "{/**.g.dart,/**.template.dart}") + + + --no-congratulate Don't show output even when there are no issues. + + + --[no-]monorepo Treats all exported code with parameters as non-nullable by default. + + + --[no-]fatal-found Treat found unnecessary nullable parameters as fatal. +``` + +## Monorepo support + +By default the command treats all code that is exported from the package as used. To disable this behavior use `--monorepo` flag. This might be useful when all the packages in your repository are only used within the repository and are not published to the pub. + +## Output example {#output-example} + +### Console {#console} + +Use `--reporter=console` to enable this format. + +![Console](../../static/img/unnecessary-nullable-console-report.png) + +### JSON {#json} + +The reporter prints a single JSON object containing meta information and the unnecessary nullable parameters. Use `--reporter=json` to enable this format. + +#### The **root** object fields are {#the-root-object-fields-are} + +- `formatVersion` - an integer representing the format version (will be incremented each time the serialization format changes) +- `timestamp` - a creation time of the report in YYYY-MM-DD HH:MM:SS format +- `unnecessaryNullable` - an array of [unnecessary nullable issues](#the-unnecessarynullable-object-fields-are) + +```JSON +{ + "formatVersion": 2, + "timestamp": "2021-04-11 14:44:42", + "unnecessaryNullable": [ + { + ... + }, + { + ... + }, + { + ... + } + ] +} +``` + +#### The **unnecessaryNullable** object fields are {#the-unnecessarynullable-object-fields-are} + +- `path` - a relative path of the file with unnecessary nullable parameters declaration +- `issues` - an array of [issues](#the-issue-object-fields-are) detected in the target class + +```JSON +{ + "path": "lib/src/some/class.dart", + "issues": [ + ... + ], +} +``` + +#### The **issue** object fields are {#the-issue-object-fields-are} + +- `declarationName` - the name of a declaration with unnecessary nullable parameters +- `declarationType` - the type of a declaration with unnecessary nullable parameters (function, method or constructor) +- `parameters` - an array of strings representing parameters that are marked as nullable +- `offset` - a zero-based offset of the class member location in the source +- `line` - a zero-based line of the class member location in the source +- `column` - a zero-based column of class member the location in the source + +```JSON +{ + "declarationName": "someFunction", + "declarationType": "function", + "parameters": "[String? value]", + "offset": 156, + "line": 7, + "column": 1 +} +``` diff --git a/website/docs/cli/check-unused-code.md b/website/docs/cli/check-unused-code.md index 77ae70f781..777a2edf8c 100644 --- a/website/docs/cli/check-unused-code.md +++ b/website/docs/cli/check-unused-code.md @@ -27,7 +27,7 @@ Usage: metrics check-unused-code [arguments] --root-folder=<./> Root folder. (defaults to current directory) - --sdk-path= Dart SDK directory path. + --sdk-path= Dart SDK directory path. Should be provided only when you run the application as compiled executable(https://dart.dev/tools/dart-compile#exe) and automatic Dart SDK path detection fails. --exclude=<{/**.g.dart,/**.template.dart}> File paths in Glob syntax to be exclude. (defaults to "{/**.g.dart,/**.template.dart}") @@ -44,7 +44,7 @@ Usage: metrics check-unused-code [arguments] ## Monorepo support -By default the command treats all code that exported from the package as used. To disable this behavior use `--monorepo` flag. This might be useful when all the packages in your repository only unused inside this repository and not published to pub. +By default the command treats all code that is exported from the package as used. To disable this behavior use `--monorepo` flag. This might be useful when all the packages in your repository are only used within the repository and are not published to the pub. ## Output example {#output-example} diff --git a/website/docs/cli/check-unused-files.md b/website/docs/cli/check-unused-files.md index 66c8a68d37..bebc5446bd 100644 --- a/website/docs/cli/check-unused-files.md +++ b/website/docs/cli/check-unused-files.md @@ -23,7 +23,7 @@ Usage: metrics check-unused-files [arguments...] --root-folder=<./> Root folder. (defaults to current directory) - --sdk-path= Dart SDK directory path. + --sdk-path= Dart SDK directory path. Should be provided only when you run the application as compiled executable(https://dart.dev/tools/dart-compile#exe) and automatic Dart SDK path detection fails. --exclude=<{/**.g.dart,/**.template.dart}> File paths in Glob syntax to be exclude. (defaults to "{/**.g.dart,/**.template.dart}") @@ -37,6 +37,10 @@ Usage: metrics check-unused-files [arguments...] -d, --[no-]delete-files Delete all unused files. ``` +## Monorepo support + +By default the command treats all files that are exported from the package as used. To disable this behavior use `--monorepo` flag. This might be useful when all the packages in your repository are only used within the repository and are not published to the pub. + ## Output example {#output-example} ### Console {#console} diff --git a/website/package-lock.json b/website/package-lock.json index 9b8ab2f831..8ebe225abc 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -14,15 +14,15 @@ "@svgr/webpack": "^6.2.1", "clsx": "^1.1.1", "file-loader": "^6.2.0", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", "react-github-btn": "^1.3.0", "url-loader": "^4.1.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "^2.0.0-beta.21", "@tsconfig/docusaurus": "^1.0.6", - "@types/react": "^18.0.12", + "@types/react": "^17.0.0", "@types/react-helmet": "^6.1.5", "@types/react-router-dom": "^5.3.3", "prettier": "2.7.1", @@ -2197,24 +2197,6 @@ "react-dom": "*" } }, - "node_modules/@docusaurus/module-type-aliases/node_modules/@docusaurus/types": { - "version": "2.0.0-beta.21", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.0.0-beta.21.tgz", - "integrity": "sha512-/GH6Npmq81eQfMC/ikS00QSv9jNyO1RXEpNSx5GLA3sFX8Iib26g2YI2zqNplM8nyxzZ2jVBuvUoeODTIbTchQ==", - "dependencies": { - "commander": "^5.1.0", - "history": "^4.9.0", - "joi": "^17.6.0", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.72.1", - "webpack-merge": "^5.8.0" - }, - "peerDependencies": { - "react": "^16.8.4 || ^17.0.0", - "react-dom": "^16.8.4 || ^17.0.0" - } - }, "node_modules/@docusaurus/preset-classic": { "version": "2.0.0-beta.21", "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-2.0.0-beta.21.tgz", @@ -2725,6 +2707,24 @@ "node": ">=16.14" } }, + "node_modules/@docusaurus/types": { + "version": "2.0.0-beta.21", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.0.0-beta.21.tgz", + "integrity": "sha512-/GH6Npmq81eQfMC/ikS00QSv9jNyO1RXEpNSx5GLA3sFX8Iib26g2YI2zqNplM8nyxzZ2jVBuvUoeODTIbTchQ==", + "dependencies": { + "commander": "^5.1.0", + "history": "^4.9.0", + "joi": "^17.6.0", + "react-helmet-async": "^1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.72.1", + "webpack-merge": "^5.8.0" + }, + "peerDependencies": { + "react": "^16.8.4 || ^17.0.0", + "react-dom": "^16.8.4 || ^17.0.0" + } + }, "node_modules/@docusaurus/utils": { "version": "2.0.0-beta.21", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.0.0-beta.21.tgz", @@ -3449,9 +3449,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "node_modules/@types/react": { - "version": "18.0.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", - "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.45.tgz", + "integrity": "sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -3477,11 +3477,11 @@ } }, "node_modules/@types/react-router-config": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.3.tgz", - "integrity": "sha512-38vpjXic0+E2sIBEKUe+RrCmbc8RqcQhNV8OmU3KUcwgy/yzTeo67MhllP+0zjZWNr7Lhw+RnUkL0hzkf63nUQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.6.tgz", + "integrity": "sha512-db1mx37a1EJDf1XeX8jJN7R3PZABmJQXR8r28yUjVMFSjkmnQo6X6pOEEmNl+Tp2gYQOGPdYbFIipBtdElZ3Yg==", "dependencies": { - "@types/history": "*", + "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } @@ -9643,11 +9643,12 @@ } }, "node_modules/react": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", - "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "dependencies": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" }, "engines": { "node": ">=0.10.0" @@ -9750,15 +9751,16 @@ } }, "node_modules/react-dom": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", - "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "dependencies": { "loose-envify": "^1.1.0", - "scheduler": "^0.22.0" + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" }, "peerDependencies": { - "react": "^18.1.0" + "react": "17.0.2" } }, "node_modules/react-error-overlay": { @@ -10475,11 +10477,12 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/scheduler": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", - "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "dependencies": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "node_modules/schema-utils": { @@ -14030,22 +14033,6 @@ "@types/react-router-config": "*", "@types/react-router-dom": "*", "react-helmet-async": "*" - }, - "dependencies": { - "@docusaurus/types": { - "version": "2.0.0-beta.21", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.0.0-beta.21.tgz", - "integrity": "sha512-/GH6Npmq81eQfMC/ikS00QSv9jNyO1RXEpNSx5GLA3sFX8Iib26g2YI2zqNplM8nyxzZ2jVBuvUoeODTIbTchQ==", - "requires": { - "commander": "^5.1.0", - "history": "^4.9.0", - "joi": "^17.6.0", - "react-helmet-async": "^1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.72.1", - "webpack-merge": "^5.8.0" - } - } } }, "@docusaurus/preset-classic": { @@ -14425,6 +14412,20 @@ "tslib": "^2.4.0" } }, + "@docusaurus/types": { + "version": "2.0.0-beta.21", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-2.0.0-beta.21.tgz", + "integrity": "sha512-/GH6Npmq81eQfMC/ikS00QSv9jNyO1RXEpNSx5GLA3sFX8Iib26g2YI2zqNplM8nyxzZ2jVBuvUoeODTIbTchQ==", + "requires": { + "commander": "^5.1.0", + "history": "^4.9.0", + "joi": "^17.6.0", + "react-helmet-async": "^1.3.0", + "utility-types": "^3.10.0", + "webpack": "^5.72.1", + "webpack-merge": "^5.8.0" + } + }, "@docusaurus/utils": { "version": "2.0.0-beta.21", "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-2.0.0-beta.21.tgz", @@ -14958,9 +14959,9 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" }, "@types/react": { - "version": "18.0.12", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.12.tgz", - "integrity": "sha512-duF1OTASSBQtcigUvhuiTB1Ya3OvSy+xORCiEf20H0P0lzx+/KeVsA99U5UjLXSbyo1DRJDlLKqTeM1ngosqtg==", + "version": "17.0.45", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.45.tgz", + "integrity": "sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -14986,11 +14987,11 @@ } }, "@types/react-router-config": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.3.tgz", - "integrity": "sha512-38vpjXic0+E2sIBEKUe+RrCmbc8RqcQhNV8OmU3KUcwgy/yzTeo67MhllP+0zjZWNr7Lhw+RnUkL0hzkf63nUQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.6.tgz", + "integrity": "sha512-db1mx37a1EJDf1XeX8jJN7R3PZABmJQXR8r28yUjVMFSjkmnQo6X6pOEEmNl+Tp2gYQOGPdYbFIipBtdElZ3Yg==", "requires": { - "@types/history": "*", + "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router": "*" } @@ -19442,11 +19443,12 @@ } }, "react": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz", - "integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", "requires": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "react-base16-styling": { @@ -19524,12 +19526,13 @@ } }, "react-dom": { - "version": "18.1.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz", - "integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", "requires": { "loose-envify": "^1.1.0", - "scheduler": "^0.22.0" + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" } }, "react-error-overlay": { @@ -20080,11 +20083,12 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "scheduler": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz", - "integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==", + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", "requires": { - "loose-envify": "^1.1.0" + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" } }, "schema-utils": { diff --git a/website/package.json b/website/package.json index 89b4d13706..fe93fbd00c 100644 --- a/website/package.json +++ b/website/package.json @@ -20,15 +20,15 @@ "@svgr/webpack": "^6.2.1", "clsx": "^1.1.1", "file-loader": "^6.2.0", - "react": "^18.1.0", - "react-dom": "^18.1.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", "react-github-btn": "^1.3.0", "url-loader": "^4.1.1" }, "devDependencies": { "@docusaurus/module-type-aliases": "^2.0.0-beta.21", "@tsconfig/docusaurus": "^1.0.6", - "@types/react": "^18.0.12", + "@types/react": "^17.0.0", "@types/react-helmet": "^6.1.5", "@types/react-router-dom": "^5.3.3", "prettier": "2.7.1", diff --git a/website/static/img/unnecessary-nullable-console-report.png b/website/static/img/unnecessary-nullable-console-report.png new file mode 100644 index 0000000000..4e43294ee0 Binary files /dev/null and b/website/static/img/unnecessary-nullable-console-report.png differ