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

Commit 8eae6f6

Browse files
authored
feat: add list-all-equatable-fields rule (#1103)
* feat: add list-all-equatable-fields rule * fix: support missed cases * chore: revert disabled plugins section * test: update text example * chore: fix formatting * test: fix test-case * feat: add support for mixins and inheritance * docs: add rule docs
1 parent 07d139f commit 8eae6f6

File tree

8 files changed

+492
-0
lines changed

8 files changed

+492
-0
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
* feat: add static code diagnostic [`list-all-equatable-fields`](https://dartcodemetrics.dev/docs/rules/common/list-all-equatable-fields).
56
* feat: add `strict` config option to [`avoid-collection-methods-with-unrelated-types`](https://dartcodemetrics.dev/docs/rules/common/avoid-collection-methods-with-unrelated-types).
67
* fix: support function expression invocations for [`prefer-moving-to-variable`](https://dartcodemetrics.dev/docs/rules/common/prefer-moving-to-variable).
78
* feat: support ignoring regular comments for [`format-comment`](https://dartcodemetrics.dev/docs/rules/common/format-comment).

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

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import 'rules_list/component_annotation_arguments_ordering/component_annotation_
3737
import 'rules_list/consistent_update_render_object/consistent_update_render_object_rule.dart';
3838
import 'rules_list/double_literal_format/double_literal_format_rule.dart';
3939
import 'rules_list/format_comment/format_comment_rule.dart';
40+
import 'rules_list/list_all_equatable_fields/list_all_equatable_fields_rule.dart';
4041
import 'rules_list/member_ordering/member_ordering_rule.dart';
4142
import 'rules_list/missing_test_assertion/missing_test_assertion_rule.dart';
4243
import 'rules_list/newline_before_return/newline_before_return_rule.dart';
@@ -117,6 +118,7 @@ final _implementedRules = <String, Rule Function(Map<String, Object>)>{
117118
ConsistentUpdateRenderObjectRule.ruleId: ConsistentUpdateRenderObjectRule.new,
118119
DoubleLiteralFormatRule.ruleId: DoubleLiteralFormatRule.new,
119120
FormatCommentRule.ruleId: FormatCommentRule.new,
121+
ListAllEquatableFieldsRule.ruleId: ListAllEquatableFieldsRule.new,
120122
MemberOrderingRule.ruleId: MemberOrderingRule.new,
121123
MissingTestAssertionRule.ruleId: MissingTestAssertionRule.new,
122124
NewlineBeforeReturnRule.ruleId: NewlineBeforeReturnRule.new,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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/element.dart';
6+
import 'package:analyzer/dart/element/type.dart';
7+
import 'package:collection/collection.dart';
8+
9+
import '../../../../../utils/node_utils.dart';
10+
import '../../../lint_utils.dart';
11+
import '../../../models/internal_resolved_unit_result.dart';
12+
import '../../../models/issue.dart';
13+
import '../../../models/severity.dart';
14+
import '../../models/flutter_rule.dart';
15+
import '../../rule_utils.dart';
16+
17+
part 'visitor.dart';
18+
19+
class ListAllEquatableFieldsRule extends FlutterRule {
20+
static const ruleId = 'list-all-equatable-fields';
21+
22+
ListAllEquatableFieldsRule([
23+
Map<String, Object> config = const {},
24+
]) : super(
25+
id: ruleId,
26+
severity: readSeverity(config, Severity.warning),
27+
excludes: readExcludes(config),
28+
includes: readIncludes(config),
29+
);
30+
31+
@override
32+
Iterable<Issue> check(InternalResolvedUnitResult source) {
33+
final visitor = _Visitor();
34+
35+
source.unit.visitChildren(visitor);
36+
37+
return visitor.declarations
38+
.map((declaration) => createIssue(
39+
rule: this,
40+
location: nodeLocation(node: declaration.node, source: source),
41+
message: declaration.errorMessage,
42+
))
43+
.toList(growable: false);
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
part of 'list_all_equatable_fields_rule.dart';
2+
3+
class _Visitor extends GeneralizingAstVisitor<void> {
4+
final _declarations = <_DeclarationInfo>[];
5+
6+
Iterable<_DeclarationInfo> get declarations => _declarations;
7+
8+
@override
9+
void visitClassDeclaration(ClassDeclaration node) {
10+
final classType = node.extendsClause?.superclass.type;
11+
12+
final isEquatable = _isEquatableOrSubclass(classType);
13+
final isMixin = node.withClause?.mixinTypes
14+
.any((mixinType) => _isEquatableMixin(mixinType.type)) ??
15+
false;
16+
final isSubclassOfMixin = _isSubclassOfEquatableMixin(classType);
17+
if (!isEquatable && !isMixin && !isSubclassOfMixin) {
18+
return;
19+
}
20+
21+
final fieldNames = node.members
22+
.whereType<FieldDeclaration>()
23+
.whereNot((field) => field.isStatic)
24+
.map((declaration) =>
25+
declaration.fields.variables.firstOrNull?.name.lexeme)
26+
.whereNotNull()
27+
.toSet();
28+
29+
if (isMixin) {
30+
fieldNames.addAll(_getParentFields(classType));
31+
}
32+
33+
final props = node.members.firstWhereOrNull((declaration) =>
34+
declaration is MethodDeclaration &&
35+
declaration.isGetter &&
36+
declaration.name.lexeme == 'props') as MethodDeclaration?;
37+
38+
if (props == null) {
39+
return;
40+
}
41+
42+
final literalVisitor = _ListLiteralVisitor();
43+
props.body.visitChildren(literalVisitor);
44+
45+
final expression = literalVisitor.literals.firstOrNull;
46+
if (expression != null) {
47+
final usedFields = expression.elements
48+
.whereType<SimpleIdentifier>()
49+
.map((identifier) => identifier.name)
50+
.toSet();
51+
52+
if (!usedFields.containsAll(fieldNames)) {
53+
final missingFields = fieldNames.difference(usedFields).join(', ');
54+
_declarations.add(_DeclarationInfo(
55+
props,
56+
'Missing declared class fields: $missingFields',
57+
));
58+
}
59+
}
60+
}
61+
62+
Set<String> _getParentFields(DartType? classType) {
63+
// ignore: deprecated_member_use
64+
final element = classType?.element2;
65+
if (element is! ClassElement) {
66+
return {};
67+
}
68+
69+
return element.fields
70+
.where(
71+
(field) =>
72+
!field.isStatic &&
73+
!field.isConst &&
74+
!field.isPrivate &&
75+
field.name != 'hashCode',
76+
)
77+
.map((field) => field.name)
78+
.toSet();
79+
}
80+
81+
bool _isEquatableOrSubclass(DartType? type) =>
82+
_isEquatable(type) || _isSubclassOfEquatable(type);
83+
84+
bool _isSubclassOfEquatable(DartType? type) =>
85+
type is InterfaceType && type.allSupertypes.any(_isEquatable);
86+
87+
bool _isEquatable(DartType? type) =>
88+
type?.getDisplayString(withNullability: false) == 'Equatable';
89+
90+
bool _isEquatableMixin(DartType? type) =>
91+
// ignore: deprecated_member_use
92+
type?.element2 is MixinElement &&
93+
type?.getDisplayString(withNullability: false) == 'EquatableMixin';
94+
95+
bool _isSubclassOfEquatableMixin(DartType? type) {
96+
// ignore: deprecated_member_use
97+
final element = type?.element2;
98+
99+
return element is ClassElement && element.mixins.any(_isEquatableMixin);
100+
}
101+
}
102+
103+
class _ListLiteralVisitor extends RecursiveAstVisitor<void> {
104+
final literals = <ListLiteral>{};
105+
106+
@override
107+
void visitListLiteral(ListLiteral node) {
108+
super.visitListLiteral(node);
109+
110+
literals.add(node);
111+
}
112+
}
113+
114+
class _DeclarationInfo {
115+
final Declaration node;
116+
final String errorMessage;
117+
118+
const _DeclarationInfo(this.node, this.errorMessage);
119+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
class SomePerson {
2+
const SomePerson(this.name);
3+
4+
final String name;
5+
6+
@override
7+
bool operator ==(Object other) =>
8+
identical(this, other) ||
9+
other is SomePerson &&
10+
runtimeType == other.runtimeType &&
11+
name == other.name;
12+
13+
@override
14+
int get hashCode => name.hashCode;
15+
}
16+
17+
class Person extends Equatable {
18+
const Person(this.name);
19+
20+
final String name;
21+
22+
@override
23+
List<Object> get props => [name];
24+
}
25+
26+
class AnotherPerson extends Equatable {
27+
const AnotherPerson(this.name, this.age);
28+
29+
final String name;
30+
31+
final int age;
32+
33+
@override
34+
List<Object> get props => [name]; // LINT
35+
}
36+
37+
class AndAnotherPerson extends Equatable {
38+
const AndAnotherPerson(this.name, this.age, this.address);
39+
40+
final String name;
41+
42+
final int age;
43+
44+
final String address;
45+
46+
@override
47+
List<Object> get props {
48+
return [name]; // LINT
49+
}
50+
}
51+
52+
class AndAnotherPerson extends Equatable {
53+
static final someProp = 'hello';
54+
55+
const AndAnotherPerson(this.name);
56+
57+
final String name;
58+
59+
@override
60+
List<Object> get props => [name];
61+
}
62+
63+
class SubPerson extends AndAnotherPerson {
64+
const SubPerson(this.value, String name) : super();
65+
66+
final int value;
67+
68+
@override
69+
List<Object> get props {
70+
return super.props..addAll([]); // LINT
71+
}
72+
}
73+
74+
class EquatableDateTimeSubclass extends EquatableDateTime {
75+
final int century;
76+
77+
EquatableDateTimeSubclass(
78+
this.century,
79+
int year, [
80+
int month = 1,
81+
int day = 1,
82+
int hour = 0,
83+
int minute = 0,
84+
int second = 0,
85+
int millisecond = 0,
86+
int microsecond = 0,
87+
]) : super(year, month, day, hour, minute, second, millisecond, microsecond);
88+
89+
@override
90+
List<Object> get props => super.props..addAll([]); // LINT
91+
}
92+
93+
class EquatableDateTime extends DateTime with EquatableMixin {
94+
EquatableDateTime(
95+
int year, [
96+
int month = 1,
97+
int day = 1,
98+
int hour = 0,
99+
int minute = 0,
100+
int second = 0,
101+
int millisecond = 0,
102+
int microsecond = 0,
103+
]) : super(year, month, day, hour, minute, second, millisecond, microsecond);
104+
105+
@override
106+
List<Object> get props {
107+
return [
108+
year,
109+
month,
110+
day,
111+
hour,
112+
minute,
113+
second,
114+
millisecond,
115+
microsecond,
116+
// LINT
117+
];
118+
}
119+
}
120+
121+
class Equatable {
122+
List<Object> get props;
123+
}
124+
125+
mixin EquatableMixin {
126+
List<Object?> get props;
127+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/models/severity.dart';
2+
import 'package:dart_code_metrics/src/analyzers/lint_analyzer/rules/rules_list/list_all_equatable_fields/list_all_equatable_fields_rule.dart';
3+
import 'package:test/test.dart';
4+
5+
import '../../../../../helpers/rule_test_helper.dart';
6+
7+
const _examplePath = 'list_all_equatable_fields/examples/example.dart';
8+
9+
void main() {
10+
group('ListAllEquatableFieldsRule', () {
11+
test('initialization', () async {
12+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
13+
final issues = ListAllEquatableFieldsRule().check(unit);
14+
15+
RuleTestHelper.verifyInitialization(
16+
issues: issues,
17+
ruleId: 'list-all-equatable-fields',
18+
severity: Severity.warning,
19+
);
20+
});
21+
22+
test('reports about found issues', () async {
23+
final unit = await RuleTestHelper.resolveFromFile(_examplePath);
24+
final issues = ListAllEquatableFieldsRule().check(unit);
25+
26+
RuleTestHelper.verifyIssues(
27+
issues: issues,
28+
startLines: [34, 47, 69, 90, 106],
29+
startColumns: [3, 3, 3, 3, 3],
30+
locationTexts: [
31+
'List<Object> get props => [name];',
32+
'List<Object> get props {\n'
33+
' return [name]; // LINT\n'
34+
' }',
35+
'List<Object> get props {\n'
36+
' return super.props..addAll([]); // LINT\n'
37+
' }',
38+
'List<Object> get props => super.props..addAll([]);',
39+
'List<Object> get props {\n'
40+
' return [\n'
41+
' year,\n'
42+
' month,\n'
43+
' day,\n'
44+
' hour,\n'
45+
' minute,\n'
46+
' second,\n'
47+
' millisecond,\n'
48+
' microsecond,\n'
49+
' // LINT\n'
50+
' ];\n'
51+
' }',
52+
],
53+
messages: [
54+
'Missing declared class fields: age',
55+
'Missing declared class fields: age, address',
56+
'Missing declared class fields: value',
57+
'Missing declared class fields: century',
58+
'Missing declared class fields: isUtc, millisecondsSinceEpoch, microsecondsSinceEpoch, timeZoneName, timeZoneOffset, weekday',
59+
],
60+
);
61+
});
62+
});
63+
}

0 commit comments

Comments
 (0)