diff --git a/lib/main.dart b/lib/main.dart index 44e0b44d..cb76e1d8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,9 +14,12 @@ import 'package:solid_lints/src/lints/cyclomatic_complexity/cyclomatic_complexit import 'package:solid_lints/src/lints/cyclomatic_complexity/models/cyclomatic_complexity_parameters.dart'; import 'package:solid_lints/src/lints/double_literal_format/double_literal_format_rule.dart'; 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'; import 'package:solid_lints/src/lints/use_nearest_context/fixes/rename_nearest_context_parameter_fix.dart'; -import 'package:solid_lints/src/lints/use_nearest_context/fixes/replace_with_nearest_context_parameter_fix.dart'; import 'package:solid_lints/src/lints/use_nearest_context/use_nearest_context_rule.dart'; /// The entry point for the Solid Lints analyser server plugin. @@ -40,6 +43,8 @@ class SolidLintsPlugin extends Plugin { final avoidUnnecessaryTypeAssertionsRule = AvoidUnnecessaryTypeAssertionsRule(); final doubleLiteralFormatRule = DoubleLiteralFormatRule(); + final preferFirstRule = PreferFirstRule(); + final preferLastRule = PreferLastRule(); final lintRules = [ AvoidFinalWithGetterRule(), @@ -60,6 +65,11 @@ class SolidLintsPlugin extends Plugin { parametersParser: CyclomaticComplexityParameters.fromJson, ), UseNearestContextRule(), + preferFirstRule, + preferLastRule, + // TODO: Add more lint rules and use analysisLoader + // for rules that need parameters + // For example: `CyclomaticComplexityRule(analysisLoader)` ]; for (final lintRule in lintRules) { @@ -86,8 +96,13 @@ class SolidLintsPlugin extends Plugin { ); registry.registerFixForRule( - UseNearestContextRule.code, - ReplaceWithNearestContextParameterFix.new, + preferFirstRule.diagnosticCode, + PreferFirstFix.new, + ); + + registry.registerFixForRule( + preferLastRule.diagnosticCode, + PreferLastFix.new, ); } } diff --git a/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart b/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart index d677ffde..0dad920b 100644 --- a/lib/src/lints/prefer_first/fixes/prefer_first_fix.dart +++ b/lib/src/lints/prefer_first/fixes/prefer_first_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_first/prefer_first_rule.dart'; /// A Quick fix for `prefer_first` rule /// Suggests to replace iterable access expressions -class PreferFirstFix extends DartFix { +class PreferFirstFix extends ParsedCorrectionProducer { static const _replaceComment = "Replace with 'first'."; + /// Creates a new instance of [PreferFirstFix] + PreferFirstFix({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.${PreferFirstRule.lintName}', + DartFixKindPriority.standard, + _replaceComment, + ); - _addReplacement(reporter, node, correction); - } - }); + @override + FixKind get multiFixKind => const FixKind( + 'solid_lints.fix.multi.${PreferFirstRule.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 - ? '..first' - : '${expression.target ?? ''}.first'; - } else if (expression is IndexExpression) { - return expression.isCascaded - ? '..first' - : '${expression.target ?? ''}.first'; - } else { - return '.first'; + switch (expression) { + case MethodInvocation(isCascaded: true, :final isNullAware): + case IndexExpression(isCascaded: true, :final isNullAware): + return isNullAware ? '?.first' : '..first'; + + case MethodInvocation(:final target?, :final isNullAware): + case IndexExpression(:final target?, :final isNullAware): + return isNullAware ? '$target?.first' : '$target.first'; + + default: + return '.first'; } } - 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_first/prefer_first_rule.dart b/lib/src/lints/prefer_first/prefer_first_rule.dart index 0af19607..b644cffe 100644 --- a/lib/src/lints/prefer_first/prefer_first_rule.dart +++ b/lib/src/lints/prefer_first/prefer_first_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_first/fixes/prefer_first_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_first/visitors/prefer_first_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[0] or iterable.elementAt(0) instead of @@ -31,37 +30,23 @@ class PreferFirstRule extends SolidLintRule { /// parameters reaches the maximum value. static const lintName = 'prefer_first'; - PreferFirstRule._(super.config); + static const _code = LintCode( + lintName, + "Use first instead of accessing the element at zero index.", + ); - /// 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.', - ); + @override + LintCode get diagnosticCode => _code; - return PreferFirstRule._(config); - } + /// Creates a new instance of [PreferFirstRule] + PreferFirstRule() : super(name: lintName, description: _code.problemMessage); @override - void run( - CustomLintResolver resolver, - DiagnosticReporter reporter, - CustomLintContext context, + void registerNodeProcessors( + RuleVisitorRegistry registry, + RuleContext context, ) { - context.registry.addCompilationUnit((node) { - final visitor = PreferFirstVisitor(); - node.accept(visitor); - - for (final element in visitor.expressions) { - reporter.atNode(element, code); - } - }); + final visitor = PreferFirstVisitor(this); + registry.addCompilationUnit(this, visitor); } - - @override - List getFixes() => [PreferFirstFix()]; } diff --git a/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart b/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart index 984d4515..222734c6 100644 --- a/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart +++ b/lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart @@ -1,14 +1,15 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_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 .first class PreferFirstVisitor extends RecursiveAstVisitor { - final _expressions = []; + final PreferFirstRule _rule; - /// List of all Iterable access expressions - Iterable get expressions => _expressions; + /// Creates a new instance of [PreferFirstVisitor] + PreferFirstVisitor(this._rule); @override void visitMethodInvocation(MethodInvocation node) { @@ -16,12 +17,11 @@ class PreferFirstVisitor extends RecursiveAstVisitor { final isIterable = isIterableOrSubclass(node.realTarget?.staticType); final isElementAt = node.methodName.name == 'elementAt'; - if (isIterable && isElementAt) { - final arg = node.argumentList.arguments.first; + if (!isIterable || !isElementAt) return; - if (arg is IntegerLiteral && arg.value == 0) { - _expressions.add(node); - } + final arg = node.argumentList.arguments.firstOrNull; + if (arg case IntegerLiteral(value: 0)) { + _rule.reportAtNode(node); } } @@ -29,12 +29,12 @@ class PreferFirstVisitor extends RecursiveAstVisitor { void visitIndexExpression(IndexExpression node) { super.visitIndexExpression(node); - if (isListOrSubclass(node.realTarget.staticType)) { - final index = node.index; + if (!isListOrSubclass(node.realTarget.staticType)) return; - if (index is IntegerLiteral && index.value == 0) { - _expressions.add(node); - } + final index = node.index; + + if (index case IntegerLiteral(value: 0)) { + _rule.reportAtNode(node); } } } 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..105d6cc0 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, :final isNullAware): + case IndexExpression(isCascaded: true, :final isNullAware): + return isNullAware ? '?.last' : '..last'; + + case MethodInvocation(:final target?, :final isNullAware): + case IndexExpression(:final target?, :final isNullAware): + return isNullAware ? '$target?.last' : '$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 7dca9d21..ff75f5ba 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,23 @@ 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()]; } 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..b4ee4c43 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,33 @@ 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 = []; + static const _lengthGetterName = 'length'; - /// List of all Iterable access expressions - Iterable get expressions => _expressions; + final PreferLastRule _rule; + + /// 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 (!isIterable || !isElementAt) return; - if (isIterableOrSubclass(target?.staticType) && - node.methodName.name == 'elementAt') { - final arg = node.argumentList.arguments.first; + final arg = node.argumentList.arguments.firstOrNull; - if (arg is BinaryExpression && - _isLastElementAccess(arg, target.toString())) { - _expressions.add(node); - } + if (arg is BinaryExpression && _isLastElementAccess(arg, target)) { + _rule.reportAtNode(node); } } @@ -34,41 +37,76 @@ class PreferLastVisitor extends RecursiveAstVisitor { final target = node.realTarget; - if (isListOrSubclass(target.staticType)) { - final index = node.index; + if (!isListOrSubclass(target.staticType)) return; - if (index is BinaryExpression && - _isLastElementAccess(index, target.toString())) { - _expressions.add(node); - } + final index = node.index; + + if (index is BinaryExpression && _isLastElementAccess(index, target)) { + _rule.reportAtNode(node); } } - bool _isLastElementAccess(BinaryExpression expression, String targetName) { - final left = expression.leftOperand; + bool _isLastElementAccess(BinaryExpression expression, Expression? target) { 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; + if (target == null) return false; + + return _isLengthOfTarget(expression.leftOperand, target); + } - return leftName == '$targetName.length'; + bool _isLengthOfTarget(Expression expression, Expression target) { + final lengthReceiver = switch (expression) { + PropertyAccess( + propertyName: SimpleIdentifier(name: _lengthGetterName), + operator: Token(type: TokenType.PERIOD), + :final target, + ) => + target, + PrefixedIdentifier( + prefix: final prefix, + identifier: SimpleIdentifier(name: _lengthGetterName), + ) => + prefix, + _ => null, + }; + + if (lengthReceiver == null) return false; + + return _referencesSameTarget(lengthReceiver, target); } - String? _getLeftOperandName(Expression expression) { - if (expression is PrefixedIdentifier) { - return expression.name; + bool _referencesSameTarget( + Expression lengthReceiver, + Expression accessTarget, + ) { + final normalizedLengthReceiver = _unwrapExpression(lengthReceiver); + final normalizedAccessTarget = _unwrapExpression(accessTarget); + + if (normalizedLengthReceiver is Identifier && + normalizedAccessTarget is Identifier) { + return normalizedLengthReceiver.element?.id == + normalizedAccessTarget.element?.id; } - /// Access target like map.keys.length is being reported as PropertyAccess - /// expression this case will handle such cases - if (expression is PropertyAccess) { - if (expression.operator.type != TokenType.PERIOD) return null; + return normalizedLengthReceiver.toString() == + normalizedAccessTarget.toString(); + } + + Expression _unwrapExpression(Expression expression) { + return switch (expression) { + PostfixExpression( + :final operand, + operator: Token(type: TokenType.BANG), + ) => + _unwrapExpression(operand), - return expression.toString(); - } + ParenthesizedExpression(:final expression) => _unwrapExpression( + expression, + ), - return null; + _ => expression, + }; } } diff --git a/lint_test/prefer_first_test.dart b/lint_test/prefer_first_test.dart deleted file mode 100644 index 2527d828..00000000 --- a/lint_test/prefer_first_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -/// 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); -} 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/lints/prefer_first/prefer_first_rule_test.dart b/test/lints/prefer_first/prefer_first_rule_test.dart new file mode 100644 index 00000000..50bdf212 --- /dev/null +++ b/test/lints/prefer_first/prefer_first_rule_test.dart @@ -0,0 +1,103 @@ +import 'package:analyzer_testing/analysis_rule/analysis_rule.dart'; +import 'package:solid_lints/src/lints/prefer_first/prefer_first_rule.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; + +import '../auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferFirstRuleTest); + }); +} + +@reflectiveTest +class PreferFirstRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = PreferFirstRule(); + super.setUp(); + } + + void test_reports_on_list_index_access_with_zero_literal() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1[0]')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1[0]')}; + ${expectLint('list2[0]')}; +} +'''); + } + + void + test_does_not_report_on_list_index_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +const zero = 0; +final zeroVar = 0; + +final list = [0, 1, 2, 3]; + +var a = list[zero]; +var b = list[1 - 1]; +var c = list[zeroVar]; +'''); + } + + void test_reports_on_list_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyList implements List {} + +T getFirst(MyList list) { + return ${expectLint('list[0]')}; +} +'''); + } + + void test_reports_on_element_at_access_with_zero_literal() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1.elementAt(0)')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1.elementAt(0)')}; + ${expectLint('list2.elementAt(0)')}; +} +'''); + } + + void + test_does_not_report_on_element_at_access_with_variable_or_constant() async { + await assertNoDiagnostics(r''' +const zero = 0; +final zeroVar = 0; + +final list = [0, 1, 2, 3]; + +var a = list.elementAt(zero); +var b = list.elementAt(1 - 1); +var c = list.elementAt(zeroVar); +'''); + } + + void test_reports_on_iterable_subclasses() async { + await assertAutoDiagnostics(''' +abstract class MyIterable implements Iterable {} + +T getFirst(MyIterable iterable) { + return ${expectLint('iterable.elementAt(0)')}; +} + +void main () { + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + ${expectLint('set.elementAt(0)')}; + ${expectLint('map.keys.elementAt(0)')}; + ${expectLint('map.values.elementAt(0)')}; +} +'''); + } +} 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..a25747c6 --- /dev/null +++ b/test/prefer_last/prefer_last_rule_test.dart @@ -0,0 +1,129 @@ +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'; + +import '../lints/auto_test_lint_offsets.dart'; + +void main() { + defineReflectiveSuite(() { + defineReflectiveTests(PreferLastRuleTest); + }); +} + +@reflectiveTest +class PreferLastRuleTest extends AnalysisRuleTest with AutoTestLintOffsets { + @override + void setUp() { + rule = PreferLastRule(); + super.setUp(); + } + + void test_reports_on_list_index_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1[list1.length - 1]')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1[list1.length - 1]')}; + ${expectLint('list2[list2.length - 1]')}; +} +'''); + } + + 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 assertAutoDiagnostics(''' +abstract class MyList implements List {} + +T getLast(MyList list) { + return ${expectLint('list[list.length - 1]')}; +} +'''); + } + + void test_reports_on_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +final list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1.elementAt(list1.length - 1)')}; + +void main () { + final list2 = [1, 0, 2, 3]; + ${expectLint('list1.elementAt(list1.length - 1)')}; + ${expectLint('list2.elementAt(list2.length - 1)')}; +} +'''); + } + + 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 assertAutoDiagnostics(''' +abstract class MyIterable implements Iterable {} + +T getLast(MyIterable iterable) { + return ${expectLint('iterable.elementAt(iterable.length - 1)')}; +} + +void main () { + final set = {0, 1, 2, 3}; + final map = {0: 0, 1: 1, 2: 2, 3: 3}; + + ${expectLint('set.elementAt(set.length - 1)')}; + ${expectLint('map.keys.elementAt(map.keys.length - 1)')}; + ${expectLint('map.values.elementAt(map.values.length - 1)')}; +} +'''); + } + + void test_reports_on_cascade_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +void main () { + final list2 = [1, 0, 2, 3]; + list2${expectLint('..elementAt(list2.length - 1)')}; +} +'''); + } + + void test_reports_on_null_aware_index_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +List? list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1?[list1!.length - 1]')}; + +void test(List? list2) { + ${expectLint('list1?[list1!.length - 1]')}; + ${expectLint('list2?[list2.length - 1]')}; +} +'''); + } + + void + test_reports_on_null_aware_element_at_access_with_length_minus_one() async { + await assertAutoDiagnostics(''' +List? list1 = [0, 1, 2, 3]; +var a = ${expectLint('list1?.elementAt(list1!.length - 1)')}; + +void test(List? list2) { + ${expectLint('list1?.elementAt(list1!.length - 1)')}; + ${expectLint('list2?.elementAt(list2.length - 1)')}; +} +'''); + } +}