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

Commit c2b4704

Browse files
feat: add static code diagnostics prefer-correct-edge-insets (#756)
* feat: add static code diagnostics `prefer-correct-edge-insets-constructor-rule`. * feat: added tests * feat: added tests * chore: review changes * feat: suppoort double * feat: suppoort double * feat: review changes * feat: added correct cases * feat: remove spaces * feat: review changes * feat: added tests * feat: formatted code * feat: support for STEB constructor * fix: unit test * feat: refactored code * feat: review changes * fix: bugs * fix: review changes * feat: fix test * feat: fix test Co-authored-by: Dmitry Krutskikh <dmitry.krutskikh@gmail.com>
1 parent 9fd3b58 commit c2b4704

File tree

16 files changed

+1181
-0
lines changed

16 files changed

+1181
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
* feat: add static code diagnostic [`avoid-duplicate-exports`](https://dartcodemetrics.dev/docs/rules/common/avoid-duplicate-exports).
66
* feat: add static code diagnostic [`avoid-top-level-members-in-tests`](https://dartcodemetrics.dev/docs/rules/common/avoid-top-level-members-in-tests).
7+
* feat: add static code diagnostic [`prefer-correct-edge-insets-constructor-rule`](https://dartcodemetrics.dev/docs/rules/flutter/prefer-correct-edge-insets-constructor).
78
* feat: add static code diagnostic [`prefer-enums-by-name`](https://dartcodemetrics.dev/docs/rules/common/prefer-enums-by-name).
89

910
## 4.17.0-dev.1

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import 'rules_list/prefer_async_await/prefer_async_await_rule.dart';
4141
import 'rules_list/prefer_commenting_analyzer_ignores/prefer_commenting_analyzer_ignores.dart';
4242
import 'rules_list/prefer_conditional_expressions/prefer_conditional_expressions_rule.dart';
4343
import 'rules_list/prefer_const_border_radius/prefer_const_border_radius_rule.dart';
44+
import 'rules_list/prefer_correct_edge_insets_constructor/prefer_correct_edge_insets_constructor_rule.dart';
4445
import 'rules_list/prefer_correct_identifier_length/prefer_correct_identifier_length_rule.dart';
4546
import 'rules_list/prefer_correct_type_name/prefer_correct_type_name_rule.dart';
4647
import 'rules_list/prefer_enums_by_name/prefer_enums_by_name_rule.dart';
@@ -105,6 +106,8 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
105106
PreferCommentingAnalyzerIgnores.ruleId: PreferCommentingAnalyzerIgnores.new,
106107
PreferConditionalExpressionsRule.ruleId: PreferConditionalExpressionsRule.new,
107108
PreferConstBorderRadiusRule.ruleId: PreferConstBorderRadiusRule.new,
109+
PreferCorrectEdgeInsetsConstructorRule.ruleId:
110+
PreferCorrectEdgeInsetsConstructorRule.new,
108111
PreferCorrectIdentifierLengthRule.ruleId:
109112
PreferCorrectIdentifierLengthRule.new,
110113
PreferCorrectTypeNameRule.ruleId: PreferCorrectTypeNameRule.new,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
part of '../prefer_correct_edge_insets_constructor_rule.dart';
2+
3+
@immutable
4+
class EdgeInsetsData {
5+
final String className;
6+
final String constructorName;
7+
final List<EdgeInsetsParam> params;
8+
9+
const EdgeInsetsData(this.className, this.constructorName, this.params);
10+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
part of '../prefer_correct_edge_insets_constructor_rule.dart';
2+
3+
@immutable
4+
class EdgeInsetsParam {
5+
final String? name;
6+
final num? value;
7+
8+
const EdgeInsetsParam({required this.value, this.name});
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// ignore_for_file: public_member_api_docs
2+
3+
import 'package:analyzer/dart/ast/ast.dart';
4+
import 'package:analyzer/dart/ast/visitor.dart';
5+
import 'package:collection/collection.dart';
6+
import 'package:meta/meta.dart';
7+
8+
import '../../../../../utils/node_utils.dart';
9+
import '../../../lint_utils.dart';
10+
import '../../../models/internal_resolved_unit_result.dart';
11+
import '../../../models/issue.dart';
12+
import '../../../models/replacement.dart';
13+
import '../../../models/severity.dart';
14+
import '../../models/flutter_rule.dart';
15+
import '../../rule_utils.dart';
16+
17+
part 'validator.dart';
18+
part 'visitor.dart';
19+
part 'models/edge_insets_param.dart';
20+
part 'models/edge_insets_data.dart';
21+
22+
class PreferCorrectEdgeInsetsConstructorRule extends FlutterRule {
23+
static const ruleId = 'prefer-correct-edge-insets-constructor';
24+
static const _issueMessage = 'Prefer using correct EdgeInsets constructor.';
25+
26+
PreferCorrectEdgeInsetsConstructorRule([
27+
Map<String, Object> config = const {},
28+
]) : super(
29+
id: ruleId,
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+
final validator = _Validator();
38+
39+
source.unit.visitChildren(visitor);
40+
validator.validate(visitor.expressions);
41+
42+
return validator.expressions.entries
43+
.map((expression) => createIssue(
44+
rule: this,
45+
location: nodeLocation(
46+
node: expression.key,
47+
source: source,
48+
),
49+
message: _issueMessage,
50+
replacement: _createReplacement(expression.value),
51+
))
52+
.toList(growable: false);
53+
}
54+
55+
Replacement? _createReplacement(String expression) => Replacement(
56+
comment: 'Prefer use $expression',
57+
replacement: expression,
58+
);
59+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
part of 'prefer_correct_edge_insets_constructor_rule.dart';
2+
3+
class _Validator {
4+
final _exceptions = <InstanceCreationExpression, String>{};
5+
6+
_Validator();
7+
8+
Map<InstanceCreationExpression, String> get expressions => _exceptions;
9+
10+
void validate(Map<InstanceCreationExpression, EdgeInsetsData> expressions) {
11+
for (final expression in expressions.entries) {
12+
final data = expression.value;
13+
14+
String? exceptionValue;
15+
switch (data.constructorName) {
16+
case _constructorNameFromSTEB:
17+
case _constructorNameFromLTRB:
18+
exceptionValue = _validateFromSTEB(data);
19+
break;
20+
case _constructorNameSymmetric:
21+
exceptionValue = _validateSymmetric(data);
22+
break;
23+
case _constructorNameOnly:
24+
exceptionValue = _validateFromOnly(data);
25+
break;
26+
}
27+
28+
if (exceptionValue != null) {
29+
_exceptions[expression.key] = exceptionValue;
30+
}
31+
}
32+
}
33+
34+
String? _validateSymmetric(EdgeInsetsData data) {
35+
final param = data.params;
36+
final vertical = param.firstWhereOrNull((e) => e.name == 'vertical')?.value;
37+
final horizontal =
38+
param.firstWhereOrNull((e) => e.name == 'horizontal')?.value;
39+
40+
final isParamsSame = vertical == horizontal && horizontal != null;
41+
final isAllParamsZero = isParamsSame && horizontal == 0;
42+
43+
if (isAllParamsZero) {
44+
return _replaceWithZero();
45+
}
46+
47+
if (isParamsSame) {
48+
return '${data.className}.all($horizontal)';
49+
}
50+
51+
if (horizontal == 0 && vertical != null) {
52+
return _replaceWithSymmetric('vertical: $vertical');
53+
}
54+
if (vertical == 0 && horizontal != null) {
55+
return _replaceWithSymmetric('horizontal: $horizontal');
56+
}
57+
58+
return null;
59+
}
60+
61+
String? _validateFromOnly(EdgeInsetsData data) {
62+
{
63+
final param = data.params;
64+
final top = param.firstWhereOrNull((e) => e.name == 'top')?.value;
65+
final bottom = param.firstWhereOrNull((e) => e.name == 'bottom')?.value;
66+
final left = param
67+
.firstWhereOrNull((e) => e.name == 'left' || e.name == 'start')
68+
?.value;
69+
final right = param
70+
.firstWhereOrNull((e) => e.name == 'right' || e.name == 'end')
71+
?.value;
72+
73+
final paramsList = [top, bottom, left, right];
74+
final hasLeftParam = left != 0 && left != null;
75+
final hasTopParam = top != 0 && top != null;
76+
final hasBottomParam = bottom != 0 && bottom != null;
77+
final hasRightParam = right != 0 && right != null;
78+
if (paramsList.every((element) => element == 0)) {
79+
return _replaceWithZero();
80+
}
81+
82+
if (paramsList.every((element) => element == top && top != null)) {
83+
return '${data.className}.all(${data.params.first.value})';
84+
}
85+
86+
if (left == right && hasLeftParam && top == bottom && hasTopParam) {
87+
final params = 'horizontal: $right, vertical: $top';
88+
89+
return _replaceWithSymmetric(params);
90+
}
91+
92+
if (top == bottom && top != 0 && !hasLeftParam && !hasRightParam) {
93+
return _replaceWithSymmetric('vertical: $top');
94+
}
95+
96+
if (left == right && right != 0 && !hasTopParam && !hasBottomParam) {
97+
return _replaceWithSymmetric('horizontal: $right');
98+
}
99+
100+
if (paramsList.contains(0)) {
101+
return '${data.className}.only(${[
102+
if (hasTopParam) 'top: $top',
103+
if (hasBottomParam) 'bottom: $bottom',
104+
if (hasLeftParam && data.className == 'EdgeInsetsDirectional')
105+
'start: $left',
106+
if (hasLeftParam && data.className == 'EdgeInsets') 'left: $left',
107+
if (hasRightParam && data.className == 'EdgeInsetsDirectional')
108+
'end: $right',
109+
if (hasRightParam && data.className == 'EdgeInsets') 'right: $right',
110+
].join(', ')})';
111+
}
112+
}
113+
114+
return null;
115+
}
116+
117+
String? _validateFromSTEB(
118+
EdgeInsetsData data,
119+
) {
120+
if (data.params.every((element) => element.value == 0)) {
121+
return _replaceWithZero();
122+
}
123+
124+
if (data.params
125+
.every((element) => element.value == data.params.first.value)) {
126+
return '${data.className}.all(${data.params.first.value})';
127+
}
128+
129+
final left = data.params.first.value;
130+
final top = data.params.elementAt(1).value;
131+
final right = data.params.elementAt(2).value;
132+
final bottom = data.params.elementAt(3).value;
133+
134+
if (left == right && top == bottom) {
135+
final params = <String>[];
136+
if (left != 0) {
137+
params.add('horizontal: $left');
138+
}
139+
if (top != 0) {
140+
params.add('vertical: $top');
141+
}
142+
143+
return _replaceWithSymmetric(params.join(', '));
144+
}
145+
146+
if (data.params.any((element) => element.value == 0)) {
147+
final params = <String>[];
148+
149+
if (left != 0) {
150+
if (data.constructorName == _constructorNameFromLTRB) {
151+
params.add('left: $left');
152+
} else {
153+
params.add('start: $left');
154+
}
155+
}
156+
if (top != 0) {
157+
params.add('top: $top');
158+
}
159+
160+
if (right != 0) {
161+
if (data.constructorName == _constructorNameFromLTRB) {
162+
params.add('right: $right');
163+
} else {
164+
params.add('end: $right');
165+
}
166+
}
167+
168+
if (bottom != 0) {
169+
params.add('bottom: $bottom');
170+
}
171+
172+
return '${data.className}.only(${params.join(', ')})';
173+
}
174+
175+
return null;
176+
}
177+
178+
String _replaceWithZero() => 'EdgeInsets.zero';
179+
180+
String _replaceWithSymmetric(String? param) => 'EdgeInsets.symmetric($param)';
181+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
part of 'prefer_correct_edge_insets_constructor_rule.dart';
2+
3+
const _className = 'EdgeInsets';
4+
const _classNameDirection = 'EdgeInsetsDirectional';
5+
6+
const _constructorNameFromLTRB = 'fromLTRB';
7+
const _constructorNameFromSTEB = 'fromSTEB';
8+
const _constructorNameSymmetric = 'symmetric';
9+
const _constructorNameOnly = 'only';
10+
11+
class _Visitor extends RecursiveAstVisitor<void> {
12+
final _expressions = <InstanceCreationExpression, EdgeInsetsData>{};
13+
14+
final _Validator validator = _Validator();
15+
16+
Map<InstanceCreationExpression, EdgeInsetsData> get expressions =>
17+
_expressions;
18+
19+
@override
20+
void visitInstanceCreationExpression(InstanceCreationExpression expression) {
21+
super.visitInstanceCreationExpression(expression);
22+
final className = expression.staticType?.getDisplayString(
23+
withNullability: true,
24+
);
25+
final constructorName = expression.constructorName.name?.name;
26+
27+
if (className == _className || className == _classNameDirection) {
28+
switch (constructorName) {
29+
case _constructorNameOnly:
30+
case _constructorNameSymmetric:
31+
_parseNamedParams(expression, constructorName, className);
32+
break;
33+
case _constructorNameFromLTRB:
34+
case _constructorNameFromSTEB:
35+
_parsePositionParams(expression, constructorName, className);
36+
break;
37+
}
38+
}
39+
}
40+
41+
void _parseNamedParams(
42+
InstanceCreationExpression expression,
43+
String? constructorName,
44+
String? className,
45+
) {
46+
final argumentsList = <EdgeInsetsParam?>[];
47+
for (final expression in expression.argumentList.arguments) {
48+
final variable = expression.childEntities.last;
49+
if (variable is IntegerLiteral || variable is DoubleLiteral) {
50+
final name = expression.beginToken.toString();
51+
final value = expression.endToken.toString();
52+
53+
argumentsList.add(EdgeInsetsParam(
54+
name: name,
55+
value: num.tryParse(value),
56+
));
57+
} else {
58+
argumentsList.add(null);
59+
}
60+
}
61+
62+
if (!argumentsList.contains(null)) {
63+
final param = argumentsList.whereType<EdgeInsetsParam>().toList();
64+
_expressions[expression] = EdgeInsetsData(
65+
className ?? '',
66+
constructorName ?? '',
67+
param,
68+
);
69+
}
70+
}
71+
72+
void _parsePositionParams(
73+
InstanceCreationExpression expression,
74+
String? constructorName,
75+
String? className,
76+
) {
77+
final arguments = expression.argumentList.arguments;
78+
if (arguments.length == 4 &&
79+
arguments.every(
80+
(element) => element is IntegerLiteral || element is DoubleLiteral,
81+
)) {
82+
final argumentsList = arguments
83+
.map((e) => EdgeInsetsParam(value: num.tryParse(e.toString())))
84+
.toList();
85+
86+
_expressions[expression] = EdgeInsetsData(
87+
className ?? '',
88+
constructorName ?? '',
89+
argumentsList,
90+
);
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)