Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
4 changes: 4 additions & 0 deletions packages/leancode_lint/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 24.1.0

- Update [`prefer_equatable_mixin`](https://github.com/leancodepl/flutter_corelibrary/tree/master/packages/leancode_lint#prefer_equatable_mixin) to suggest mixing in `Equatable` directly when the linted package depends on `equatable` 2.1.0 or higher, where `EquatableMixin` is deprecated.

# 24.0.0

- Add new custom lints:
Expand Down
18 changes: 17 additions & 1 deletion packages/leancode_lint/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,12 @@ None

### `prefer_equatable_mixin`

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

Since `equatable` 2.1.0, `Equatable` can be used as a mixin and `EquatableMixin`
is deprecated. This lint suggests mixing in `Equatable` directly when the package
depends on `equatable` 2.1.0 or higher, and falls back to suggesting `EquatableMixin`
for older versions.

**BAD:**

Expand All @@ -848,6 +853,17 @@ class Foobar extends Equatable {
```dart
import 'package:equatable/equatable.dart';

// `equatable` >= 2.1.0
class Foobar with Equatable {
const Foobar(this.value);

final int value;

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

// `equatable` < 2.1.0
class Foobar with EquatableMixin {
const Foobar(this.value);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ List<String> _findMissingFieldNames(
///
/// Returns `false` when:
/// - the superclass has no Equatable-shaped ancestor (e.g. a plain class that
/// only mixes in `EquatableMixin` — its supertype is [Object]);
/// only mixes in `Equatable`/`EquatableMixin` — its supertype is [Object]);
/// - the superclass is `Equatable` or `EquatableMixin` itself (both declare
/// `props` as abstract, so `super.props` would target the abstract member);
/// - the superclass is an intermediate Equatable-shaped class that does not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ 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/dart/element/element.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';
Expand All @@ -17,7 +18,7 @@ class PreferEquatableMixin extends AnalysisRule {

static const code = LintCode(
'prefer_equatable_mixin',
'The class {0} should mix in EquatableMixin instead of extending Equatable.',
'The class {0} should mix in {1} instead of extending Equatable.',
correctionMessage: 'Replace with a mixin.',
severity: .WARNING,
);
Expand Down Expand Up @@ -69,20 +70,31 @@ class _Visitor extends SimpleAstVisitor<void> {
if (isEquatable && !isEquatableMixin) {
rule.reportAtNode(
extendsClause.superclass,
arguments: [node.namePart.typeName.lexeme],
arguments: [
node.namePart.typeName.lexeme,
_recommendedMixinName(extendsClause.superclass),
],
);
}
}
}

/// Recommends `Equatable` for `equatable` >= 2.1.0 (where it's a `mixin class`)
/// and the deprecated `EquatableMixin` for older versions.
String _recommendedMixinName(NamedType equatableType) =>
switch (equatableType.element) {
ClassElement(isMixinClass: true) => 'Equatable',
_ => 'EquatableMixin',
};

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

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

@override
Expand All @@ -93,6 +105,7 @@ class ConvertToEquatableMixin extends ResolvedCorrectionProducer {
final classDeclaration = node.thisOrAncestorOfType<ClassDeclaration>()!;
final extendsClause = classDeclaration.extendsClause!;
final withClause = classDeclaration.withClause;
final mixinName = _recommendedMixinName(extendsClause.superclass);

await builder.addDartFileEdit(file, (builder) {
if (withClause != null) {
Expand All @@ -103,12 +116,12 @@ class ConvertToEquatableMixin extends ResolvedCorrectionProducer {
)
..addSimpleInsertion(
withClause.mixinTypes.first.offset,
'EquatableMixin, ',
'$mixinName, ',
);
} else {
builder.addSimpleReplacement(
extendsClause.sourceRange,
'with EquatableMixin',
'with $mixinName',
);
}
});
Expand Down
2 changes: 1 addition & 1 deletion packages/leancode_lint/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: leancode_lint
version: 24.0.0
version: 24.1.0
homepage: https://github.com/leancodepl/flutter_corelibrary/tree/master/packages/leancode_lint
repository: https://github.com/leancodepl/flutter_corelibrary
description: Robust and high-quality lint rules used at LeanCode.
Expand Down
9 changes: 7 additions & 2 deletions packages/leancode_lint/test/assert_ranges.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ extension AssertDiagnosticsInRangesX on AnalysisRuleTest {
bool positionShorthand = true,
bool rangeShorthand = true,
bool zeroWidthMarker = true,
List<List<Pattern>> messageContainsAll = const [],
}) {
final code = TestCode.parse(
content,
Expand All @@ -17,8 +18,12 @@ extension AssertDiagnosticsInRangesX on AnalysisRuleTest {
zeroWidthMarker: zeroWidthMarker,
);
return assertDiagnostics(code.code, [
for (final range in code.ranges)
lint(range.sourceRange.offset, range.sourceRange.length),
for (final (index, range) in code.ranges.indexed)
lint(
range.sourceRange.offset,
range.sourceRange.length,
messageContainsAll: messageContainsAll.elementAtOrNull(index) ?? [],
),
for (final position in code.positions) lint(position.offset, 0),
]);
}
Expand Down
22 changes: 22 additions & 0 deletions packages/leancode_lint/test/mock_libraries/equatable.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
part of '../mock_libraries.dart';

/// Mocks `equatable` 2.1.0 or higher, where `Equatable` can be used as a mixin
/// and `EquatableMixin` is deprecated.
mixin MockEquatable on AnalysisRuleTest {
@override
void setUp() {
newPackage('equatable').addFile('lib/equatable.dart', '''
abstract mixin class Equatable {
const Equatable();
List<Object?> get props;
}

@Deprecated('use Equatable as a mixin instead')
mixin EquatableMixin {
List<Object?> get props;
}
''');
super.setUp();
}
}

/// Mocks `equatable` older than 2.1.0, where `Equatable` cannot be used as a
/// mixin and `EquatableMixin` should be used instead.
mixin MockOldEquatable on AnalysisRuleTest {
@override
void setUp() {
newPackage('equatable').addFile('lib/equatable.dart', '''
class Equatable {
const Equatable();
List<Object?> get props;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ class MissingEquatablePropsTest extends AnalysisRuleTest with MockEquatable {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a;
Expand All @@ -40,6 +40,24 @@ class MyState with EquatableMixin {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class MyState with Equatable {
MyState(this.a, this.b, this.c);

final int a;
final String b;
final double c;

@override
List<Object?> get props => /*[0*/[a]/*0]*/;
}
''');
}

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

// ignore: deprecated_member_use
class MyState with EquatableMixin {
MyState(this.a, this.b, this.c);

Expand Down Expand Up @@ -73,7 +91,7 @@ class MyState extends Equatable {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a);

static const int unused = 0;
Expand All @@ -89,7 +107,7 @@ class MyState with EquatableMixin {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a);

final int a;
Expand All @@ -105,7 +123,7 @@ class MyState with EquatableMixin {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a;
Expand Down Expand Up @@ -135,7 +153,7 @@ class Plain {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a, b;
Expand All @@ -150,7 +168,7 @@ class MyState with EquatableMixin {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.onTap);

final int a;
Expand All @@ -166,7 +184,7 @@ class MyState with EquatableMixin {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a;
Expand All @@ -182,7 +200,7 @@ class MyState with EquatableMixin {
await assertDiagnosticsInRanges('''
import 'package:equatable/equatable.dart';

class Parent with EquatableMixin {
class Parent with Equatable {
Parent(this.a);

final int a;
Expand All @@ -206,7 +224,7 @@ class Sub extends Parent {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class Parent with EquatableMixin {
class Parent with Equatable {
Parent(this.a);

final int a;
Expand All @@ -230,7 +248,7 @@ class Sub extends Parent {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class Parent with EquatableMixin {
class Parent with Equatable {
Parent(this.a);

final int a;
Expand All @@ -250,7 +268,8 @@ class Sub extends Parent {
''');
}

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

Expand All @@ -266,7 +285,8 @@ class MyState extends Equatable {
''');
}

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

Expand All @@ -286,7 +306,8 @@ class Sub extends AbstractBase {
''');
}

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

Expand All @@ -306,7 +327,8 @@ class Sub extends AbstractBase {
''');
}

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

Expand Down Expand Up @@ -334,7 +356,7 @@ class Sub extends AbstractBase {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a;
Expand All @@ -352,7 +374,7 @@ class MyState with EquatableMixin {
await assertNoDiagnostics('''
import 'package:equatable/equatable.dart';

class MyState with EquatableMixin {
class MyState with Equatable {
MyState(this.a, this.b);

final int a;
Expand Down
Loading
Loading