Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions packages/leancode_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -575,15 +575,53 @@ class MyWidget extends StatelessWidget {

None

### `prefer_equatable_mixin`
Copy link
Copy Markdown
Member

@Albert221 Albert221 Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explaining all lints in README takes much space. Maybe we could wrap them into <details>? Out of scope though

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do next 💯


**DO** mix in `EquatableMixin` instead of extending `Equatable`.

**BAD:**

```dart
import 'package:equatable/equatable.dart';

class Foobar extends Equatable {
const Foobar(this.value);

final int value;

@override
List<Object?> get props => [value];
}
```

**GOOD:**

```dart
import 'package:equatable/equatable.dart';

class Foobar with EquatableMixin {
const Foobar(this.value);

final int value;

@override
List<Object?> get props => [value];
}
```

#### Configuration

None.

## Assists

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>.

See linked source code containing explanation in dart doc.

- [Convert positional to named formal](./lib/assists/convert_positional_to_named_formal.dart)
- [Convert record into nominal type](./lib/assists/convert_record_into_nominal_type.dart)
- [Convert iterable map to collection-for](./lib/assists/convert_iterable_map_to_collection_for.dart)
- [Convert positional to named formal](./lib/src/assists/convert_positional_to_named_formal.dart)
- [Convert record into nominal type](./lib/src/assists/convert_record_into_nominal_type.dart)
- [Convert iterable map to collection-for](./lib/src/assists/convert_iterable_map_to_collection_for.dart)

[pub-badge]: https://img.shields.io/pub/v/leancode_lint
[pub-badge-link]: https://pub.dev/packages/leancode_lint
8 changes: 7 additions & 1 deletion packages/leancode_lint/lib/plugin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:leancode_lint/src/lints/avoid_single_child_in_multi_child_widget
import 'package:leancode_lint/src/lints/catch_parameter_names.dart';
import 'package:leancode_lint/src/lints/constructor_parameters_and_fields_should_have_the_same_order.dart';
import 'package:leancode_lint/src/lints/hook_widget_does_not_use_hooks.dart';
import 'package:leancode_lint/src/lints/prefer_equatable_mixin.dart';
import 'package:leancode_lint/src/lints/prefix_widgets_returning_slivers.dart';
import 'package:leancode_lint/src/lints/start_comments_with_space.dart';
import 'package:leancode_lint/src/lints/use_align.dart';
Expand Down Expand Up @@ -64,8 +65,13 @@ final class LeanCodeLintPlugin extends Plugin {
UseDedicatedMediaQueryMethods.code,
ReplaceMediaQueryOfWithDedicatedMethodFix.new,
)
..registerWarningRule(PreferEquatableMixin())
..registerFixForRule(
PreferEquatableMixin.code,
ConvertToEquatableMixin.new,
)
// TODO: uncomment when `prefer_center_over_align` is migrated
// ..registerAssist(PreferCenterOverAlign())
// ..registerWarningRule(PreferCenterOverAlign())
..registerAssist(ConvertRecordIntoNominalType.new)
..registerAssist(ConvertPositionalToNamedFormal.new)
..registerAssist(ConvertIterableMapToCollectionFor.new);
Expand Down
116 changes: 116 additions & 0 deletions packages/leancode_lint/lib/src/lints/prefer_equatable_mixin.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import 'package:analysis_server_plugin/edit/dart/correction_producer.dart';
import 'package:analysis_server_plugin/edit/dart/dart_fix_kind_priority.dart';
import 'package:analyzer/analysis_rule/analysis_rule.dart';
import 'package:analyzer/analysis_rule/rule_context.dart';
import 'package:analyzer/analysis_rule/rule_visitor_registry.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/error/error.dart';
import 'package:analyzer_plugin/utilities/change_builder/change_builder_core.dart';
import 'package:analyzer_plugin/utilities/fixes/fixes.dart';
import 'package:analyzer_plugin/utilities/range_factory.dart';
import 'package:leancode_lint/src/type_checker.dart';

class PreferEquatableMixin extends AnalysisRule {
PreferEquatableMixin()
: super(name: code.lowerCaseName, description: code.problemMessage);

static const code = LintCode(
'prefer_equatable_mixin',
'The class {0} should mix in EquatableMixin instead of extending Equatable.',
correctionMessage: 'Replace with a mixin application.',
Comment thread
PiotrRogulski marked this conversation as resolved.
Outdated
severity: .WARNING,
Comment thread
Albert221 marked this conversation as resolved.
);

@override
LintCode get diagnosticCode => code;

@override
void registerNodeProcessors(
RuleVisitorRegistry registry,
RuleContext context,
) {
registry.addClassDeclaration(this, _Visitor(this, context));
}
}

class _Visitor extends SimpleAstVisitor<void> {
_Visitor(this.rule, this.context);

final AnalysisRule rule;
final RuleContext context;

static const equatable = TypeChecker.fromName(
'Equatable',
packageName: 'equatable',
);
static const equatableMixin = TypeChecker.fromName(
'EquatableMixin',
packageName: 'equatable',
);

@override
void visitClassDeclaration(ClassDeclaration node) {
final extendsClause = node.extendsClause;
if (extendsClause == null) {
return;
}

final superType = extendsClause.superclass.type;
final isEquatable = superType != null && equatable.isExactlyType(superType);

final isEquatableMixin =
node.withClause?.mixinTypes
.map((mixin) => mixin.type)
.nonNulls
.any(equatableMixin.isExactlyType) ??
false;

if (isEquatable && !isEquatableMixin) {
rule.reportAtNode(
extendsClause.superclass,
arguments: [node.namePart.typeName.lexeme],
);
}
}
}

class ConvertToEquatableMixin extends ResolvedCorrectionProducer {
ConvertToEquatableMixin({required super.context});

@override
FixKind? get fixKind => const FixKind(
'leancode_lint.fix.convertToEquatableMixin',
DartFixKindPriority.standard,
'Convert to EquatableMixin',
);

@override
CorrectionApplicability get applicability => .automatically;

@override
Future<void> compute(ChangeBuilder builder) async {
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>()!;
final extendsClause = classDeclaration.extendsClause!;
final withClause = classDeclaration.withClause;

await builder.addDartFileEdit(file, (builder) {
if (withClause != null) {
builder
..addSimpleReplacement(
range.startStart(extendsClause, withClause),
'',
)
..addSimpleInsertion(
withClause.mixinTypes.first.offset,
'EquatableMixin, ',
);
} else {
builder.addSimpleReplacement(
extendsClause.sourceRange,
'with EquatableMixin',
);
}
});
}
}
1 change: 1 addition & 0 deletions packages/leancode_lint/test/mock_libraries.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';

part 'mock_libraries/bloc.dart';
part 'mock_libraries/equatable.dart';
part 'mock_libraries/flutter.dart';
part 'mock_libraries/flutter_bloc.dart';
part 'mock_libraries/flutter_hooks.dart';
Expand Down
18 changes: 18 additions & 0 deletions packages/leancode_lint/test/mock_libraries/equatable.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
part of '../mock_libraries.dart';

mixin MockEquatable on AnalysisRuleTest {
@override
void setUp() {
newPackage('equatable').addFile('lib/equatable.dart', '''
class Equatable {
const Equatable();
List<Object?> get props;
}

mixin EquatableMixin {
List<Object?> get props;
}
''');
super.setUp();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:analyzer_testing/analysis_rule/analysis_rule.dart';
import 'package:leancode_lint/src/lints/prefer_equatable_mixin.dart';
import 'package:test_reflective_loader/test_reflective_loader.dart';

import '../assert_ranges.dart';
import '../mock_libraries.dart';

void main() {
defineReflectiveSuite(() {
defineReflectiveTests(PreferEquatableMixinTest);
});
}

@reflectiveTest
class PreferEquatableMixinTest extends AnalysisRuleTest with MockEquatable {
@override
void setUp() {
rule = PreferEquatableMixin();

super.setUp();
}

Future<void> test_only_directly_extending_equatable() async {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class MyState extends [!Equatable!] {
@override
List<Object?> get props => [];
}

class MyState2 extends MyState {
@override
List<Object?> get props => [];
}
''');
}

Future<void> test_mixin_not_flagged() async {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState3 with EquatableMixin {
@override
List<Object?> get props => [];
}
''');
}
Comment thread
PiotrRogulski marked this conversation as resolved.
}