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
12 changes: 12 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import 'package:solid_lints/src/lints/avoid_returning_widgets/avoid_returning_wi
import 'package:solid_lints/src/lints/avoid_returning_widgets/models/avoid_returning_widgets_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/proper_super_calls/proper_super_calls_rule.dart';

/// The entry point for the Solid Lints analyser server plugin.
Expand All @@ -31,6 +33,8 @@ class SolidLintsPlugin extends Plugin {
final analysisLoader = AnalysisOptionsLoader();

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

final lintRules = [
AvoidFinalWithGetterRule(),
AvoidGlobalStateRule(),
Expand All @@ -42,6 +46,10 @@ class SolidLintsPlugin extends Plugin {
analysisOptionsLoader: analysisLoader,
parametersParser: AvoidReturningWidgetsParameters.fromJson,
),
preferFirstRule,
// TODO: Add more lint rules and use analysisLoader
// for rules that need parameters
// For example: `CyclomaticComplexityRule(analysisLoader)`
];

for (final lintRule in lintRules) {
Expand All @@ -56,5 +64,9 @@ class SolidLintsPlugin extends Plugin {
AvoidFinalWithGetterRule.code,
AvoidFinalWithGetterFix.new,
);
registry.registerFixForRule(
preferFirstRule.diagnosticCode,
PreferFirstFix.new,
);
}
}
97 changes: 50 additions & 47 deletions lib/src/lints/prefer_first/fixes/prefer_first_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_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<Diagnostic> 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<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);
}
Comment thread
andrew-bekhiet-solid marked this conversation as resolved.

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<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,
);
});
}
}
47 changes: 16 additions & 31 deletions lib/src/lints/prefer_first/prefer_first_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_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
Expand Down Expand Up @@ -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<Fix> getFixes() => [PreferFirstFix()];
}
26 changes: 13 additions & 13 deletions lib/src/lints/prefer_first/visitors/prefer_first_visitor.dart
Original file line number Diff line number Diff line change
@@ -1,40 +1,40 @@
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<void> {
final _expressions = <Expression>[];
final PreferFirstRule _rule;

/// List of all Iterable access expressions
Iterable<Expression> get expressions => _expressions;
/// Creates a new instance of [PreferFirstVisitor]
PreferFirstVisitor(this._rule);

@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 (!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);
}
}

@override
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);
}
}
}
21 changes: 0 additions & 21 deletions lint_test/prefer_first_test.dart

This file was deleted.

Loading
Loading