Skip to content
This repository was archived by the owner on Jul 16, 2023. It is now read-only.

Commit 5dbb174

Browse files
authored
feat: Prefer first (#497)
* feat: implement `prefer-first` rule * chore: update changelog * fix: add missed trailing dots * feat: add some test cases
1 parent fd16bc0 commit 5dbb174

File tree

14 files changed

+602
-32
lines changed

14 files changed

+602
-32
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* chore: changed min `SDK` version to `2.14.0`.
66
* chore: restrict `analyzer` version to `>=2.4.0 <2.6.0`.
77
* chore: changed the supported `analyzer_plugin` version to `^0.8.0`.
8-
* feat: add static code diagnostic `prefer-correct-identifier-length`.
8+
* feat: add static code diagnostic `prefer-correct-identifier-length`, `prefer-first`.
99

1010
## 4.4.0
1111

lib/src/analyzers/lint_analyzer/rules/rules_factory.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import 'rules_list/prefer_conditional_expressions/prefer_conditional_expressions
2424
import 'rules_list/prefer_const_border_radius/prefer_const_border_radius.dart';
2525
import 'rules_list/prefer_correct_identifier_length/prefer_correct_identifier_length.dart';
2626
import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks.dart';
27+
import 'rules_list/prefer_first/prefer_first.dart';
2728
import 'rules_list/prefer_intl_name/prefer_intl_name.dart';
2829
import 'rules_list/prefer_match_file_name/prefer_match_file_name.dart';
2930
import 'rules_list/prefer_on_push_cd_strategy/prefer_on_push_cd_strategy.dart';
@@ -72,6 +73,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
7273
PreferCorrectIdentifierLength(config),
7374
PreferExtractingCallbacksRule.ruleId: (config) =>
7475
PreferExtractingCallbacksRule(config),
76+
PreferFirstRule.ruleId: (config) => PreferFirstRule(config),
7577
PreferIntlNameRule.ruleId: (config) => PreferIntlNameRule(config),
7678
PreferMatchFileName.ruleId: (config) => PreferMatchFileName(config),
7779
PreferOnPushCdStrategyRule.ruleId: (config) =>

lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_conditional_expressions/prefer_conditional_expressions.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ part 'visitor.dart';
1919
class PreferConditionalExpressionsRule extends CommonRule {
2020
static const String ruleId = 'prefer-conditional-expressions';
2121

22-
static const _warningMessage = 'Prefer conditional expression';
23-
static const _correctionMessage = 'Convert to conditional expression';
22+
static const _warningMessage = 'Prefer conditional expression.';
23+
static const _correctionMessage = 'Convert to conditional expression.';
2424

2525
PreferConditionalExpressionsRule([Map<String, Object> config = const {}])
2626
: super(

lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_const_border_radius/prefer_const_border_radius.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ part 'visitor.dart';
1717
class PreferConstBorderRadiusRule extends FlutterRule {
1818
static const ruleId = 'prefer-const-border-radius';
1919
static const _issueMessage =
20-
'Prefer using const constructor BorderRadius.all';
20+
'Prefer using const constructor BorderRadius.all.';
2121
static const _replaceComment = 'Replace with const BorderRadius constructor.';
2222

2323
PreferConstBorderRadiusRule([Map<String, Object> config = const {}])
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:analyzer/dart/ast/ast.dart';
2+
import 'package:analyzer/dart/ast/visitor.dart';
3+
import 'package:analyzer/dart/element/type.dart';
4+
5+
import '../../../../../utils/node_utils.dart';
6+
import '../../../lint_utils.dart';
7+
import '../../../models/internal_resolved_unit_result.dart';
8+
import '../../../models/issue.dart';
9+
import '../../../models/replacement.dart';
10+
import '../../../models/severity.dart';
11+
import '../../models/common_rule.dart';
12+
import '../../models/rule_documentation.dart';
13+
import '../../rule_utils.dart';
14+
15+
part 'visitor.dart';
16+
17+
class PreferFirstRule extends CommonRule {
18+
static const ruleId = 'prefer-first';
19+
static const _warningMessage =
20+
'Use first instead of access to element at zero index.';
21+
static const _replaceComment = "Replace with 'first'.";
22+
23+
PreferFirstRule([Map<String, Object> config = const {}])
24+
: super(
25+
id: ruleId,
26+
documentation: const RuleDocumentation(
27+
name: 'Prefer first',
28+
brief: 'Use `first` to gets the first element',
29+
),
30+
severity: readSeverity(config, Severity.style),
31+
excludes: readExcludes(config),
32+
);
33+
34+
@override
35+
Iterable<Issue> check(InternalResolvedUnitResult source) {
36+
final visitor = _Visitor();
37+
source.unit.visitChildren(visitor);
38+
39+
return visitor.expressions
40+
.map((expression) => createIssue(
41+
rule: this,
42+
location: nodeLocation(
43+
node: expression,
44+
source: source,
45+
withCommentOrMetadata: true,
46+
),
47+
message: _warningMessage,
48+
replacement: _createReplacement(expression),
49+
))
50+
.toList(growable: false);
51+
}
52+
53+
Replacement _createReplacement(Expression expression) {
54+
String replacement;
55+
56+
if (expression is MethodInvocation) {
57+
replacement = expression.isCascaded
58+
? '..first'
59+
: '${expression.target ?? ''}.first';
60+
} else if (expression is IndexExpression) {
61+
replacement = expression.isCascaded
62+
? '..first'
63+
: '${expression.target ?? ''}.first';
64+
} else {
65+
replacement = '.first';
66+
}
67+
68+
return Replacement(
69+
comment: _replaceComment,
70+
replacement: replacement,
71+
);
72+
}
73+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
part of 'prefer_first.dart';
2+
3+
class _Visitor extends RecursiveAstVisitor<void> {
4+
final _expressions = <Expression>[];
5+
6+
Iterable<Expression> get expressions => _expressions;
7+
8+
@override
9+
void visitMethodInvocation(MethodInvocation node) {
10+
super.visitMethodInvocation(node);
11+
12+
if (_isIterableOrSubclass(node.realTarget?.staticType) &&
13+
node.methodName.name == 'elementAt') {
14+
final arg = node.argumentList.arguments.first;
15+
16+
if (arg is IntegerLiteral && arg.value == 0) {
17+
_expressions.add(node);
18+
}
19+
}
20+
}
21+
22+
@override
23+
void visitIndexExpression(IndexExpression node) {
24+
super.visitIndexExpression(node);
25+
26+
if (_isListOrSubclass(node.realTarget.staticType)) {
27+
final index = node.index;
28+
29+
if (index is IntegerLiteral && index.value == 0) {
30+
_expressions.add(node);
31+
}
32+
}
33+
}
34+
35+
bool _isIterableOrSubclass(DartType? type) =>
36+
_isIterable(type) || _isSubclassOfIterable(type);
37+
38+
bool _isIterable(DartType? type) => type?.isDartCoreIterable ?? false;
39+
40+
bool _isSubclassOfIterable(DartType? type) =>
41+
type is InterfaceType && type.allSupertypes.any(_isIterable);
42+
43+
bool _isListOrSubclass(DartType? type) =>
44+
_isList(type) || _isSubclassOfList(type);
45+
46+
bool _isList(DartType? type) => type?.isDartCoreList ?? false;
47+
48+
bool _isSubclassOfList(DartType? type) =>
49+
type is InterfaceType && type.allSupertypes.any(_isList);
50+
}

lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_intl_name/prefer_intl_name.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class PreferIntlNameRule extends IntlRule {
2222
static const _intlPackageUrl = 'package:intl/intl.dart';
2323
static const _notCorrectNameFailure = 'Incorrect Intl name, should be';
2424
static const _notCorrectNameCorrectionComment = 'Rename';
25-
static const _notExistsNameFailure = 'Argument `name` does not exists';
25+
static const _notExistsNameFailure = 'Argument `name` does not exists.';
2626

2727
PreferIntlNameRule([Map<String, Object> config = const {}])
2828
: super(
@@ -61,7 +61,7 @@ class PreferIntlNameRule extends IntlRule {
6161
source: source,
6262
withCommentOrMetadata: true,
6363
),
64-
message: '$_notCorrectNameFailure $correction',
64+
message: '$_notCorrectNameFailure $correction.',
6565
replacement: Replacement(
6666
comment: _notCorrectNameCorrectionComment,
6767
replacement: correction,

lib/src/analyzers/lint_analyzer/rules/rules_list/prefer_match_file_name/prefer_match_file_name.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ part 'visitor.dart';
1616
class PreferMatchFileName extends CommonRule {
1717
static const String ruleId = 'prefer-match-file-name';
1818
static const _notMatchNameFailure =
19-
'File name does not match with first class name';
19+
'File name does not match with first class name.';
2020
static final _onlySymbolsRegex = RegExp('[^a-zA-Z0-9]');
2121

2222
PreferMatchFileName([Map<String, Object> config = const {}])

test/analyzers/lint_analyzer/rules/rules_list/prefer_conditional_expressions/prefer_conditional_expressions_test.dart

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,14 @@ void main() {
6969
' return a;',
7070
],
7171
messages: [
72-
'Prefer conditional expression',
73-
'Prefer conditional expression',
74-
'Prefer conditional expression',
75-
'Prefer conditional expression',
76-
'Prefer conditional expression',
77-
'Prefer conditional expression',
78-
'Prefer conditional expression',
79-
'Prefer conditional expression',
72+
'Prefer conditional expression.',
73+
'Prefer conditional expression.',
74+
'Prefer conditional expression.',
75+
'Prefer conditional expression.',
76+
'Prefer conditional expression.',
77+
'Prefer conditional expression.',
78+
'Prefer conditional expression.',
79+
'Prefer conditional expression.',
8080
],
8181
replacements: [
8282
'a = a == 3 ? 2 : 3;',
@@ -89,14 +89,14 @@ void main() {
8989
'return a == 20 ? 2 : a;',
9090
],
9191
replacementComments: [
92-
'Convert to conditional expression',
93-
'Convert to conditional expression',
94-
'Convert to conditional expression',
95-
'Convert to conditional expression',
96-
'Convert to conditional expression',
97-
'Convert to conditional expression',
98-
'Convert to conditional expression',
99-
'Convert to conditional expression',
92+
'Convert to conditional expression.',
93+
'Convert to conditional expression.',
94+
'Convert to conditional expression.',
95+
'Convert to conditional expression.',
96+
'Convert to conditional expression.',
97+
'Convert to conditional expression.',
98+
'Convert to conditional expression.',
99+
'Convert to conditional expression.',
100100
],
101101
);
102102
});

test/analyzers/lint_analyzer/rules/rules_list/prefer_const_border_radius/prefer_const_border_radius_test.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ void main() {
3232
startColumns: [22, 31, 31, 32, 31, 31],
3333
endOffsets: [108, 268, 393, 460, 608, 898],
3434
messages: [
35-
'Prefer using const constructor BorderRadius.all',
36-
'Prefer using const constructor BorderRadius.all',
37-
'Prefer using const constructor BorderRadius.all',
38-
'Prefer using const constructor BorderRadius.all',
39-
'Prefer using const constructor BorderRadius.all',
40-
'Prefer using const constructor BorderRadius.all',
35+
'Prefer using const constructor BorderRadius.all.',
36+
'Prefer using const constructor BorderRadius.all.',
37+
'Prefer using const constructor BorderRadius.all.',
38+
'Prefer using const constructor BorderRadius.all.',
39+
'Prefer using const constructor BorderRadius.all.',
40+
'Prefer using const constructor BorderRadius.all.',
4141
],
4242
replacementComments: [
4343
'Replace with const BorderRadius constructor.',

0 commit comments

Comments
 (0)