Skip to content

Commit e749b3b

Browse files
Add the prefer_equatable_mixin lint (#503)
* Add the `prefer_equatable_mixin` lint * More tests * Update correction message
1 parent cb451c7 commit e749b3b

6 files changed

Lines changed: 253 additions & 2 deletions

File tree

packages/leancode_lint/README.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,44 @@ class MyWidget extends StatelessWidget {
585585

586586
None
587587

588+
### `prefer_equatable_mixin`
589+
590+
**DO** mix in `EquatableMixin` instead of extending `Equatable`.
591+
592+
**BAD:**
593+
594+
```dart
595+
import 'package:equatable/equatable.dart';
596+
597+
class Foobar extends Equatable {
598+
const Foobar(this.value);
599+
600+
final int value;
601+
602+
@override
603+
List<Object?> get props => [value];
604+
}
605+
```
606+
607+
**GOOD:**
608+
609+
```dart
610+
import 'package:equatable/equatable.dart';
611+
612+
class Foobar with EquatableMixin {
613+
const Foobar(this.value);
614+
615+
final int value;
616+
617+
@override
618+
List<Object?> get props => [value];
619+
}
620+
```
621+
622+
#### Configuration
623+
624+
None.
625+
588626
## Assists
589627

590628
Assists are IDE refactorings not related to a particular issue. They can be triggered by placing your cursor over a relevant piece of code and opening the code actions dialog. For instance, in VSCode this is done with <kbd>ctrl</kbd>+<kbd>.</kbd> or <kbd>⌘</kbd>+<kbd>.</kbd>.
@@ -631,4 +669,4 @@ We are **top-tier experts** focused on Flutter Enterprise solutions.
631669
[leancode-landing]: https://leancode.co/?utm_source=github.com&utm_medium=referral&utm_campaign=leancode-lint
632670
[leancode-estimate]: https://leancode.co/get-estimate?utm_source=github.com&utm_medium=referral&utm_campaign=leancode-lint
633671
[leancode-packages]: https://pub.dev/packages?q=publisher%3Aleancode.co&sort=downloads
634-
[patrol-landing]: https://patrol.leancode.co/?utm_source=github.com&utm_medium=referral&utm_campaign=leancode-lint
672+
[patrol-landing]: https://patrol.leancode.co/?utm_source=github.com&utm_medium=referral&utm_campaign=leancode-lint

packages/leancode_lint/lib/plugin.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'package:leancode_lint/src/lints/avoid_single_child_in_multi_child_widget
1010
import 'package:leancode_lint/src/lints/catch_parameter_names.dart';
1111
import 'package:leancode_lint/src/lints/constructor_parameters_and_fields_should_have_the_same_order.dart';
1212
import 'package:leancode_lint/src/lints/hook_widget_does_not_use_hooks.dart';
13+
import 'package:leancode_lint/src/lints/prefer_equatable_mixin.dart';
1314
import 'package:leancode_lint/src/lints/prefix_widgets_returning_slivers.dart';
1415
import 'package:leancode_lint/src/lints/start_comments_with_space.dart';
1516
import 'package:leancode_lint/src/lints/use_align.dart';
@@ -64,8 +65,13 @@ final class LeanCodeLintPlugin extends Plugin {
6465
UseDedicatedMediaQueryMethods.code,
6566
ReplaceMediaQueryOfWithDedicatedMethodFix.new,
6667
)
68+
..registerWarningRule(PreferEquatableMixin())
69+
..registerFixForRule(
70+
PreferEquatableMixin.code,
71+
ConvertToEquatableMixin.new,
72+
)
6773
// TODO: uncomment when `prefer_center_over_align` is migrated
68-
// ..registerAssist(PreferCenterOverAlign())
74+
// ..registerWarningRule(PreferCenterOverAlign())
6975
..registerAssist(ConvertRecordIntoNominalType.new)
7076
..registerAssist(ConvertPositionalToNamedFormal.new)
7177
..registerAssist(ConvertIterableMapToCollectionFor.new);
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
2+
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
3+
import 'package:analyzer/analysis_rule/analysis_rule.dart';
4+
import 'package:analyzer/analysis_rule/rule_context.dart';
5+
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
6+
import 'package:analyzer/dart/ast/ast.dart';
7+
import 'package:analyzer/dart/ast/visitor.dart';
8+
import 'package:analyzer/error/error.dart';
9+
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
10+
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
11+
import 'package:analyzer_plugin/utilities/range_factory.dart';
12+
import 'package:leancode_lint/src/type_checker.dart';
13+
14+
class PreferEquatableMixin extends AnalysisRule {
15+
PreferEquatableMixin()
16+
: super(name: code.lowerCaseName, description: code.problemMessage);
17+
18+
static const code = LintCode(
19+
'prefer_equatable_mixin',
20+
'The class {0} should mix in EquatableMixin instead of extending Equatable.',
21+
correctionMessage: 'Replace with a mixin.',
22+
severity: .WARNING,
23+
);
24+
25+
@override
26+
LintCode get diagnosticCode => code;
27+
28+
@override
29+
void registerNodeProcessors(
30+
RuleVisitorRegistry registry,
31+
RuleContext context,
32+
) {
33+
registry.addClassDeclaration(this, _Visitor(this, context));
34+
}
35+
}
36+
37+
class _Visitor extends SimpleAstVisitor<void> {
38+
_Visitor(this.rule, this.context);
39+
40+
final AnalysisRule rule;
41+
final RuleContext context;
42+
43+
static const equatable = TypeChecker.fromName(
44+
'Equatable',
45+
packageName: 'equatable',
46+
);
47+
static const equatableMixin = TypeChecker.fromName(
48+
'EquatableMixin',
49+
packageName: 'equatable',
50+
);
51+
52+
@override
53+
void visitClassDeclaration(ClassDeclaration node) {
54+
final extendsClause = node.extendsClause;
55+
if (extendsClause == null) {
56+
return;
57+
}
58+
59+
final superType = extendsClause.superclass.type;
60+
final isEquatable = superType != null && equatable.isExactlyType(superType);
61+
62+
final isEquatableMixin =
63+
node.withClause?.mixinTypes
64+
.map((mixin) => mixin.type)
65+
.nonNulls
66+
.any(equatableMixin.isExactlyType) ??
67+
false;
68+
69+
if (isEquatable && !isEquatableMixin) {
70+
rule.reportAtNode(
71+
extendsClause.superclass,
72+
arguments: [node.namePart.typeName.lexeme],
73+
);
74+
}
75+
}
76+
}
77+
78+
class ConvertToEquatableMixin extends ResolvedCorrectionProducer {
79+
ConvertToEquatableMixin({required super.context});
80+
81+
@override
82+
FixKind? get fixKind => const FixKind(
83+
'leancode_lint.fix.convertToEquatableMixin',
84+
DartFixKindPriority.standard,
85+
'Convert to EquatableMixin',
86+
);
87+
88+
@override
89+
CorrectionApplicability get applicability => .automatically;
90+
91+
@override
92+
Future<void> compute(ChangeBuilder builder) async {
93+
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>()!;
94+
final extendsClause = classDeclaration.extendsClause!;
95+
final withClause = classDeclaration.withClause;
96+
97+
await builder.addDartFileEdit(file, (builder) {
98+
if (withClause != null) {
99+
builder
100+
..addSimpleReplacement(
101+
range.startStart(extendsClause, withClause),
102+
'',
103+
)
104+
..addSimpleInsertion(
105+
withClause.mixinTypes.first.offset,
106+
'EquatableMixin, ',
107+
);
108+
} else {
109+
builder.addSimpleReplacement(
110+
extendsClause.sourceRange,
111+
'with EquatableMixin',
112+
);
113+
}
114+
});
115+
}
116+
}

packages/leancode_lint/test/mock_libraries.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
22

33
part 'mock_libraries/bloc.dart';
4+
part 'mock_libraries/equatable.dart';
45
part 'mock_libraries/flutter.dart';
56
part 'mock_libraries/flutter_bloc.dart';
67
part 'mock_libraries/flutter_hooks.dart';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
part of '../mock_libraries.dart';
2+
3+
mixin MockEquatable on AnalysisRuleTest {
4+
@override
5+
void setUp() {
6+
newPackage('equatable').addFile('lib/equatable.dart', '''
7+
class Equatable {
8+
const Equatable();
9+
List<Object?> get props;
10+
}
11+
12+
mixin EquatableMixin {
13+
List<Object?> get props;
14+
}
15+
''');
16+
super.setUp();
17+
}
18+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
2+
import 'package:leancode_lint/src/lints/prefer_equatable_mixin.dart';
3+
import 'package:test_reflective_loader/test_reflective_loader.dart';
4+
5+
import '../assert_ranges.dart';
6+
import '../mock_libraries.dart';
7+
8+
void main() {
9+
defineReflectiveSuite(() {
10+
defineReflectiveTests(PreferEquatableMixinTest);
11+
});
12+
}
13+
14+
@reflectiveTest
15+
class PreferEquatableMixinTest extends AnalysisRuleTest with MockEquatable {
16+
@override
17+
void setUp() {
18+
rule = PreferEquatableMixin();
19+
20+
super.setUp();
21+
}
22+
23+
Future<void> test_only_directly_extending_equatable() async {
24+
await assertDiagnosticsInRanges('''
25+
import 'package:equatable/equatable.dart';
26+
27+
class MyState extends [!Equatable!] {
28+
@override
29+
List<Object?> get props => [];
30+
}
31+
32+
class MyState2 extends MyState {
33+
@override
34+
List<Object?> get props => [];
35+
}
36+
''');
37+
}
38+
39+
Future<void> test_mixin_not_flagged() async {
40+
await assertNoDiagnostics('''
41+
import 'package:equatable/equatable.dart';
42+
43+
class MyState3 with EquatableMixin {
44+
@override
45+
List<Object?> get props => [];
46+
}
47+
''');
48+
}
49+
50+
Future<void> test_with_other_mixins() async {
51+
await assertDiagnosticsInRanges('''
52+
import 'package:equatable/equatable.dart';
53+
54+
mixin SomethingElse {}
55+
56+
class MyState extends [!Equatable!] with SomethingElse {
57+
@override
58+
List<Object?> get props => [];
59+
}
60+
61+
class MyState2 extends MyState with SomethingElse {
62+
@override
63+
List<Object?> get props => [];
64+
}
65+
66+
class MyState3 with SomethingElse, EquatableMixin {
67+
@override
68+
List<Object?> get props => [];
69+
}
70+
''');
71+
}
72+
}

0 commit comments

Comments
 (0)