From b7dab5bcb4c78fd83e0e65bb394446b69532beb2 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 18:49:30 +0300 Subject: [PATCH 1/5] refactor: migrate prefer_last rule & visitor --- lib/main.dart | 9 ++++ .../lints/prefer_last/prefer_last_rule.dart | 46 +++++++------------ .../visitors/prefer_last_visitor.dart | 45 +++++++++--------- 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 7bdc9cdc..d20c5b58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,6 +12,8 @@ import 'package:solid_lints/src/lints/double_literal_format/double_literal_forma import 'package:solid_lints/src/lints/double_literal_format/fixes/double_literal_format_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/fixes/prefer_first_fix.dart'; import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; +import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/lints/proper_super_calls/proper_super_calls_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -34,6 +36,7 @@ class SolidLintsPlugin extends Plugin { final doubleLiteralFormatRule = DoubleLiteralFormatRule(); final preferFirstRule = PreferFirstRule(); + final preferLastRule = PreferLastRule(); final lintRules = [ AvoidFinalWithGetterRule(), @@ -47,6 +50,7 @@ class SolidLintsPlugin extends Plugin { parametersParser: AvoidReturningWidgetsParameters.fromJson, ), preferFirstRule, + preferLastRule, // TODO: Add more lint rules and use analysisLoader // for rules that need parameters // For example: `CyclomaticComplexityRule(analysisLoader)` @@ -68,5 +72,10 @@ class SolidLintsPlugin extends Plugin { preferFirstRule.diagnosticCode, PreferFirstFix.new, ); + + registry.registerFixForRule( + preferLastRule.diagnosticCode, + PreferLastFix.new, + ); } } diff --git a/lib/src/lints/prefer_last/prefer_last_rule.dart b/lib/src/lints/prefer_last/prefer_last_rule.dart index 7dca9d21..32d86452 100644 --- a/lib/src/lints/prefer_last/prefer_last_rule.dart +++ b/lib/src/lints/prefer_last/prefer_last_rule.dart @@ -1,8 +1,7 @@ -import 'package:analyzer/error/listener.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; -import 'package:solid_lints/src/lints/prefer_last/fixes/prefer_last_fix.dart'; +import 'package:analyzer/analysis_rule/rule_context.dart'; +import 'package:analyzer/analysis_rule/rule_visitor_registry.dart'; +import 'package:analyzer/error/error.dart'; import 'package:solid_lints/src/lints/prefer_last/visitors/prefer_last_visitor.dart'; -import 'package:solid_lints/src/models/rule_config.dart'; import 'package:solid_lints/src/models/solid_lint_rule.dart'; /// Warns about usage of `iterable[length - 1]` or @@ -31,37 +30,26 @@ class PreferLastRule extends SolidLintRule { /// access can be simplified. static const lintName = 'prefer_last'; - PreferLastRule._(super.config); + static const _code = LintCode( + lintName, + "Use last instead of accessing the last element by index.", + ); /// Creates a new instance of [PreferLastRule] - /// based on the lint configuration. - factory PreferLastRule.createRule(CustomLintConfigs configs) { - final config = RuleConfig( - configs: configs, - name: lintName, - problemMessage: (value) => - 'Use last instead of accessing the last element by index.', - ); + PreferLastRule() : super(name: lintName, description: _code.problemMessage); - return PreferLastRule._(config); - } + @override + LintCode get diagnosticCode => _code; @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = PreferLastVisitor(); - node.accept(visitor); - - for (final element in visitor.expressions) { - reporter.atNode(element, code); - } - }); + final visitor = PreferLastVisitor(this); + registry.addCompilationUnit(this, visitor); } - @override - List getFixes() => [PreferLastFix()]; + // @override + // List getFixes() => [PreferLastFix()]; } diff --git a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart index e8b12bb2..68175903 100644 --- a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart +++ b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart @@ -1,30 +1,32 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; import 'package:solid_lints/src/utils/types_utils.dart'; /// The AST visitor that will collect all Iterable access expressions /// which can be replaced with .last class PreferLastVisitor extends RecursiveAstVisitor { - final _expressions = []; + final PreferLastRule _rule; - /// List of all Iterable access expressions - Iterable get expressions => _expressions; + /// Creates a new instance of [PreferLastVisitor] + PreferLastVisitor(this._rule); @override void visitMethodInvocation(MethodInvocation node) { super.visitMethodInvocation(node); final target = node.realTarget; + final isIterable = isIterableOrSubclass(target?.staticType); + final isElementAt = node.methodName.name == 'elementAt'; - if (isIterableOrSubclass(target?.staticType) && - node.methodName.name == 'elementAt') { - final arg = node.argumentList.arguments.first; + if (!isIterable || !isElementAt) return; - if (arg is BinaryExpression && - _isLastElementAccess(arg, target.toString())) { - _expressions.add(node); - } + final arg = node.argumentList.arguments.first; + + if (arg is BinaryExpression && + _isLastElementAccess(arg, target.toString())) { + _rule.reportAtNode(node); } } @@ -34,24 +36,25 @@ class PreferLastVisitor extends RecursiveAstVisitor { final target = node.realTarget; - if (isListOrSubclass(target.staticType)) { - final index = node.index; + if (!isListOrSubclass(target.staticType)) return; + + final index = node.index; - if (index is BinaryExpression && - _isLastElementAccess(index, target.toString())) { - _expressions.add(node); - } + if (index is BinaryExpression && + _isLastElementAccess(index, target.toString())) { + _rule.reportAtNode(node); } } bool _isLastElementAccess(BinaryExpression expression, String targetName) { - final left = expression.leftOperand; final right = expression.rightOperand; - final leftName = _getLeftOperandName(left); + if (right is! IntegerLiteral || right.value != 1) return false; - if (right is! IntegerLiteral) return false; - if (right.value != 1) return false; - if (expression.operator.type != TokenType.MINUS) return false; + final isMinusExpression = expression.operator.type == TokenType.MINUS; + if (!isMinusExpression) return false; + + final left = expression.leftOperand; + final leftName = _getLeftOperandName(left); return leftName == '$targetName.length'; } From b63c7ab8e4bb915e40aa8217b9483d84213cc856 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 18:53:49 +0300 Subject: [PATCH 2/5] refactor: migrate prefer_last test --- lint_test/prefer_last_test.dart | 25 ----- test/prefer_last/prefer_last_rule_test.dart | 105 ++++++++++++++++++++ 2 files changed, 105 insertions(+), 25 deletions(-) delete mode 100644 lint_test/prefer_last_test.dart create mode 100644 test/prefer_last/prefer_last_rule_test.dart diff --git a/lint_test/prefer_last_test.dart b/lint_test/prefer_last_test.dart deleted file mode 100644 index 4b87476f..00000000 --- a/lint_test/prefer_last_test.dart +++ /dev/null @@ -1,25 +0,0 @@ -/// Check the `prefer_first` rule -void fun() { - final list = [0, 1, 2, 3]; - final length = list.length - 1; - final set = {0, 1, 2, 3}; - final map = {0: 0, 1: 1, 2: 2, 3: 3}; - - // expect_lint: prefer_last - list[list.length - 1]; - - list[length - 1]; - - // expect_lint: prefer_last - list.elementAt(list.length - 1); - list.elementAt(length - 1); - - // expect_lint: prefer_last - set.elementAt(set.length - 1); - - // expect_lint: prefer_last - map.keys.elementAt(map.keys.length - 1); - - // expect_lint: prefer_last - map.values.elementAt(map.values.length - 1); -} diff --git a/test/prefer_last/prefer_last_rule_test.dart b/test/prefer_last/prefer_last_rule_test.dart new file mode 100644 index 00000000..ab489d08 --- /dev/null +++ b/test/prefer_last/prefer_last_rule_test.dart @@ -0,0 +1,105 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferLastRuleTest); + }); +} + +@reflectiveTest +class PreferLastRuleTest extends AnalysisRuleTest { + @override + void setUp() { + rule = PreferLastRule(); + super.setUp(); + } + + void test_reports_on_list_index_access_with_length_minus_one() async { + await assertDiagnostics( + r''' +final list1 = [0, 1, 2, 3]; +var a = list1[list1.length - 1]; + +void main () { + final list2 = [1, 0, 2, 3]; + list1[list1.length - 1]; + list2[list2.length - 1]; +} +''', + [lint(36, 23), lint(109, 23), lint(136, 23)], + ); + } + + void + test_does_not_report_on_list_index_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +final list = [0, 1, 2, 3]; +final length = list.length - 1; + +var a = list[length - 1]; +'''); + } + + void test_reports_on_list_subclasses() async { + await assertDiagnostics( + r''' +abstract class MyList implements List {} + +T getLast(MyList list) { + return list[list.length - 1]; +} +''', + [lint(88, 21)], + ); + } + + void test_reports_on_element_at_access_with_length_minus_one() async { + await assertDiagnostics( + r''' +final list1 = [0, 1, 2, 3]; +var a = list1.elementAt(list1.length - 1); + +void main () { + final list2 = [1, 0, 2, 3]; + list1.elementAt(list1.length - 1); + list2.elementAt(list2.length - 1); +} +''', + [lint(36, 33), lint(119, 33), lint(156, 33)], + ); + } + + void + test_does_not_report_on_element_at_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +final list = [0, 1, 2, 3]; +final length = list.length - 1; + +var a = list.elementAt(length - 1); +'''); + } + + void test_reports_on_iterable_subclasses() async { + await assertDiagnostics( + r''' +abstract class MyIterable implements Iterable {} + +T getLast(MyIterable iterable) { + return iterable.elementAt(iterable.length - 1); +} + +void main () { + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + set.elementAt(set.length - 1); + map.keys.elementAt(map.keys.length - 1); + map.values.elementAt(map.values.length - 1); +} +''', + [lint(104, 39), lint(234, 29), lint(267, 39), lint(310, 43)], + ); + } +} From 6e866d451ecdc780163188c0cfd786d6df18794b Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 18:55:36 +0300 Subject: [PATCH 3/5] refactor: migrate prefer_last fix --- .../prefer_last/fixes/prefer_last_fix.dart | 97 ++++++++++--------- .../lints/prefer_last/prefer_last_rule.dart | 3 - 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart index 95148903..d568a82d 100644 --- a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart +++ b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart @@ -1,67 +1,70 @@ +import 'package:analysis_server_plugin/edit/dart/correction_producer.dart'; +import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart'; import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/diagnostic/diagnostic.dart'; -import 'package:analyzer/source/source_range.dart'; -import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart'; +import 'package:analyzer_plugin/utilities/fixes/fixes.dart'; +import 'package:solid_lints/src/lints/prefer_last/prefer_last_rule.dart'; /// A Quick fix for `prefer_last` rule /// Suggests to replace iterable access expressions -class PreferLastFix extends DartFix { +class PreferLastFix extends ParsedCorrectionProducer { static const _replaceComment = "Replace with 'last'."; + /// Creates a new instance of [PreferLastFix] + PreferLastFix({required super.context}); + @override - void run( - CustomLintResolver resolver, - ChangeReporter reporter, - CustomLintContext context, - Diagnostic analysisError, - List others, - ) { - context.registry.addMethodInvocation((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + FixKind get fixKind => const FixKind( + 'solid_lints.fix.${PreferLastRule.lintName}', + DartFixKindPriority.standard, + _replaceComment, + ); - _addReplacement(reporter, node, correction); - } - }); + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${PreferLastRule.lintName}', + DartFixKindPriority.standard, + '$_replaceComment across files', + ); - context.registry.addIndexExpression((node) { - if (analysisError.sourceRange.intersects(node.sourceRange)) { - final correction = _createCorrection(node); + @override + CorrectionApplicability get applicability => + CorrectionApplicability.automatically; - _addReplacement(reporter, node, correction); - } - }); + @override + Future compute(ChangeBuilder builder) async { + final targetNode = node.thisOrAncestorMatching( + (n) => n is MethodInvocation || n is IndexExpression, + ); + if (targetNode is! Expression) return; + + final correction = _createCorrection(targetNode); + await _addReplacement(builder, targetNode, correction); } String _createCorrection(Expression expression) { - if (expression is MethodInvocation) { - return expression.isCascaded - ? '..last' - : '${expression.target ?? ''}.last'; - } else if (expression is IndexExpression) { - return expression.isCascaded - ? '..last' - : '${expression.target ?? ''}.last'; - } else { - return '.last'; + switch (expression) { + case MethodInvocation(isCascaded: true): + case IndexExpression(isCascaded: true): + return '..last'; + + case MethodInvocation(:final target?): + case IndexExpression(:final target?): + return '$target.last'; + + default: + return '.last'; } } - void _addReplacement( - ChangeReporter reporter, - Expression node, + Future _addReplacement( + ChangeBuilder builder, + AstNode node, String correction, - ) { - final changeBuilder = reporter.createChangeBuilder( - message: _replaceComment, - priority: 1, + ) async { + await builder.addDartFileEdit( + file, + (builder) => builder.addSimpleReplacement(node.sourceRange, correction), ); - - changeBuilder.addDartFileEdit((builder) { - builder.addSimpleReplacement( - SourceRange(node.offset, node.length), - correction, - ); - }); } } diff --git a/lib/src/lints/prefer_last/prefer_last_rule.dart b/lib/src/lints/prefer_last/prefer_last_rule.dart index 32d86452..ff75f5ba 100644 --- a/lib/src/lints/prefer_last/prefer_last_rule.dart +++ b/lib/src/lints/prefer_last/prefer_last_rule.dart @@ -49,7 +49,4 @@ class PreferLastRule extends SolidLintRule { final visitor = PreferLastVisitor(this); registry.addCompilationUnit(this, visitor); } - - // @override - // List getFixes() => [PreferLastFix()]; } From b776b41f4da85a8f3a07288c24b82f964df2166e Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 20:51:18 +0300 Subject: [PATCH 4/5] fix: handle null awareness --- lib/src/lints/prefer_last/fixes/prefer_last_fix.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart index d568a82d..105d6cc0 100644 --- a/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart +++ b/lib/src/lints/prefer_last/fixes/prefer_last_fix.dart @@ -44,13 +44,13 @@ class PreferLastFix extends ParsedCorrectionProducer { String _createCorrection(Expression expression) { switch (expression) { - case MethodInvocation(isCascaded: true): - case IndexExpression(isCascaded: true): - return '..last'; + case MethodInvocation(isCascaded: true, :final isNullAware): + case IndexExpression(isCascaded: true, :final isNullAware): + return isNullAware ? '?.last' : '..last'; - case MethodInvocation(:final target?): - case IndexExpression(:final target?): - return '$target.last'; + case MethodInvocation(:final target?, :final isNullAware): + case IndexExpression(:final target?, :final isNullAware): + return isNullAware ? '$target?.last' : '$target.last'; default: return '.last'; From 8cbbc84036dc583a589a49c1f73c338e156cf235 Mon Sep 17 00:00:00 2001 From: Andrew Bekhiet Date: Mon, 15 Jun 2026 20:52:17 +0300 Subject: [PATCH 5/5] fix: prefer firstOrNull --- lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart index 68175903..d44dbfc8 100644 --- a/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart +++ b/lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart @@ -22,7 +22,7 @@ class PreferLastVisitor extends RecursiveAstVisitor { if (!isIterable || !isElementAt) return; - final arg = node.argumentList.arguments.first; + final arg = node.argumentList.arguments.firstOrNull; if (arg is BinaryExpression && _isLastElementAccess(arg, target.toString())) {