Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added prefer first rule #60

Merged
67 changes: 67 additions & 0 deletions lib/lints/prefer_first/prefer_first_fix.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer/source/source_range.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';

/// A Quick fix for `prefer-first` rule
/// Suggests to replace iterable access expressions
class PreferFirstFix extends DartFix {
static const _replaceComment = "Replace with 'first'.";

@override
void run(
CustomLintResolver resolver,
ChangeReporter reporter,
CustomLintContext context,
AnalysisError analysisError,
List<AnalysisError> others,
) {
context.registry.addMethodInvocation((node) {
if (analysisError.sourceRange.intersects(node.sourceRange)) {
final correction = _createCorrection(node);

_addReplacement(reporter, node, correction);
}
});

context.registry.addIndexExpression((node) {
if (analysisError.sourceRange.intersects(node.sourceRange)) {
final correction = _createCorrection(node);

_addReplacement(reporter, node, 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';
}
}

void _addReplacement(
ChangeReporter reporter,
Expression node,
String correction,
) {
final changeBuilder = reporter.createChangeBuilder(
message: _replaceComment,
priority: 1,
);

changeBuilder.addDartFileEdit((builder) {
builder.addSimpleReplacement(
SourceRange(node.offset, node.length),
correction,
);
});
}
}
48 changes: 48 additions & 0 deletions lib/lints/prefer_first/prefer_first_rule.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
import 'package:solid_lints/lints/prefer_first/prefer_first_fix.dart';
import 'package:solid_lints/lints/prefer_first/prefer_first_visitor.dart';
import 'package:solid_lints/models/rule_config.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';

/// A `prefer-first` rule which warns about
/// usage of iterable[0] or iterable.elementAt(0)
class PreferFirstRule extends SolidLintRule {
/// The [LintCode] of this lint rule that represents the error if number of
/// parameters reaches the maximum value.
static const lintName = 'prefer-first';

PreferFirstRule._(super.config);

/// 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.',
);

return PreferFirstRule._(config);
}

@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
context.registry.addCompilationUnit((node) {
final visitor = PreferFirstVisitor();
node.accept(visitor);

for (final element in visitor.expressions) {
reporter.reportErrorForNode(code, element);
}
});
}

@override
List<Fix> getFixes() => [PreferFirstFix()];
}
40 changes: 40 additions & 0 deletions lib/lints/prefer_first/prefer_first_visitor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:solid_lints/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>[];

/// List of all Iterable access expressions
Iterable<Expression> get expressions => _expressions;

@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 (arg is IntegerLiteral && arg.value == 0) {
_expressions.add(node);
}
}
}

@override
void visitIndexExpression(IndexExpression node) {
super.visitIndexExpression(node);

if (isListOrSubclass(node.realTarget.staticType)) {
final index = node.index;

if (index is IntegerLiteral && index.value == 0) {
_expressions.add(node);
}
}
}
}
2 changes: 2 additions & 0 deletions lib/solid_lints.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import 'package:solid_lints/lints/no_equal_then_else/no_equal_then_else_rule.dar
import 'package:solid_lints/lints/no_magic_number/no_magic_number_rule.dart';
import 'package:solid_lints/lints/number_of_parameters/number_of_parameters_metric.dart';
import 'package:solid_lints/lints/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart';
import 'package:solid_lints/lints/prefer_first/prefer_first_rule.dart';
import 'package:solid_lints/models/solid_lint_rule.dart';

/// Creates a plugin for our custom linter
Expand Down Expand Up @@ -49,6 +50,7 @@ class _SolidLints extends PluginBase {
MemberOrderingRule.createRule(configs),
NoMagicNumberRule.createRule(configs),
PreferConditionalExpressionsRule.createRule(configs),
PreferFirstRule.createRule(configs),
];

// Return only enabled rules
Expand Down
7 changes: 7 additions & 0 deletions lib/utils/types_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import 'package:analyzer/dart/element/nullability_suffix.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:collection/collection.dart';

bool hasWidgetType(DartType type) =>
(isWidgetOrSubclass(type) ||
Expand Down Expand Up @@ -162,3 +163,9 @@ bool _isFutureInheritedProvider(DartType type) =>
type.isDartAsyncFuture &&
type is InterfaceType &&
_isSubclassOfInheritedProvider(type.typeArguments.firstOrNull);

bool isIterableOrSubclass(DartType? type) =>
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreIterable ?? false);

bool isListOrSubclass(DartType? type) =>
_checkSelfOrSupertypes(type, (t) => t?.isDartCoreList ?? false);
1 change: 1 addition & 0 deletions lint_test/analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ custom_lint:
- dispose-method
- no-magic-number
- prefer-conditional-expressions
- prefer-first
21 changes: 21 additions & 0 deletions lint_test/prefer_first_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/// 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);
}