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) {
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.
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()];
}
102 changes: 70 additions & 32 deletions lib/src/lints/prefer_last/visitors/prefer_last_visitor.dart
Original file line number Diff line number Diff line change
@@ -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<void> {
final _expressions = <Expression>[];
static const _lengthGetterName = 'length';

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

Expand All @@ -34,41 +37,76 @@ 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)) {
_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();
}

return expression.toString();
}
Expression _unwrapExpression(Expression expression) {
switch (expression) {
case PostfixExpression(
:final operand,
operator: Token(type: TokenType.BANG),
):
return _unwrapExpression(operand);

return null;
case ParenthesizedExpression(:final expression):
return _unwrapExpression(expression);

default:
return expression;
}
}
}
25 changes: 0 additions & 25 deletions lint_test/prefer_last_test.dart

This file was deleted.

Loading
Loading