Skip to content

Commit

Permalink
Added prefer first rule (#60)
Browse files Browse the repository at this point in the history
* Add prefer-conditional-expressions rule and fix

* Add tests for prefer-conditional-expressions rule

* fix nested test plugin path

* Fix tests after merge

* Add prefer-first rule and fix

* Add tests for prefer-first rule

---------

Co-authored-by: Denis Bogatirov <[email protected]>
Co-authored-by: Yurii Prykhodko <[email protected]>
  • Loading branch information
3 people authored Sep 22, 2023
1 parent 051ab41 commit 7e1deaf
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 0 deletions.
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);
}

0 comments on commit 7e1deaf

Please sign in to comment.