From 7e1deaf2920b9c2b0f6f469a110e3cce2b66fb79 Mon Sep 17 00:00:00 2001 From: DenisBogatirov Date: Fri, 22 Sep 2023 13:28:32 +0300 Subject: [PATCH] Added prefer first rule (#60) * Add prefer-conditional-expressions rule and fix * Add tests for prefer-conditional-expressions rule * fix nested test plugin path * Fix tests after merge * Add prefer-first rule and fix * Add tests for prefer-first rule --------- Co-authored-by: Denis Bogatirov Co-authored-by: Yurii Prykhodko <144313329+yurii-prykhodko-solid@users.noreply.github.com> --- lib/lints/prefer_first/prefer_first_fix.dart | 67 +++++++++++++++++++ lib/lints/prefer_first/prefer_first_rule.dart | 48 +++++++++++++ .../prefer_first/prefer_first_visitor.dart | 40 +++++++++++ lib/solid_lints.dart | 2 + lib/utils/types_utils.dart | 7 ++ lint_test/analysis_options.yaml | 1 + lint_test/prefer_first_test.dart | 21 ++++++ 7 files changed, 186 insertions(+) create mode 100644 lib/lints/prefer_first/prefer_first_fix.dart create mode 100644 lib/lints/prefer_first/prefer_first_rule.dart create mode 100644 lib/lints/prefer_first/prefer_first_visitor.dart create mode 100644 lint_test/prefer_first_test.dart diff --git a/lib/lints/prefer_first/prefer_first_fix.dart b/lib/lints/prefer_first/prefer_first_fix.dart new file mode 100644 index 00000000..c1557c97 --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_fix.dart @@ -0,0 +1,67 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +/// A Quick fix for `prefer-first` rule +/// Suggests to replace iterable access expressions +class PreferFirstFix extends DartFix { + static const _replaceComment = "Replace with 'first'."; + + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addMethodInvocation((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + + context.registry.addIndexExpression((node) { + if (analysisError.sourceRange.intersects(node.sourceRange)) { + final correction = _createCorrection(node); + + _addReplacement(reporter, node, correction); + } + }); + } + + String _createCorrection(Expression expression) { + if (expression is MethodInvocation) { + return expression.isCascaded + ? '..first' + : '${expression.target ?? ''}.first'; + } else if (expression is IndexExpression) { + return expression.isCascaded + ? '..first' + : '${expression.target ?? ''}.first'; + } else { + return '.first'; + } + } + + void _addReplacement( + ChangeReporter reporter, + Expression node, + String correction, + ) { + final changeBuilder = reporter.createChangeBuilder( + message: _replaceComment, + priority: 1, + ); + + changeBuilder.addDartFileEdit((builder) { + builder.addSimpleReplacement( + SourceRange(node.offset, node.length), + correction, + ); + }); + } +} diff --git a/lib/lints/prefer_first/prefer_first_rule.dart b/lib/lints/prefer_first/prefer_first_rule.dart new file mode 100644 index 00000000..c9e33f26 --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_rule.dart @@ -0,0 +1,48 @@ +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_fix.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_visitor.dart'; +import 'package:solid_lints/models/rule_config.dart'; +import 'package:solid_lints/models/solid_lint_rule.dart'; + +/// A `prefer-first` rule which warns about +/// usage of iterable[0] or iterable.elementAt(0) +class PreferFirstRule extends SolidLintRule { + /// The [LintCode] of this lint rule that represents the error if number of + /// parameters reaches the maximum value. + static const lintName = 'prefer-first'; + + PreferFirstRule._(super.config); + + /// Creates a new instance of [PreferFirstRule] + /// based on the lint configuration. + factory PreferFirstRule.createRule(CustomLintConfigs configs) { + final config = RuleConfig( + configs: configs, + name: lintName, + problemMessage: (value) => + 'Use first instead of accessing the element at zero index.', + ); + + return PreferFirstRule._(config); + } + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addCompilationUnit((node) { + final visitor = PreferFirstVisitor(); + node.accept(visitor); + + for (final element in visitor.expressions) { + reporter.reportErrorForNode(code, element); + } + }); + } + + @override + List getFixes() => [PreferFirstFix()]; +} diff --git a/lib/lints/prefer_first/prefer_first_visitor.dart b/lib/lints/prefer_first/prefer_first_visitor.dart new file mode 100644 index 00000000..3abdf1e5 --- /dev/null +++ b/lib/lints/prefer_first/prefer_first_visitor.dart @@ -0,0 +1,40 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/utils/types_utils.dart'; + +/// The AST visitor that will collect all Iterable access expressions +/// which can be replaced with .first +class PreferFirstVisitor extends RecursiveAstVisitor { + final _expressions = []; + + /// List of all Iterable access expressions + Iterable get expressions => _expressions; + + @override + void visitMethodInvocation(MethodInvocation node) { + super.visitMethodInvocation(node); + final isIterable = isIterableOrSubclass(node.realTarget?.staticType); + final isElementAt = node.methodName.name == 'elementAt'; + + if (isIterable && isElementAt) { + final arg = node.argumentList.arguments.first; + + if (arg is IntegerLiteral && arg.value == 0) { + _expressions.add(node); + } + } + } + + @override + void visitIndexExpression(IndexExpression node) { + super.visitIndexExpression(node); + + if (isListOrSubclass(node.realTarget.staticType)) { + final index = node.index; + + if (index is IntegerLiteral && index.value == 0) { + _expressions.add(node); + } + } + } +} diff --git a/lib/solid_lints.dart b/lib/solid_lints.dart index 8c95d096..b6711421 100644 --- a/lib/solid_lints.dart +++ b/lib/solid_lints.dart @@ -20,6 +20,7 @@ import 'package:solid_lints/lints/no_equal_then_else/no_equal_then_else_rule.dar import 'package:solid_lints/lints/no_magic_number/no_magic_number_rule.dart'; import 'package:solid_lints/lints/number_of_parameters/number_of_parameters_metric.dart'; import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart'; +import 'package:solid_lints/lints/prefer_first/prefer_first_rule.dart'; import 'package:solid_lints/models/solid_lint_rule.dart'; /// Creates a plugin for our custom linter @@ -49,6 +50,7 @@ class _SolidLints extends PluginBase { MemberOrderingRule.createRule(configs), NoMagicNumberRule.createRule(configs), PreferConditionalExpressionsRule.createRule(configs), + PreferFirstRule.createRule(configs), ]; // Return only enabled rules diff --git a/lib/utils/types_utils.dart b/lib/utils/types_utils.dart index d2584308..d73ce35f 100644 --- a/lib/utils/types_utils.dart +++ b/lib/utils/types_utils.dart @@ -24,6 +24,7 @@ import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; bool hasWidgetType(DartType type) => (isWidgetOrSubclass(type) || @@ -162,3 +163,9 @@ bool _isFutureInheritedProvider(DartType type) => type.isDartAsyncFuture && type is InterfaceType && _isSubclassOfInheritedProvider(type.typeArguments.firstOrNull); + +bool isIterableOrSubclass(DartType? type) => + _checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false); + +bool isListOrSubclass(DartType? type) => + _checkSelfOrSupertypes(type, (t) => t?.isDartCoreList ?? false); diff --git a/lint_test/analysis_options.yaml b/lint_test/analysis_options.yaml index 4164f2ef..406ed6a2 100644 --- a/lint_test/analysis_options.yaml +++ b/lint_test/analysis_options.yaml @@ -50,3 +50,4 @@ custom_lint: - dispose-method - no-magic-number - prefer-conditional-expressions + - prefer-first diff --git a/lint_test/prefer_first_test.dart b/lint_test/prefer_first_test.dart new file mode 100644 index 00000000..49d6bf54 --- /dev/null +++ b/lint_test/prefer_first_test.dart @@ -0,0 +1,21 @@ +/// Check the `prefer-first` rule +void fun() { + const zero = 0; + final list = [0, 1, 2, 3]; + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + // expect_lint: prefer-first + list[0]; + list[zero]; + // expect_lint: prefer-first + list.elementAt(0); + list.elementAt(zero); + // expect_lint: prefer-first + set.elementAt(0); + + // expect_lint: prefer-first + map.keys.elementAt(0); + // expect_lint: prefer-first + map.values.elementAt(0); +}