Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -34,6 +36,7 @@ class SolidLintsPlugin extends Plugin {

final doubleLiteralFormatRule = DoubleLiteralFormatRule();
final preferFirstRule = PreferFirstRule();
final preferLastRule = PreferLastRule();

final lintRules = [
AvoidFinalWithGetterRule(),
Expand All @@ -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)`
Expand All @@ -68,5 +72,10 @@ class SolidLintsPlugin extends Plugin {
preferFirstRule.diagnosticCode,
PreferFirstFix.new,
);

registry.registerFixForRule(
preferLastRule.diagnosticCode,
PreferLastFix.new,
);
}
}
97 changes: 50 additions & 47 deletions lib/src/lints/prefer_last/fixes/prefer_last_fix.dart
Original file line number Diff line number Diff line change
@@ -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<Diagnostic> 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<void> 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we also add test cases for cascades and null-awares, I think that only makes sense?

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';
}
}
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.

void _addReplacement(
ChangeReporter reporter,
Expression node,
Future<void> _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,
);
});
}
}
45 changes: 15 additions & 30 deletions lib/src/lints/prefer_last/prefer_last_rule.dart
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<Fix> getFixes() => [PreferLastFix()];
}
45 changes: 24 additions & 21 deletions lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart
Original file line number Diff line number Diff line change
@@ -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<void> {
final _expressions = <Expression>[];
final PreferLastRule _rule;

/// List of all Iterable access expressions
Iterable<Expression> 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.firstOrNull;

if (arg is BinaryExpression &&
_isLastElementAccess(arg, target.toString())) {
_rule.reportAtNode(node);
}
}

Expand All @@ -34,24 +36,25 @@ class PreferLastVisitor extends RecursiveAstVisitor<void> {

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';
}
Expand Down
25 changes: 0 additions & 25 deletions lint_test/prefer_last_test.dart

This file was deleted.

Loading
Loading