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

Commit a254d6e

Browse files
feat: add static code diagnostic prefer-iterable-of (#999)
Co-authored-by: Dmitry Krutskikh <[email protected]>
1 parent 36baaa5 commit a254d6e

File tree

15 files changed

+654
-0
lines changed

15 files changed

+654
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
* feat: add static code diagnostic [`prefer-iterable-of`](https://dartcodemetrics.dev/docs/rules/common/prefer-iterable-of).
6+
37
## 4.18.3
48

59
* fix: fix regression in is! checks for [`avoid-unnecessary-type-assertions`](https://dartcodemetrics.dev/docs/rules/common/avoid-unnecessary-type-assertions).

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import 'rules_list/prefer_extracting_callbacks/prefer_extracting_callbacks_rule.
5151
import 'rules_list/prefer_first/prefer_first_rule.dart';
5252
import 'rules_list/prefer_immediate_return/prefer_immediate_return_rule.dart';
5353
import 'rules_list/prefer_intl_name/prefer_intl_name_rule.dart';
54+
import 'rules_list/prefer_iterable_of/prefer_iterable_of_rule.dart';
5455
import 'rules_list/prefer_last/prefer_last_rule.dart';
5556
import 'rules_list/prefer_match_file_name/prefer_match_file_name_rule.dart';
5657
import 'rules_list/prefer_moving_to_variable/prefer_moving_to_variable_rule.dart';
@@ -121,6 +122,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
121122
PreferFirstRule.ruleId: PreferFirstRule.new,
122123
PreferImmediateReturnRule.ruleId: PreferImmediateReturnRule.new,
123124
PreferIntlNameRule.ruleId: PreferIntlNameRule.new,
125+
PreferIterableOfRule.ruleId: PreferIterableOfRule.new,
124126
PreferLastRule.ruleId: PreferLastRule.new,
125127
PreferMatchFileNameRule.ruleId: PreferMatchFileNameRule.new,
126128
PreferMovingToVariableRule.ruleId: PreferMovingToVariableRule.new,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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:analyzer/dart/element/nullability_suffix.dart';
6+
import 'package:analyzer/dart/element/type.dart';
7+
import 'package:collection/collection.dart';
8+
9+
import '../../../../../utils/dart_types_utils.dart';
10+
import '../../../../../utils/node_utils.dart';
11+
import '../../../lint_utils.dart';
12+
import '../../../models/internal_resolved_unit_result.dart';
13+
import '../../../models/issue.dart';
14+
import '../../../models/replacement.dart';
15+
import '../../../models/severity.dart';
16+
import '../../models/common_rule.dart';
17+
import '../../rule_utils.dart';
18+
19+
part 'visitor.dart';
20+
21+
class PreferIterableOfRule extends CommonRule {
22+
static const ruleId = 'prefer-iterable-of';
23+
24+
static const _warningMessage = 'Prefer using .of';
25+
static const _replaceComment = "Replace with 'of'.";
26+
27+
PreferIterableOfRule([Map<String, Object> config = const {}])
28+
: super(
29+
id: ruleId,
30+
severity: readSeverity(config, Severity.warning),
31+
excludes: readExcludes(config),
32+
);
33+
34+
@override
35+
Iterable<Issue> check(InternalResolvedUnitResult source) {
36+
final visitor = _Visitor();
37+
38+
source.unit.visitChildren(visitor);
39+
40+
return visitor.expressions
41+
.map((expression) => createIssue(
42+
rule: this,
43+
location: nodeLocation(node: expression, source: source),
44+
message: _warningMessage,
45+
replacement: Replacement(
46+
comment: _replaceComment,
47+
replacement: expression.toString().replaceAll('.from', '.of'),
48+
),
49+
))
50+
.toList(growable: false);
51+
}
52+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// ignore_for_file: deprecated_member_use
2+
3+
part of 'prefer_iterable_of_rule.dart';
4+
5+
class _Visitor extends RecursiveAstVisitor<void> {
6+
final _expressions = <InstanceCreationExpression>[];
7+
8+
Iterable<InstanceCreationExpression> get expressions => _expressions;
9+
10+
@override
11+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
12+
super.visitInstanceCreationExpression(node);
13+
14+
if (isIterableOrSubclass(node.staticType) &&
15+
node.constructorName.name?.name == 'from') {
16+
final arg = node.argumentList.arguments.first;
17+
18+
final argumentType = _getType(arg.staticType);
19+
final castedType = _getType(node.staticType);
20+
21+
if (argumentType != null &&
22+
!argumentType.isDartCoreObject &&
23+
!argumentType.isDynamic &&
24+
_isUnnecessaryTypeCheck(castedType, argumentType)) {
25+
_expressions.add(node);
26+
}
27+
}
28+
}
29+
30+
DartType? _getType(DartType? type) {
31+
if (type == null || type is! InterfaceType) {
32+
return null;
33+
}
34+
35+
final typeArgument = type.typeArguments.firstOrNull;
36+
if (typeArgument == null) {
37+
return null;
38+
}
39+
40+
return typeArgument;
41+
}
42+
43+
bool _isUnnecessaryTypeCheck(
44+
DartType? objectType,
45+
DartType? castedType,
46+
) {
47+
if (objectType == null || castedType == null) {
48+
return false;
49+
}
50+
51+
if (objectType == castedType) {
52+
return true;
53+
}
54+
55+
if (_checkNullableCompatibility(objectType, castedType)) {
56+
return false;
57+
}
58+
59+
final objectCastedType =
60+
_foundCastedTypeInObjectTypeHierarchy(objectType, castedType);
61+
if (objectCastedType == null) {
62+
return true;
63+
}
64+
65+
if (!_checkGenerics(objectCastedType, castedType)) {
66+
return false;
67+
}
68+
69+
return false;
70+
}
71+
72+
bool _checkNullableCompatibility(DartType objectType, DartType castedType) {
73+
final isObjectTypeNullable =
74+
objectType.nullabilitySuffix != NullabilitySuffix.none;
75+
final isCastedTypeNullable =
76+
castedType.nullabilitySuffix != NullabilitySuffix.none;
77+
78+
// Only one case `Type? is Type` always valid assertion case.
79+
return isObjectTypeNullable && !isCastedTypeNullable;
80+
}
81+
82+
DartType? _foundCastedTypeInObjectTypeHierarchy(
83+
DartType objectType,
84+
DartType castedType,
85+
) {
86+
if (objectType.element == castedType.element) {
87+
return objectType;
88+
}
89+
90+
if (objectType is InterfaceType) {
91+
return objectType.allSupertypes
92+
.firstWhereOrNull((value) => value.element == castedType.element);
93+
}
94+
95+
return null;
96+
}
97+
98+
bool _checkGenerics(DartType objectType, DartType castedType) {
99+
if (objectType is! ParameterizedType || castedType is! ParameterizedType) {
100+
return false;
101+
}
102+
103+
final length = objectType.typeArguments.length;
104+
if (length != castedType.typeArguments.length) {
105+
return false;
106+
}
107+
108+
for (var argumentIndex = 0; argumentIndex < length; argumentIndex++) {
109+
if (!_isUnnecessaryTypeCheck(
110+
objectType.typeArguments[argumentIndex],
111+
castedType.typeArguments[argumentIndex],
112+
)) {
113+
return false;
114+
}
115+
}
116+
117+
return true;
118+
}
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const queue = DoubleLinkedQueue.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = DoubleLinkedQueue<int>.from(queue); // LINT
7+
final numQueue = DoubleLinkedQueue<num>.from(queue); // LINT
8+
9+
final intQueue = DoubleLinkedQueue<int>.from(numQueue);
10+
11+
final unspecifedQueue = DoubleLinkedQueue.from(queue); // LINT
12+
13+
final dynamicQueue = DoubleLinkedQueue<dynamic>.from([1, 2, 3]);
14+
final copy = DoubleLinkedQueue<int>.from(dynamicQueue);
15+
final dynamicCopy = DoubleLinkedQueue.from(dynamicQueue);
16+
17+
final objectQueue = DoubleLinkedQueue<Object>.from([1, 2, 3]);
18+
final copy = DoubleLinkedQueue<int>.from(objectQueue);
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const hashSet = HashSet.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = HashSet<int>.from(hashSet); // LINT
7+
final numSet = HashSet<num>.from(hashSet); // LINT
8+
9+
final intSet = HashSet<int>.from(numSet);
10+
11+
final unspecifedSet = HashSet.from(hashSet); // LINT
12+
13+
final dynamicSet = HashSet<dynamic>.from([1, 2, 3]);
14+
final copy = HashSet<int>.from(dynamicSet);
15+
final dynamicCopy = HashSet.from(dynamicSet);
16+
17+
final objectSet = HashSet<Object>.from([1, 2, 3]);
18+
final copy = HashSet<int>.from(objectSet);
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const hashSet = LinkedHashSet.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = LinkedHashSet<int>.from(hashSet); // LINT
7+
final numSet = LinkedHashSet<num>.from(hashSet); // LINT
8+
9+
final intSet = LinkedHashSet<int>.from(numSet);
10+
11+
final unspecifedSet = LinkedHashSet.from(hashSet); // LINT
12+
13+
final dynamicSet = LinkedHashSet<dynamic>.from([1, 2, 3]);
14+
final copy = LinkedHashSet<int>.from(dynamicSet);
15+
final dynamicCopy = LinkedHashSet.from(dynamicSet);
16+
17+
final objectSet = LinkedHashSet<Object>.from([1, 2, 3]);
18+
final copy = LinkedHashSet<int>.from(objectSet);
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
void main() {
2+
const array = [1, 2, 3, 4, 5, 6, 7, 8, 9];
3+
4+
final copy = List<int>.from(array); // LINT
5+
final numList = List<num>.from(array); // LINT
6+
7+
final intList = List<int>.from(numList);
8+
9+
final unspecifedList = List.from(array); // LINT
10+
11+
final dynamicArray = <dynamic>[1, 2, 3];
12+
final copy = List<int>.from(dynamicArray);
13+
final dynamicCopy = List.from(dynamicArray);
14+
15+
final objectArray = <Object>[1, 2, 3];
16+
final copy = List<int>.from(objectArray);
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const queue = ListQueue.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = ListQueue<int>.from(queue); // LINT
7+
final numQueue = ListQueue<num>.from(queue); // LINT
8+
9+
final intQueue = ListQueue<int>.from(numQueue);
10+
11+
final unspecifedQueue = ListQueue.from(queue); // LINT
12+
13+
final dynamicQueue = ListQueue<dynamic>.from([1, 2, 3]);
14+
final copy = ListQueue<int>.from(dynamicQueue);
15+
final dynamicCopy = ListQueue.from(dynamicQueue);
16+
17+
final objectQueue = ListQueue<Object>.from([1, 2, 3]);
18+
final copy = ListQueue<int>.from(objectQueue);
19+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const queue = Queue.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = Queue<int>.from(queue); // LINT
7+
final numQueue = Queue<num>.from(queue); // LINT
8+
9+
final intQueue = Queue<int>.from(numQueue);
10+
11+
final unspecifedQueue = Queue.from(queue); // LINT
12+
13+
final dynamicQueue = Queue<dynamic>.from([1, 2, 3]);
14+
final copy = Queue<int>.from(dynamicQueue);
15+
final dynamicCopy = Queue.from(dynamicQueue);
16+
17+
final objectQueue = Queue<Object>.from([1, 2, 3]);
18+
final copy = Queue<int>.from(objectQueue);
19+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
void main() {
2+
const set = {1, 2, 3, 4, 5, 6, 7, 8, 9};
3+
4+
final copy = Set<int>.from(set); // LINT
5+
final numSet = Set<num>.from(set); // LINT
6+
7+
final intSet = Set<int>.from(numSet);
8+
9+
final unspecifedSet = Set.from(set); // LINT
10+
11+
final dynamicSet = <dynamic>{1, 2, 3};
12+
final copy = Set<int>.from(dynamicSet);
13+
final dynamicCopy = Set.from(dynamicSet);
14+
15+
final objectSet = <Object>{1, 2, 3};
16+
final copy = Set<int>.from(objectSet);
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import 'dart:collection';
2+
3+
void main() {
4+
const hashSet = SplayTreeSet.of([1, 2, 3, 4, 5, 6, 7, 8, 9]);
5+
6+
final copy = SplayTreeSet<int>.from(hashSet); // LINT
7+
final numSet = SplayTreeSet<num>.from(hashSet); // LINT
8+
9+
final intSet = SplayTreeSet<int>.from(numSet);
10+
11+
final unspecifedSet = SplayTreeSet.from(hashSet); // LINT
12+
13+
final dynamicSet = SplayTreeSet<dynamic>.from([1, 2, 3]);
14+
final copy = SplayTreeSet<int>.from(dynamicSet);
15+
final dynamicCopy = SplayTreeSet.from(dynamicSet);
16+
17+
final objectSet = SplayTreeSet<Object>.from([1, 2, 3]);
18+
final copy = SplayTreeSet<int>.from(objectSet);
19+
}

0 commit comments

Comments
 (0)