From 03e7e0d584c7f09252576ec1c0272d256190ba98 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 12:16:09 +0200 Subject: [PATCH 01/20] Add the `bloc_related_class_naming` lint --- packages/leancode_lint/lib/helpers.dart | 84 +++++++++++++++++++ packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lints/bloc_related_classes_naming.dart | 42 ++++++++++ 3 files changed, 128 insertions(+) create mode 100644 packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart diff --git a/packages/leancode_lint/lib/helpers.dart b/packages/leancode_lint/lib/helpers.dart index 93df347c..fad1152a 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -1,6 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; +import 'package:analyzer/dart/element/element.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; import 'package:leancode_lint/utils.dart'; @@ -216,4 +217,87 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { listener(buildMethod.body, diagnosticNode); }); } + + void addBloc(void Function(ClassDeclaration node, _BlocData data) listener) { + addClassDeclaration((node) { + if (_maybeBlocData(node) case final data?) { + listener(node, data); + } + }); + } +} + +const _blocBase = TypeChecker.fromName('BlocBase', packageName: 'bloc'); +const _bloc = TypeChecker.fromName('Bloc', packageName: 'bloc'); +const _blocPresentation = TypeChecker.fromName( + 'BlocPresentationMixin', + packageName: 'bloc_presentation', +); + +typedef _BlocData = + ({ + String baseName, + InterfaceElement blocElement, + InterfaceElement stateElement, + InterfaceElement? eventElement, + InterfaceElement? presentationEventElement, + }); + +_BlocData? _maybeBlocData(ClassDeclaration clazz) { + final blocElement = clazz.declaredElement; + + if (blocElement == null || !_blocBase.isAssignableFrom(blocElement)) { + return null; + } + + final baseName = clazz.name.lexeme.replaceAll(RegExp(r'(Cubit|Bloc)$'), ''); + + final stateType = + blocElement.allSupertypes + .firstWhere((t) => _blocBase.isExactly(t.element)) + .typeArguments + .singleOrNull; + if (stateType == null) { + return null; + } + + final stateElement = stateType.element; + if (stateElement is! InterfaceElement) { + return null; + } + + final eventElement = + blocElement.allSupertypes + .firstWhereOrNull((t) => _bloc.isExactly(t.element)) + ?.typeArguments + .firstOrNull + ?.element; + if (eventElement is! InterfaceElement?) { + return null; + } + + final presentationEventElement = + blocElement.mixins + .firstWhereOrNull((m) => _blocPresentation.isExactly(m.element)) + ?.typeArguments + .elementAtOrNull(1) + ?.element; + if (presentationEventElement is! InterfaceElement?) { + return null; + } + + return ( + baseName: baseName, + blocElement: blocElement, + stateElement: stateElement, + eventElement: eventElement, + presentationEventElement: presentationEventElement, + ); +} + +bool inSameFile(Element element1, Element element2) { + final file1 = element1.source?.uri; + final file2 = element2.source?.uri; + + return file1 != null && file2 != null && file1 == file2; } diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index 337aa620..084446cb 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -5,6 +5,7 @@ import 'package:leancode_lint/assists/convert_record_into_nominal_type.dart'; import 'package:leancode_lint/lints/add_cubit_suffix_for_cubits.dart'; import 'package:leancode_lint/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dart'; +import 'package:leancode_lint/lints/bloc_related_classes_naming.dart'; import 'package:leancode_lint/lints/catch_parameter_names.dart'; import 'package:leancode_lint/lints/constructor_parameters_and_fields_should_have_the_same_order.dart'; import 'package:leancode_lint/lints/hook_widget_does_not_use_hooks.dart'; @@ -26,6 +27,7 @@ class _Linter extends PluginBase { HookWidgetDoesNotUseHooks(), ConstructorParametersAndFieldsShouldHaveTheSameOrder(), AvoidSingleChildInMultiChildWidgets(), + const BlocRelatedClassNaming(), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart new file mode 100644 index 00000000..1e2a1719 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart @@ -0,0 +1,42 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/helpers.dart'; + +class BlocRelatedClassNaming extends DartLintRule { + const BlocRelatedClassNaming() + : super( + code: const LintCode( + name: 'bloc_related_class_naming', + problemMessage: "The name of {0}'s {1} should be {2}.", + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + void checkClass(InterfaceElement? element, String type, String suffix) { + final expectedName = '${data.baseName}$suffix'; + if (element != null && + element.name != expectedName && + inSameFile(data.blocElement, element)) { + reporter.atElement( + element, + code, + arguments: [node.name.lexeme, type, expectedName], + ); + } + } + + checkClass(data.stateElement, 'state', 'State'); + checkClass(data.eventElement, 'event', 'Event'); + checkClass(data.presentationEventElement, 'presentation event', 'Event'); + }); + } +} From e618f171d54a1fe8160e2fb07807843d4d31f1b8 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 13:07:41 +0200 Subject: [PATCH 02/20] Add tests for `bloc_related_class_naming` --- .../lib/bloc_related_class_naming_test.dart | 83 +++++++++++++++++++ .../lib/bloc_related_class_naming_utils.dart | 1 + .../test/lints_test_app/pubspec.lock | 8 ++ .../test/lints_test_app/pubspec.yaml | 1 + 4 files changed, 93 insertions(+) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_utils.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart new file mode 100644 index 00000000..7a2edbce --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -0,0 +1,83 @@ +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lints_test_app/bloc_related_class_naming_utils.dart'; + +// region Incorrect blocs + +// Test for invalid state name +class Test1Cubit extends Cubit { + Test1Cubit() : super(WrongStateName()); +} + +// expect_lint: bloc_related_class_naming +class WrongStateName {} + +// Test for invalid event name +class Test2Bloc extends Bloc { + Test2Bloc() : super(Test2State()); +} + +// expect_lint: bloc_related_class_naming +class WrongEventName {} + +class Test2State {} + +// Test for invalid presentation event name +class Test3Bloc extends Bloc + with BlocPresentationMixin { + Test3Bloc() : super(Test3State()); +} + +class Test3Event {} + +class Test3State {} + +// expect_lint: bloc_related_class_naming +class WrongPresentationEventName {} + +// Invalid names detected for enums +class Test4Cubit extends Bloc + with BlocPresentationMixin { + Test4Cubit() : super(WrongEnumState.one); +} + +// expect_lint: bloc_related_class_naming +enum WrongEnumEvent { one } + +// expect_lint: bloc_related_class_naming +enum WrongEnumState { one } + +// expect_lint: bloc_related_class_naming +enum WrongEnumPresentationEvent { one } + +// endregion + +/////////////////////////////////////////////////////////////////////// + +// region Correct blocs + +// No lint when names are correct +class CorrectBloc extends Bloc + with BlocPresentationMixin { + CorrectBloc() : super(CorrectState()); +} + +class CorrectEvent {} + +class CorrectState {} + +class CorrectPresentationEvent {} + +// No lint when class is from the same package, but another file +class SamePackageBloc extends Bloc + with BlocPresentationMixin { + SamePackageBloc() : super(ClassFromSamePackage()); +} + +// No lint then class is from another package +class AnotherPackageBloc extends Bloc + with BlocPresentationMixin { + AnotherPackageBloc() : super(0); +} + +// endregion diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_utils.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_utils.dart new file mode 100644 index 00000000..b98b75bd --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_utils.dart @@ -0,0 +1 @@ +class ClassFromSamePackage {} diff --git a/packages/leancode_lint/test/lints_test_app/pubspec.lock b/packages/leancode_lint/test/lints_test_app/pubspec.lock index f4d5490b..c60c7e63 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.lock +++ b/packages/leancode_lint/test/lints_test_app/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.0" + bloc_presentation: + dependency: "direct main" + description: + name: bloc_presentation + sha256: "03ea22745a23274a7fa4425ac16e120838471d3073fa37a3332c18641cb2d8a2" + url: "https://pub.dev" + source: hosted + version: "1.1.0" boolean_selector: dependency: transitive description: diff --git a/packages/leancode_lint/test/lints_test_app/pubspec.yaml b/packages/leancode_lint/test/lints_test_app/pubspec.yaml index d4e96e26..f0e2966b 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.yaml +++ b/packages/leancode_lint/test/lints_test_app/pubspec.yaml @@ -9,6 +9,7 @@ environment: sdk: '>=3.7.0 <4.0.0' dependencies: + bloc_presentation: ^1.1.0 flutter: sdk: flutter flutter_bloc: ^9.1.1 From c7faf0bd351293c166f714d625d9c32f1aca4a63 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 14:45:09 +0200 Subject: [PATCH 03/20] Avoid conflict of event classes --- .../lints/bloc_related_classes_naming.dart | 6 ++++- .../lib/bloc_related_class_naming_test.dart | 22 ++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart index 1e2a1719..834ee0f2 100644 --- a/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart @@ -36,7 +36,11 @@ class BlocRelatedClassNaming extends DartLintRule { checkClass(data.stateElement, 'state', 'State'); checkClass(data.eventElement, 'event', 'Event'); - checkClass(data.presentationEventElement, 'presentation event', 'Event'); + checkClass( + data.presentationEventElement, + 'presentation event', + data.eventElement == null ? 'Event' : 'PresentationEvent', + ); }); } } diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart index 7a2edbce..4eafc492 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -57,16 +57,26 @@ enum WrongEnumPresentationEvent { one } // region Correct blocs // No lint when names are correct -class CorrectBloc extends Bloc - with BlocPresentationMixin { - CorrectBloc() : super(CorrectState()); +class Correct1Cubit extends Cubit + with BlocPresentationMixin { + Correct1Cubit() : super(Correct1State()); } -class CorrectEvent {} +class Correct1State {} -class CorrectState {} +class Correct1Event {} -class CorrectPresentationEvent {} +// No lint when names are correct, with event disambiguation +class Correct2Bloc extends Bloc + with BlocPresentationMixin { + Correct2Bloc() : super(Correct2State()); +} + +class Correct2Event {} + +class Correct2State {} + +class Correct2PresentationEvent {} // No lint when class is from the same package, but another file class SamePackageBloc extends Bloc From b5d3e852bd83dc87a5c1393f872f022a02d31648 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 14:51:16 +0200 Subject: [PATCH 04/20] Add the `prefer_equatable_mixin` lint --- packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lib/lints/use_equatable_mixin.dart | 81 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 packages/leancode_lint/lib/lints/use_equatable_mixin.dart diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index 084446cb..42a9c523 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -12,6 +12,7 @@ import 'package:leancode_lint/lints/hook_widget_does_not_use_hooks.dart'; import 'package:leancode_lint/lints/prefix_widgets_returning_slivers.dart'; import 'package:leancode_lint/lints/start_comments_with_space.dart'; import 'package:leancode_lint/lints/use_design_system_item.dart'; +import 'package:leancode_lint/lints/use_equatable_mixin.dart'; PluginBase createPlugin() => _Linter(); @@ -28,6 +29,7 @@ class _Linter extends PluginBase { ConstructorParametersAndFieldsShouldHaveTheSameOrder(), AvoidSingleChildInMultiChildWidgets(), const BlocRelatedClassNaming(), + const UseEquatableMixin(), ]; @override diff --git a/packages/leancode_lint/lib/lints/use_equatable_mixin.dart b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart new file mode 100644 index 00000000..45ba02fd --- /dev/null +++ b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:analyzer/source/source_range.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class UseEquatableMixin extends DartLintRule { + const UseEquatableMixin() + : super( + code: const LintCode( + name: 'prefer_equatable_mixin', + problemMessage: + 'The class {0} should mix in EquatableMixin instead of extending Equatable.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const typeCheckers = ( + equatable: TypeChecker.fromName('Equatable', packageName: 'equatable'), + equatableMixin: TypeChecker.fromName( + 'EquatableMixin', + packageName: 'equatable', + ), + ); + + @override + List getFixes() => [ConvertToMixin()]; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addClassDeclaration((node) { + final extendsClause = node.extendsClause; + if (extendsClause == null) { + return; + } + + final superType = extendsClause.superclass.type; + final isEquatable = + superType != null && typeCheckers.equatable.isExactlyType(superType); + + final isEquatableMixin = + node.withClause?.mixinTypes + .map((mixin) => mixin.type) + .nonNulls + .any(typeCheckers.equatableMixin.isExactlyType) ?? + false; + + if (isEquatable && !isEquatableMixin) { + reporter.atNode( + extendsClause.superclass, + code, + arguments: [node.name.lexeme], + data: extendsClause.sourceRange, + ); + } + }); + } +} + +class ConvertToMixin extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + reporter + .createChangeBuilder(message: 'Replace with a mixin', priority: 1) + .addDartFileEdit( + (builder) => builder.addSimpleReplacement( + analysisError.data! as SourceRange, + 'with EquatableMixin', + ), + ); + } +} From cd0e9224e24e3f0333f7f7828092be0df8b8564d Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 14:54:47 +0200 Subject: [PATCH 05/20] Add tests for `prefer_equatable_mixin` --- .../lib/prefer_equatable_mixin_test.dart | 22 +++++++++++++++++++ .../test/lints_test_app/pubspec.lock | 8 +++++++ .../test/lints_test_app/pubspec.yaml | 1 + 3 files changed, 31 insertions(+) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart b/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart new file mode 100644 index 00000000..c44a2095 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart @@ -0,0 +1,22 @@ +import 'package:equatable/equatable.dart'; + +// Directly extending Equatable is flagged +class MyState + extends + // expect_lint: prefer_equatable_mixin + Equatable { + @override + List get props => []; +} + +// Transitively extending Equatable is not flagged +class MyState2 extends MyState { + @override + List get props => []; +} + +// Using EquatableMixin is not flagged +class MyState3 with EquatableMixin { + @override + List get props => []; +} diff --git a/packages/leancode_lint/test/lints_test_app/pubspec.lock b/packages/leancode_lint/test/lints_test_app/pubspec.lock index c60c7e63..a896d283 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.lock +++ b/packages/leancode_lint/test/lints_test_app/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.1" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" file: dependency: transitive description: diff --git a/packages/leancode_lint/test/lints_test_app/pubspec.yaml b/packages/leancode_lint/test/lints_test_app/pubspec.yaml index f0e2966b..732ef6c0 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.yaml +++ b/packages/leancode_lint/test/lints_test_app/pubspec.yaml @@ -10,6 +10,7 @@ environment: dependencies: bloc_presentation: ^1.1.0 + equatable: ^2.0.7 flutter: sdk: flutter flutter_bloc: ^9.1.1 From 78b6a0d1169a22d6ca66ed5d77b7b37d64a80a16 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 15:25:28 +0200 Subject: [PATCH 06/20] Add the `bloc_subclasses_naming` lint --- packages/leancode_lint/lib/helpers.dart | 13 ++++++ packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lib/lints/bloc_subclasses_naming.dart | 43 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart diff --git a/packages/leancode_lint/lib/helpers.dart b/packages/leancode_lint/lib/helpers.dart index fad1152a..77a03095 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -301,3 +301,16 @@ bool inSameFile(Element element1, Element element2) { return file1 != null && file2 != null && file1 == file2; } + +extension TypeSubclasses on InterfaceElement { + Iterable get subclasses { + final typeChecker = TypeChecker.fromStatic(thisType); + return library.units + .expand((u) => u.classes) + .where( + (clazz) => + typeChecker.isAssignableFrom(clazz) && + !typeChecker.isExactly(clazz), + ); + } +} diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index 42a9c523..b9794166 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -6,6 +6,7 @@ import 'package:leancode_lint/lints/add_cubit_suffix_for_cubits.dart'; import 'package:leancode_lint/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dart'; import 'package:leancode_lint/lints/bloc_related_classes_naming.dart'; +import 'package:leancode_lint/lints/bloc_subclasses_naming.dart'; import 'package:leancode_lint/lints/catch_parameter_names.dart'; import 'package:leancode_lint/lints/constructor_parameters_and_fields_should_have_the_same_order.dart'; import 'package:leancode_lint/lints/hook_widget_does_not_use_hooks.dart'; @@ -30,6 +31,7 @@ class _Linter extends PluginBase { AvoidSingleChildInMultiChildWidgets(), const BlocRelatedClassNaming(), const UseEquatableMixin(), + const BlocSubclassesNaming(), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart b/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart new file mode 100644 index 00000000..45ed017b --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart @@ -0,0 +1,43 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/helpers.dart'; + +class BlocSubclassesNaming extends DartLintRule { + const BlocSubclassesNaming() + : super( + code: const LintCode( + name: 'bloc_subclasses_naming', + problemMessage: "{0}'s {1} subclasses should start with {2}", + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + void check(InterfaceElement? element, String type) { + if (element != null && inSameFile(data.blocElement, element)) { + for (final subtype in element.subclasses) { + if (!subtype.name.startsWith(element.name)) { + reporter.atElement( + subtype, + code, + arguments: [node.name.lexeme, type, element.name], + ); + } + } + } + } + + check(data.stateElement, 'state'); + check(data.eventElement, 'event'); + check(data.presentationEventElement, 'presentation event'); + }); + } +} From 911b7a7460098e8c36fc59cbb44d8741622b2761 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 15:37:52 +0200 Subject: [PATCH 07/20] Add tests for `bloc_subclasses_naming` --- .../lib/bloc_subclasses_naming_test.dart | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart new file mode 100644 index 00000000..fbb2f19e --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart @@ -0,0 +1,94 @@ +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Invalid name of a state subclass is flagged +class Test1Cubit extends Cubit { + Test1Cubit() : super(InvalidState()); +} + +sealed class Test1State {} + +final class +// expect_lint: bloc_subclasses_naming +InvalidState + extends Test1State {} + +////////////////////////////////////////////////////////////////////////// + +// Invalid name of an event subclass is flagged +class Test2Bloc extends Bloc { + Test2Bloc() : super(Test2State()); +} + +final class Test2State {} + +sealed class Test2Event {} + +final class +// expect_lint: bloc_subclasses_naming +InvalidEvent + extends Test2Event {} + +////////////////////////////////////////////////////////////////////////// + +// Invalid name of a presentation event subclass is flagged +class Test3Cubit extends Cubit + with BlocPresentationMixin { + Test3Cubit() : super(Test3State()); +} + +final class Test3State {} + +sealed class Test3Event {} + +final class +// expect_lint: bloc_subclasses_naming +InvalidPresentationEvent + extends Test3Event {} + +////////////////////////////////////////////////////////////////////////// + +// Valid subclass names for a cubit are not flagged + +class Test4Cubit extends Cubit + with BlocPresentationMixin { + Test4Cubit() : super(Test4StateFoo()); +} + +sealed class Test4State {} + +final class Test4StateFoo extends Test4State {} + +final class Test4StateBar extends Test4State {} + +sealed class Test4Event {} + +final class Test4EventFoo extends Test4Event {} + +final class Test4EventBar extends Test4Event {} + +////////////////////////////////////////////////////////////////////////// + +// Valid subclass names for a bloc are not flagged +class Test5Bloc extends Bloc + with BlocPresentationMixin { + Test5Bloc() : super(Test5StateFoo()); +} + +sealed class Test5State {} + +final class Test5StateFoo extends Test5State {} + +final class Test5StateBar extends Test5State {} + +sealed class Test5Event {} + +final class Test5EventFoo extends Test5Event {} + +final class Test5EventBar extends Test5Event {} + +sealed class Test5PresentationEvent {} + +final class Test5PresentationEventFoo extends Test5PresentationEvent {} + +final class Test5PresentationEventBar extends Test5PresentationEvent {} From 4094c288331faf9dcdba571d28a530c2b8549247 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:08:58 +0200 Subject: [PATCH 08/20] Add the `bloc_class_modifiers` lint --- packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lib/lints/bloc_class_modifiers.dart | 60 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 packages/leancode_lint/lib/lints/bloc_class_modifiers.dart diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index b9794166..ca2c729a 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -5,6 +5,7 @@ import 'package:leancode_lint/assists/convert_record_into_nominal_type.dart'; import 'package:leancode_lint/lints/add_cubit_suffix_for_cubits.dart'; import 'package:leancode_lint/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dart'; +import 'package:leancode_lint/lints/bloc_class_modifiers.dart'; import 'package:leancode_lint/lints/bloc_related_classes_naming.dart'; import 'package:leancode_lint/lints/bloc_subclasses_naming.dart'; import 'package:leancode_lint/lints/catch_parameter_names.dart'; @@ -32,6 +33,7 @@ class _Linter extends PluginBase { const BlocRelatedClassNaming(), const UseEquatableMixin(), const BlocSubclassesNaming(), + const BlocClassModifiers(), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart b/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart new file mode 100644 index 00000000..d73820b0 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart @@ -0,0 +1,60 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/helpers.dart'; + +class BlocClassModifiers extends DartLintRule { + const BlocClassModifiers() + : super( + code: const LintCode( + name: 'bloc_class_modifiers', + problemMessage: 'The class {0} should be {1}.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + void checkHierarchy(InterfaceElement? element) { + if (element is! ClassElement || + !inSameFile(data.blocElement, element)) { + return; + } + + final subclasses = element.subclasses; + + if (subclasses.isNotEmpty) { + if (!element.isSealed) { + reporter.atElement( + element, + code, + arguments: [element.name, 'sealed'], + data: 'sealed', + ); + } + } else { + if (!element.isFinal) { + reporter.atElement( + element, + code, + arguments: [element.name, 'final'], + data: 'final', + ); + } + } + + subclasses.forEach(checkHierarchy); + } + + checkHierarchy(data.stateElement); + checkHierarchy(data.eventElement); + checkHierarchy(data.presentationEventElement); + }); + } +} From 0fba7273a87eeb07248ba4b63762016820891e90 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:09:11 +0200 Subject: [PATCH 09/20] Add tests for `bloc_class_modifiers` --- .../lib/bloc_class_modifiers_test.dart | 90 +++++++++++++++++++ .../lib/bloc_related_class_naming_test.dart | 22 ++--- 2 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart new file mode 100644 index 00000000..8a295e92 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart @@ -0,0 +1,90 @@ +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Cubit's state and presentation event classes not final or sealed are flagged +class Test1Cubit extends Cubit + with BlocPresentationMixin { + Test1Cubit() : super(Test1StateInitial()); +} + +class +// expect_lint: bloc_class_modifiers +Test1State {} + +class +// expect_lint: bloc_class_modifiers +Test1StateInitial + extends Test1State {} + +class +// expect_lint: bloc_class_modifiers +Test1Event {} + +class +// expect_lint: bloc_class_modifiers +Test1EventFoo + extends Test1Event {} + +/////////////////////////////////////////////////////////////////////// + +// Bloc's state, event, and presentation event classes not final or sealed are flagged +class Test2Bloc extends Bloc + with BlocPresentationMixin { + Test2Bloc() : super(Test2StateInitial()); +} + +class +// expect_lint: bloc_class_modifiers +Test2State {} + +class +// expect_lint: bloc_class_modifiers +Test2StateInitial + extends Test2State {} + +class +// expect_lint: bloc_class_modifiers +Test2Event {} + +class +// expect_lint: bloc_class_modifiers +Test2EventFoo + extends Test2Event {} + +class +// expect_lint: bloc_class_modifiers +Test2PresentationEvent {} + +class +// expect_lint: bloc_class_modifiers +Test2PresentationEventFoo + extends Test2PresentationEvent {} + +/////////////////////////////////////////////////////////////////////// + +// The abstract modifier is flagged +class Test3Cubit extends Cubit { + Test3Cubit() : super(Test3StateInitial()); +} + +abstract class +// expect_lint: bloc_class_modifiers +Test3State {} + +final class Test3StateInitial extends Test3State {} + +/////////////////////////////////////////////////////////////////////// + +// Sealed and final classes are not flagged +class Test4Cubit extends Cubit + with BlocPresentationMixin { + Test4Cubit() : super(Test4StateInitial()); +} + +sealed class Test4State {} + +final class Test4StateInitial extends Test4State {} + +sealed class Test4Event {} + +final class Test4EventFoo extends Test4Event {} diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart index 4eafc492..ca43808b 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -10,7 +10,7 @@ class Test1Cubit extends Cubit { } // expect_lint: bloc_related_class_naming -class WrongStateName {} +final class WrongStateName {} // Test for invalid event name class Test2Bloc extends Bloc { @@ -18,9 +18,9 @@ class Test2Bloc extends Bloc { } // expect_lint: bloc_related_class_naming -class WrongEventName {} +final class WrongEventName {} -class Test2State {} +final class Test2State {} // Test for invalid presentation event name class Test3Bloc extends Bloc @@ -28,12 +28,12 @@ class Test3Bloc extends Bloc Test3Bloc() : super(Test3State()); } -class Test3Event {} +final class Test3Event {} -class Test3State {} +final class Test3State {} // expect_lint: bloc_related_class_naming -class WrongPresentationEventName {} +final class WrongPresentationEventName {} // Invalid names detected for enums class Test4Cubit extends Bloc @@ -62,9 +62,9 @@ class Correct1Cubit extends Cubit Correct1Cubit() : super(Correct1State()); } -class Correct1State {} +final class Correct1State {} -class Correct1Event {} +final class Correct1Event {} // No lint when names are correct, with event disambiguation class Correct2Bloc extends Bloc @@ -72,11 +72,11 @@ class Correct2Bloc extends Bloc Correct2Bloc() : super(Correct2State()); } -class Correct2Event {} +final class Correct2Event {} -class Correct2State {} +final class Correct2State {} -class Correct2PresentationEvent {} +final class Correct2PresentationEvent {} // No lint when class is from the same package, but another file class SamePackageBloc extends Bloc From b81699026590b7529f6457787a547ad4416cc895 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:25:40 +0200 Subject: [PATCH 10/20] Add the `bloc_related_classes_equatable` lint --- packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lints/bloc_related_classes_equatable.dart | 81 +++++++++++++++++++ .../lib/bloc_class_modifiers_test.dart | 3 + .../lib/bloc_related_class_naming_test.dart | 3 + .../lib/bloc_subclasses_naming_test.dart | 3 + 5 files changed, 92 insertions(+) create mode 100644 packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index ca2c729a..2438f544 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -6,6 +6,7 @@ import 'package:leancode_lint/lints/add_cubit_suffix_for_cubits.dart'; import 'package:leancode_lint/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dart'; import 'package:leancode_lint/lints/bloc_class_modifiers.dart'; +import 'package:leancode_lint/lints/bloc_related_classes_equatable.dart'; import 'package:leancode_lint/lints/bloc_related_classes_naming.dart'; import 'package:leancode_lint/lints/bloc_subclasses_naming.dart'; import 'package:leancode_lint/lints/catch_parameter_names.dart'; @@ -34,6 +35,7 @@ class _Linter extends PluginBase { const UseEquatableMixin(), const BlocSubclassesNaming(), const BlocClassModifiers(), + const BlocRelatedClassesEquatable(), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart new file mode 100644 index 00000000..9170f86e --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart @@ -0,0 +1,81 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/helpers.dart'; + +class BlocRelatedClassesEquatable extends DartLintRule { + const BlocRelatedClassesEquatable() + : super( + code: const LintCode( + name: 'bloc_related_classes_equatable', + problemMessage: 'The class {0} should mix in EquatableMixin.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + List getFixes() => [AddMixin()]; + + static const _equatableMixin = TypeChecker.fromName( + 'EquatableMixin', + packageName: 'equatable', + ); + + static const _equatable = TypeChecker.fromName( + 'Equatable', + packageName: 'equatable', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + void check(InterfaceElement? element) { + if (element is! ClassElement || + !inSameFile(data.blocElement, element)) { + return; + } + + final isEquatableMixin = element.mixins.any( + _equatableMixin.isExactlyType, + ); + final isEquatable = _equatable.isAssignableFrom(element); + + if (!isEquatableMixin && !isEquatable) { + reporter.atElement(element, code, arguments: [element.name]); + } + } + + check(data.stateElement); + check(data.eventElement); + check(data.presentationEventElement); + }); + } +} + +class AddMixin extends DartFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + reporter + .createChangeBuilder(message: 'Add EquatableMixin', priority: 1) + .addDartFileEdit( + (builder) => + builder + ..importLibrary(Uri.parse('package:equatable/equatable.dart')) + ..addSimpleInsertion( + analysisError.offset + analysisError.length, + ' with EquatableMixin', + ), + ); + } +} diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart index 8a295e92..60918a21 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart @@ -1,3 +1,6 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable + import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart index ca43808b..1cf4c78b 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -1,3 +1,6 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable + import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lints_test_app/bloc_related_class_naming_utils.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart index fbb2f19e..3bd4c51d 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart @@ -1,3 +1,6 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable + import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; From 8af91e05efcd7edf39cd6ecb913bb0985158d97c Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:25:48 +0200 Subject: [PATCH 11/20] Add tests for `bloc_related_classes_equatable` --- .../bloc_related_classes_equatable_test.dart | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart new file mode 100644 index 00000000..9bd5245e --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart @@ -0,0 +1,84 @@ +// for tests +// ignore_for_file: prefer_equatable_mixin + +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Bloc-related classes that are not `equatable` are flagged +class Test1Bloc extends Bloc + with BlocPresentationMixin { + Test1Bloc() : super(Test1State()); +} + +final class +// expect_lint: bloc_related_classes_equatable +Test1Event {} + +final class +// expect_lint: bloc_related_classes_equatable +Test1State {} + +final class +// expect_lint: bloc_related_classes_equatable +Test1PresentationEvent {} + +////////////////////////////////////////////////////////////////////////// + +// Bloc-related classes that `extend Equatable` are not flagged +class Test2Bloc extends Bloc + with BlocPresentationMixin { + Test2Bloc() : super(Test2State()); +} + +final class Test2Event extends Equatable { + @override + List get props => []; +} + +final class Test2State extends Equatable { + @override + List get props => []; +} + +final class Test2PresentationEvent extends Equatable { + @override + List get props => []; +} + +////////////////////////////////////////////////////////////////////////// + +// Bloc-related classes that `mixin EquatableMixin` are not flagged +class Test3Bloc extends Bloc + with BlocPresentationMixin { + Test3Bloc() : super(Test3State()); +} + +final class Test3Event with EquatableMixin { + @override + List get props => []; +} + +final class Test3State with EquatableMixin { + @override + List get props => []; +} + +final class Test3PresentationEvent with EquatableMixin { + @override + List get props => []; +} + +////////////////////////////////////////////////////////////////////////// + +// Bloc-related enums are not flagged +class Test4Bloc extends Bloc + with BlocPresentationMixin { + Test4Bloc() : super(Test4State.one); +} + +enum Test4Event { event1 } + +enum Test4State { one } + +enum Test4PresentationEvent { event1 } From 2ec49e557aa02b6803c41675d7e010fd6105ee29 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:46:20 +0200 Subject: [PATCH 12/20] Add the `bloc_const_constructors` lint --- packages/leancode_lint/lib/leancode_lint.dart | 2 + .../lib/lints/bloc_const_constructors.dart | 46 +++++++++++++++++++ .../lib/bloc_class_modifiers_test.dart | 2 +- .../lib/bloc_related_class_naming_test.dart | 2 +- .../bloc_related_classes_equatable_test.dart | 2 +- .../lib/bloc_subclasses_naming_test.dart | 2 +- 6 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 packages/leancode_lint/lib/lints/bloc_const_constructors.dart diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index 2438f544..3d535653 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -6,6 +6,7 @@ import 'package:leancode_lint/lints/add_cubit_suffix_for_cubits.dart'; import 'package:leancode_lint/lints/avoid_conditional_hooks.dart'; import 'package:leancode_lint/lints/avoid_single_child_in_multi_child_widget.dart'; import 'package:leancode_lint/lints/bloc_class_modifiers.dart'; +import 'package:leancode_lint/lints/bloc_const_constructors.dart'; import 'package:leancode_lint/lints/bloc_related_classes_equatable.dart'; import 'package:leancode_lint/lints/bloc_related_classes_naming.dart'; import 'package:leancode_lint/lints/bloc_subclasses_naming.dart'; @@ -36,6 +37,7 @@ class _Linter extends PluginBase { const BlocSubclassesNaming(), const BlocClassModifiers(), const BlocRelatedClassesEquatable(), + const BlocConstConstructors(), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_const_constructors.dart b/packages/leancode_lint/lib/lints/bloc_const_constructors.dart new file mode 100644 index 00000000..ef2c283c --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_const_constructors.dart @@ -0,0 +1,46 @@ +import 'package:analyzer/error/error.dart' hide LintCode; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/helpers.dart'; + +class BlocConstConstructors extends DartLintRule { + const BlocConstConstructors() + : super( + code: const LintCode( + name: 'bloc_const_constructors', + problemMessage: + 'The class {0} should have an unnamed const constructor.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + final elements = { + for (final element in [ + data.stateElement, + data.eventElement, + data.presentationEventElement, + ]) + if (element != null) ...{element, ...element.subclasses}, + }; + + for (final element in elements) { + if (element.unnamedConstructor case final unnamedConstructor? + when !unnamedConstructor.isConst && + inSameFile(data.blocElement, element)) { + reporter.atElement( + unnamedConstructor.isSynthetic ? element : unnamedConstructor, + code, + arguments: [element.name], + ); + } + } + }); + } +} diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart index 60918a21..8b1d1ba2 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart @@ -1,5 +1,5 @@ // for tests -// ignore_for_file: bloc_related_classes_equatable +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart index 1cf4c78b..68bceb46 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -1,5 +1,5 @@ // for tests -// ignore_for_file: bloc_related_classes_equatable +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart index 9bd5245e..5db7f5d1 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_classes_equatable_test.dart @@ -1,5 +1,5 @@ // for tests -// ignore_for_file: prefer_equatable_mixin +// ignore_for_file: prefer_equatable_mixin, bloc_const_constructors import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:equatable/equatable.dart'; diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart index 3bd4c51d..6f4de360 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart @@ -1,5 +1,5 @@ // for tests -// ignore_for_file: bloc_related_classes_equatable +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; From 68be7ef0d7d10ae55ecc72f053559b474ef091b8 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 16:46:29 +0200 Subject: [PATCH 13/20] Add tests for `bloc_const_constructors` --- .../lib/bloc_const_constructors_test.dart | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 packages/leancode_lint/test/lints_test_app/lib/bloc_const_constructors_test.dart diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_const_constructors_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_const_constructors_test.dart new file mode 100644 index 00000000..a5e3c733 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_const_constructors_test.dart @@ -0,0 +1,66 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable, bloc_class_modifiers + +import 'package:bloc_presentation/bloc_presentation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Bloc-related classes without an explicit unnamed constructor are flagged +class Test1Bloc extends Bloc + with BlocPresentationMixin { + Test1Bloc() : super(Test1State()); +} + +class +// expect_lint: bloc_const_constructors +Test1Event {} + +class +// expect_lint: bloc_const_constructors +Test1State {} + +class +// expect_lint: bloc_const_constructors +Test1PresentationEvent {} + +////////////////////////////////////////////////////////////////////////// + +// Bloc-related classes with a non-const unnamed constructor are flagged +class Test2Bloc extends Bloc + with BlocPresentationMixin { + Test2Bloc() : super(Test2State()); +} + +class Test2Event { + // expect_lint: bloc_const_constructors + Test2Event(); +} + +class Test2State { + // expect_lint: bloc_const_constructors + Test2State(); +} + +class Test2PresentationEvent { + // expect_lint: bloc_const_constructors + Test2PresentationEvent(); +} + +////////////////////////////////////////////////////////////////////////// + +// Bloc-related classes with a const unnamed constructor are not flagged +class Test3Bloc extends Bloc + with BlocPresentationMixin { + Test3Bloc() : super(const Test3State()); +} + +class Test3Event { + const Test3Event(); +} + +class Test3State { + const Test3State(); +} + +class Test3PresentationEvent { + const Test3PresentationEvent(); +} From 41034b308be09198e11971d94dd80476644602c4 Mon Sep 17 00:00:00 2001 From: PiotrRogulski Date: Fri, 9 May 2025 17:00:26 +0200 Subject: [PATCH 14/20] Collect type checkers instances --- ...onvert_iterable_map_to_collection_for.dart | 5 +- .../lib/common_type_checkers.dart | 75 +++++++++++++++++++ packages/leancode_lint/lib/helpers.dart | 36 ++++----- .../lints/add_cubit_suffix_for_cubits.dart | 6 +- ...id_single_child_in_multi_child_widget.dart | 31 +++----- .../lints/bloc_related_classes_equatable.dart | 15 +--- .../lib/lints/use_equatable_mixin.dart | 13 +--- 7 files changed, 110 insertions(+), 71 deletions(-) create mode 100644 packages/leancode_lint/lib/common_type_checkers.dart diff --git a/packages/leancode_lint/lib/assists/convert_iterable_map_to_collection_for.dart b/packages/leancode_lint/lib/assists/convert_iterable_map_to_collection_for.dart index 8e032972..5f182887 100644 --- a/packages/leancode_lint/lib/assists/convert_iterable_map_to_collection_for.dart +++ b/packages/leancode_lint/lib/assists/convert_iterable_map_to_collection_for.dart @@ -1,6 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; import 'package:leancode_lint/helpers.dart'; /// Converts an iterable call to [Iterable.map] with an optional @@ -39,8 +40,6 @@ class ConvertIterableMapToCollectionFor extends DartAssist { } void _handleIterable(MethodInvocation node, ChangeReporter reporter) { - const iterableChecker = TypeChecker.fromUrl('dart:core#Iterable'); - if (node case MethodInvocation( target: Expression( staticType: final targetType?, @@ -57,7 +56,7 @@ class ConvertIterableMapToCollectionFor extends DartAssist { ), ], ), - ) when iterableChecker.isAssignableFromType(targetType)) { + ) when TypeCheckers.iterable.isAssignableFromType(targetType)) { final expression = maybeGetSingleReturnExpression(functionBody); if (expression == null) { return; diff --git a/packages/leancode_lint/lib/common_type_checkers.dart b/packages/leancode_lint/lib/common_type_checkers.dart new file mode 100644 index 00000000..e4439066 --- /dev/null +++ b/packages/leancode_lint/lib/common_type_checkers.dart @@ -0,0 +1,75 @@ +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +extension TypeCheckers on Never { + // dart + static const iterable = TypeChecker.fromUrl('dart:core#Iterable'); + + // equatable + static const equatable = TypeChecker.fromName( + 'Equatable', + packageName: 'equatable', + ); + static const equatableMixin = TypeChecker.fromName( + 'EquatableMixin', + packageName: 'equatable', + ); + + // flutter + static const statelessWidget = TypeChecker.fromName( + 'StatelessWidget', + packageName: 'flutter', + ); + static const state = TypeChecker.fromName('State', packageName: 'flutter'); + static const column = TypeChecker.fromName('Column', packageName: 'flutter'); + static const row = TypeChecker.fromName('Row', packageName: 'flutter'); + static const wrap = TypeChecker.fromName('Wrap', packageName: 'flutter'); + static const flex = TypeChecker.fromName('Flex', packageName: 'flutter'); + static const sliverList = TypeChecker.fromName( + 'SliverList', + packageName: 'flutter', + ); + static const sliverMainAxisGroup = TypeChecker.fromName( + 'SliverMainAxisGroup', + packageName: 'flutter', + ); + static const sliverCrossAxisGroup = TypeChecker.fromName( + 'SliverCrossAxisGroup', + packageName: 'flutter', + ); + static const sliverChildListDelegate = TypeChecker.fromName( + 'SliverChildListDelegate', + packageName: 'flutter', + ); + + static const multiSliver = TypeChecker.fromName( + 'MultiSliver', + packageName: 'sliver_tools', + ); + + // hooks + static const hookWidget = TypeChecker.fromName( + 'HookWidget', + packageName: 'flutter_hooks', + ); + static const hookBuilder = TypeChecker.fromName( + 'HookBuilder', + packageName: 'flutter_hooks', + ); + static const hookConsumer = TypeChecker.fromName( + 'HookConsumer', + packageName: 'hooks_riverpod', + ); + static const hookConsumerWidget = TypeChecker.fromName( + 'HookConsumerWidget', + packageName: 'hooks_riverpod', + ); + + // bloc + static const cubit = TypeChecker.fromName('Cubit', packageName: 'bloc'); + static const bloc = TypeChecker.fromName('Bloc', packageName: 'bloc'); + static const blocBase = TypeChecker.fromName('BlocBase', packageName: 'bloc'); + static const blocPresentation = TypeChecker.fromName( + 'BlocPresentationMixin', + packageName: 'bloc_presentation', + ); +} diff --git a/packages/leancode_lint/lib/helpers.dart b/packages/leancode_lint/lib/helpers.dart index 77a03095..9214b64b 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -3,6 +3,7 @@ import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; import 'package:leancode_lint/utils.dart'; String typeParametersString( @@ -76,8 +77,8 @@ FunctionBody? maybeHookBuilderBody(InstanceCreationExpression node) { } final isHookBuilder = const TypeChecker.any([ - TypeChecker.fromName('HookBuilder', packageName: 'flutter_hooks'), - TypeChecker.fromName('HookConsumer', packageName: 'hooks_riverpod'), + TypeCheckers.hookBuilder, + TypeCheckers.hookConsumer, ]).isExactly(classElement); if (!isHookBuilder) { return null; @@ -126,9 +127,9 @@ List getAllReturnExpressions(FunctionBody body) { bool isWidgetClass(ClassDeclaration node) => switch (node.declaredElement) { final element? => const TypeChecker.any([ - TypeChecker.fromName('StatelessWidget', packageName: 'flutter'), - TypeChecker.fromName('State', packageName: 'flutter'), - TypeChecker.fromName('HookWidget', packageName: 'flutter_hooks'), + TypeCheckers.statelessWidget, + TypeCheckers.state, + TypeCheckers.hookWidget, ]).isSuperOf(element), _ => false, }; @@ -181,11 +182,8 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { } const checker = TypeChecker.any([ - TypeChecker.fromName('HookWidget', packageName: 'flutter_hooks'), - TypeChecker.fromName( - 'HookConsumerWidget', - packageName: 'hooks_riverpod', - ), + TypeCheckers.hookWidget, + TypeCheckers.hookConsumerWidget, ]); final AstNode diagnosticNode; @@ -227,13 +225,6 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { } } -const _blocBase = TypeChecker.fromName('BlocBase', packageName: 'bloc'); -const _bloc = TypeChecker.fromName('Bloc', packageName: 'bloc'); -const _blocPresentation = TypeChecker.fromName( - 'BlocPresentationMixin', - packageName: 'bloc_presentation', -); - typedef _BlocData = ({ String baseName, @@ -246,7 +237,8 @@ typedef _BlocData = _BlocData? _maybeBlocData(ClassDeclaration clazz) { final blocElement = clazz.declaredElement; - if (blocElement == null || !_blocBase.isAssignableFrom(blocElement)) { + if (blocElement == null || + !TypeCheckers.blocBase.isAssignableFrom(blocElement)) { return null; } @@ -254,7 +246,7 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final stateType = blocElement.allSupertypes - .firstWhere((t) => _blocBase.isExactly(t.element)) + .firstWhere((t) => TypeCheckers.blocBase.isExactly(t.element)) .typeArguments .singleOrNull; if (stateType == null) { @@ -268,7 +260,7 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final eventElement = blocElement.allSupertypes - .firstWhereOrNull((t) => _bloc.isExactly(t.element)) + .firstWhereOrNull((t) => TypeCheckers.bloc.isExactly(t.element)) ?.typeArguments .firstOrNull ?.element; @@ -278,7 +270,9 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final presentationEventElement = blocElement.mixins - .firstWhereOrNull((m) => _blocPresentation.isExactly(m.element)) + .firstWhereOrNull( + (m) => TypeCheckers.blocPresentation.isExactly(m.element), + ) ?.typeArguments .elementAtOrNull(1) ?.element; diff --git a/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart b/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart index 5990272f..1fa65aeb 100644 --- a/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart +++ b/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart @@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; /// Displays warning for cubits which do not have the `Cubit` suffix in their /// class name. @@ -34,10 +35,7 @@ class AddCubitSuffixForYourCubits extends DartLintRule { bool _hasCubitSuffix(String className) => className.endsWith('Cubit'); bool _isCubitClass(ClassDeclaration node) => switch (node.declaredElement) { - final element? => const TypeChecker.fromName( - 'Cubit', - packageName: 'bloc', - ).isSuperOf(element), + final element? => TypeCheckers.cubit.isSuperOf(element), _ => false, }; diff --git a/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart b/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart index fa721cc2..a124cd41 100644 --- a/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart +++ b/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart @@ -2,6 +2,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; import 'package:leancode_lint/utils.dart'; /// Enforces that some widgets that accept multiple children do not have a single child. @@ -17,27 +18,15 @@ class AvoidSingleChildInMultiChildWidgets extends DartLintRule { ); static const _complain = [ - ('children', TypeChecker.fromName('Column', packageName: 'flutter')), - ('children', TypeChecker.fromName('Row', packageName: 'flutter')), - ('children', TypeChecker.fromName('Wrap', packageName: 'flutter')), - ('children', TypeChecker.fromName('Flex', packageName: 'flutter')), - ('children', TypeChecker.fromName('SliverList', packageName: 'flutter')), - ( - 'slivers', - TypeChecker.fromName('SliverMainAxisGroup', packageName: 'flutter'), - ), - ( - 'slivers', - TypeChecker.fromName('SliverCrossAxisGroup', packageName: 'flutter'), - ), - ( - 'children', - TypeChecker.fromName('MultiSliver', packageName: 'sliver_tools'), - ), - ( - 'children', - TypeChecker.fromName('SliverChildListDelegate', packageName: 'flutter'), - ), + ('children', TypeCheckers.column), + ('children', TypeCheckers.row), + ('children', TypeCheckers.wrap), + ('children', TypeCheckers.flex), + ('children', TypeCheckers.sliverList), + ('slivers', TypeCheckers.sliverMainAxisGroup), + ('slivers', TypeCheckers.sliverCrossAxisGroup), + ('children', TypeCheckers.multiSliver), + ('children', TypeCheckers.sliverChildListDelegate), ]; @override diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart index 9170f86e..6d94f50a 100644 --- a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart @@ -2,6 +2,7 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; import 'package:leancode_lint/helpers.dart'; class BlocRelatedClassesEquatable extends DartLintRule { @@ -17,16 +18,6 @@ class BlocRelatedClassesEquatable extends DartLintRule { @override List getFixes() => [AddMixin()]; - static const _equatableMixin = TypeChecker.fromName( - 'EquatableMixin', - packageName: 'equatable', - ); - - static const _equatable = TypeChecker.fromName( - 'Equatable', - packageName: 'equatable', - ); - @override void run( CustomLintResolver resolver, @@ -41,9 +32,9 @@ class BlocRelatedClassesEquatable extends DartLintRule { } final isEquatableMixin = element.mixins.any( - _equatableMixin.isExactlyType, + TypeCheckers.equatableMixin.isExactlyType, ); - final isEquatable = _equatable.isAssignableFrom(element); + final isEquatable = TypeCheckers.equatable.isAssignableFrom(element); if (!isEquatableMixin && !isEquatable) { reporter.atElement(element, code, arguments: [element.name]); diff --git a/packages/leancode_lint/lib/lints/use_equatable_mixin.dart b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart index 45ba02fd..b2ad202d 100644 --- a/packages/leancode_lint/lib/lints/use_equatable_mixin.dart +++ b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart @@ -2,6 +2,7 @@ import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:analyzer/source/source_range.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:leancode_lint/common_type_checkers.dart'; class UseEquatableMixin extends DartLintRule { const UseEquatableMixin() @@ -14,14 +15,6 @@ class UseEquatableMixin extends DartLintRule { ), ); - static const typeCheckers = ( - equatable: TypeChecker.fromName('Equatable', packageName: 'equatable'), - equatableMixin: TypeChecker.fromName( - 'EquatableMixin', - packageName: 'equatable', - ), - ); - @override List getFixes() => [ConvertToMixin()]; @@ -39,13 +32,13 @@ class UseEquatableMixin extends DartLintRule { final superType = extendsClause.superclass.type; final isEquatable = - superType != null && typeCheckers.equatable.isExactlyType(superType); + superType != null && TypeCheckers.equatable.isExactlyType(superType); final isEquatableMixin = node.withClause?.mixinTypes .map((mixin) => mixin.type) .nonNulls - .any(typeCheckers.equatableMixin.isExactlyType) ?? + .any(TypeCheckers.equatableMixin.isExactlyType) ?? false; if (isEquatable && !isEquatableMixin) { From b3e78b26d8f0d379c236e6bc922e373b527a0411 Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Fri, 9 May 2025 21:55:23 +0200 Subject: [PATCH 15/20] Clean up tests --- .../lib/bloc_related_class_naming_test.dart | 34 +++++++++++-------- .../lib/prefer_equatable_mixin_test.dart | 4 +++ 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart index 68bceb46..7095cf8a 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -5,9 +5,7 @@ import 'package:bloc_presentation/bloc_presentation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:lints_test_app/bloc_related_class_naming_utils.dart'; -// region Incorrect blocs - -// Test for invalid state name +// Invalid state name is flagged class Test1Cubit extends Cubit { Test1Cubit() : super(WrongStateName()); } @@ -15,7 +13,9 @@ class Test1Cubit extends Cubit { // expect_lint: bloc_related_class_naming final class WrongStateName {} -// Test for invalid event name +////////////////////////////////////////////////////////////////////////// + +// Invalid bloc event name is flagged class Test2Bloc extends Bloc { Test2Bloc() : super(Test2State()); } @@ -25,7 +25,9 @@ final class WrongEventName {} final class Test2State {} -// Test for invalid presentation event name +////////////////////////////////////////////////////////////////////////// + +// Invalid presentation event name is flagged class Test3Bloc extends Bloc with BlocPresentationMixin { Test3Bloc() : super(Test3State()); @@ -38,10 +40,12 @@ final class Test3State {} // expect_lint: bloc_related_class_naming final class WrongPresentationEventName {} -// Invalid names detected for enums -class Test4Cubit extends Bloc +////////////////////////////////////////////////////////////////////////// + +// Invalid names of enums are flagged +class Test4Bloc extends Bloc with BlocPresentationMixin { - Test4Cubit() : super(WrongEnumState.one); + Test4Bloc() : super(WrongEnumState.one); } // expect_lint: bloc_related_class_naming @@ -53,12 +57,8 @@ enum WrongEnumState { one } // expect_lint: bloc_related_class_naming enum WrongEnumPresentationEvent { one } -// endregion - /////////////////////////////////////////////////////////////////////// -// region Correct blocs - // No lint when names are correct class Correct1Cubit extends Cubit with BlocPresentationMixin { @@ -69,6 +69,8 @@ final class Correct1State {} final class Correct1Event {} +////////////////////////////////////////////////////////////////////////// + // No lint when names are correct, with event disambiguation class Correct2Bloc extends Bloc with BlocPresentationMixin { @@ -81,16 +83,18 @@ final class Correct2State {} final class Correct2PresentationEvent {} +////////////////////////////////////////////////////////////////////////// + // No lint when class is from the same package, but another file class SamePackageBloc extends Bloc with BlocPresentationMixin { SamePackageBloc() : super(ClassFromSamePackage()); } -// No lint then class is from another package +////////////////////////////////////////////////////////////////////////// + +// No lint when class is from another package class AnotherPackageBloc extends Bloc with BlocPresentationMixin { AnotherPackageBloc() : super(0); } - -// endregion diff --git a/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart b/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart index c44a2095..fdb99ae8 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart @@ -9,12 +9,16 @@ class MyState List get props => []; } +////////////////////////////////////////////////////////////////////////// + // Transitively extending Equatable is not flagged class MyState2 extends MyState { @override List get props => []; } +////////////////////////////////////////////////////////////////////////// + // Using EquatableMixin is not flagged class MyState3 with EquatableMixin { @override From 577c20373776a95912c813f5860f4bbb7e0e3d6e Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Fri, 9 May 2025 22:13:28 +0200 Subject: [PATCH 16/20] Add new lints to README.md --- packages/leancode_lint/README.md | 197 +++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) diff --git a/packages/leancode_lint/README.md b/packages/leancode_lint/README.md index fddcb05f..9b1f35e2 100644 --- a/packages/leancode_lint/README.md +++ b/packages/leancode_lint/README.md @@ -341,6 +341,203 @@ Container(), None. +### `bloc_class_modifiers` + +**DO** add `final` or `sealed` modifiers to bloc state, event, and presentation event classes. + +**BAD:** + +```dart +class MyState {} + +class MyStateInitial extends MyState {} + +class MyStateLoading extends MyState {} +``` + +**GOOD:** + +```dart +sealed class MyState {} + +final class MyStateInitial extends MyState {} + +final class MyStateLoading extends MyState {} +``` + +#### Configuration + +None. + +### `bloc_const_constructors` + +**DO** define unnamed const constructors for bloc state, event, and presentation event classes. + +**BAD:** + +```dart +class MyState {} +``` + +**BAD:** + +```dart +class MyState { + MyState(); +} +``` + +**GOOD:** + +```dart +class MyState { + const MyState(); +} +``` + +#### Configuration + +None. + +### `bloc_related_classes_equatable` + +**DO** mix in `EquatableMixin` in bloc state, event, and presentation event classes. + +**BAD:** + +```dart +class MyState { + MyState(this.value); + + final int value; +} +``` + +**GOOD:** + +```dart +import 'package:equatable/equatable.dart'; + +class MyState with EquatableMixin { + const MyState(this.value); + + final int value; + + @override + List get props => [value]; +} +``` + +#### Configuration + +None. + +### `bloc_related_class_naming` + +**DO** prefix bloc state, event, and presentation event classes with the name of the bloc. + +**BAD:** + +```dart +class MyAwesomeBloc extends Bloc { + // ... +} + +class WrongEvent {} + +class SomeState {} +``` + +**GOOD:** + +```dart +class MyAwesomeBloc extends Bloc { + // ... +} + +class MyAwesomeEvent {} + +class MyAwesomeState {} +``` + +#### Configuration + +None. + +### `bloc_subclasses_naming` + +**DO** prefix bloc state, event and presentation event subclasses with the name of the base class. + +**BAD:** + +```dart +class MyAwesomeBloc extends Bloc { + // ... +} + +sealed class MyAwesomeState {} + +final class SomeState extends MyAwesomeState {} + +final class AnotherState extends MyAwesomeState {} +``` + +**GOOD:** + +```dart +class MyAwesomeBloc extends Bloc { + // ... +} + +sealed class MyAwesomeState {} + +final class MyAwesomeStateInitial extends MyAwesomeState {} + +final class MyAwesomeStateLoading extends MyAwesomeState {} +``` + +#### Configuration + +None. + +### `prefer_equatable_mixin` + +**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 get props => [value]; +} +``` + +**GOOD:** + +```dart +import 'package:equatable/equatable.dart'; + +class Foobar with EquatableMixin { + const Foobar(this.value); + + final int value; + + @override + List 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 ctrl+. or +.. From 99c86fe7ca08de1d2d2bbc2590f048e4e953f6e5 Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Fri, 9 May 2025 23:09:32 +0200 Subject: [PATCH 17/20] Simplify checking types --- packages/leancode_lint/lib/helpers.dart | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/leancode_lint/lib/helpers.dart b/packages/leancode_lint/lib/helpers.dart index 9214b64b..4fb762be 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -246,7 +246,7 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final stateType = blocElement.allSupertypes - .firstWhere((t) => TypeCheckers.blocBase.isExactly(t.element)) + .firstWhere(TypeCheckers.blocBase.isExactlyType) .typeArguments .singleOrNull; if (stateType == null) { @@ -260,7 +260,7 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final eventElement = blocElement.allSupertypes - .firstWhereOrNull((t) => TypeCheckers.bloc.isExactly(t.element)) + .firstWhereOrNull(TypeCheckers.bloc.isExactlyType) ?.typeArguments .firstOrNull ?.element; @@ -270,9 +270,7 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { final presentationEventElement = blocElement.mixins - .firstWhereOrNull( - (m) => TypeCheckers.blocPresentation.isExactly(m.element), - ) + .firstWhereOrNull(TypeCheckers.blocPresentation.isExactlyType) ?.typeArguments .elementAtOrNull(1) ?.element; From 27f12a2824e15e2a399afe8199a4f59717eb3769 Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Wed, 13 Aug 2025 10:48:46 +0200 Subject: [PATCH 18/20] Replace deprecated usages --- packages/leancode_lint/lib/helpers.dart | 85 +++++++++---------- .../lints/add_cubit_suffix_for_cubits.dart | 9 +- ...id_single_child_in_multi_child_widget.dart | 6 +- .../lib/lints/bloc_class_modifiers.dart | 14 +-- .../lib/lints/bloc_const_constructors.dart | 21 +++-- .../lints/bloc_related_classes_equatable.dart | 25 +++--- .../lints/bloc_related_classes_naming.dart | 8 +- .../lib/lints/bloc_subclasses_naming.dart | 10 +-- ...and_fields_should_have_the_same_order.dart | 2 +- .../lib/lints/use_instead_type.dart | 16 ++-- 10 files changed, 103 insertions(+), 93 deletions(-) diff --git a/packages/leancode_lint/lib/helpers.dart b/packages/leancode_lint/lib/helpers.dart index 27d2066d..d4a3a9a4 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -1,7 +1,7 @@ import 'package:analyzer/dart/ast/ast.dart'; import 'package:analyzer/dart/ast/token.dart'; import 'package:analyzer/dart/ast/visitor.dart'; -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/error/error.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -73,15 +73,15 @@ List getAllInnerHookExpressions(AstNode node) { /// Given an instance creation, returns the builder function body if the node is a HookBuilder. FunctionBody? maybeHookBuilderBody(InstanceCreationExpression node) { - final classElement = node.constructorName.type.element; - if (classElement == null) { + final type = node.constructorName.type.type; + if (type == null) { return null; } final isHookBuilder = const TypeChecker.any([ TypeCheckers.hookBuilder, TypeCheckers.hookConsumer, - ]).isExactly(classElement); + ]).isExactlyType(type); if (!isHookBuilder) { return null; } @@ -127,14 +127,15 @@ List getAllReturnExpressions(FunctionBody body) { }; } -bool isWidgetClass(ClassDeclaration node) => switch (node.declaredElement) { - final element? => const TypeChecker.any([ - TypeCheckers.statelessWidget, - TypeCheckers.state, - TypeCheckers.hookWidget, - ]).isSuperOf(element), - _ => false, -}; +bool isWidgetClass(ClassDeclaration node) => + switch (node.declaredFragment?.element.thisType) { + final type? => const TypeChecker.any([ + TypeCheckers.statelessWidget, + TypeCheckers.state, + TypeCheckers.hookWidget, + ]).isSuperTypeOf(type), + _ => false, + }; MethodDeclaration? getBuildMethod(ClassDeclaration node) => node.members .whereType() @@ -178,8 +179,8 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { } }); addClassDeclaration((node) { - final element = node.declaredElement; - if (element == null) { + final thisType = node.declaredFragment?.element.thisType; + if (thisType == null) { return; } @@ -191,18 +192,18 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { final AstNode diagnosticNode; if (isExactly) { final superclass = node.extendsClause?.superclass; - final superclassElement = superclass?.element; - if (superclass == null || superclassElement == null) { + final superclassType = superclass?.type; + if (superclass == null || superclassType == null) { return; } - final isDirectHookWidget = checker.isExactly(superclassElement); + final isDirectHookWidget = checker.isExactlyType(superclassType); if (!isDirectHookWidget) { return; } diagnosticNode = superclass; } else { - final isHookWidget = checker.isSuperOf(element); + final isHookWidget = checker.isSuperTypeOf(thisType); if (!isHookWidget) { return; } @@ -229,17 +230,17 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { typedef _BlocData = ({ String baseName, - InterfaceElement blocElement, - InterfaceElement stateElement, - InterfaceElement? eventElement, - InterfaceElement? presentationEventElement, + InterfaceElement2 blocElement, + InterfaceElement2 stateElement, + InterfaceElement2? eventElement, + InterfaceElement2? presentationEventElement, }); _BlocData? _maybeBlocData(ClassDeclaration clazz) { - final blocElement = clazz.declaredElement; + final blocElement = clazz.declaredFragment?.element; if (blocElement == null || - !TypeCheckers.blocBase.isAssignableFrom(blocElement)) { + !TypeCheckers.blocBase.isAssignableFromType(blocElement.thisType)) { return null; } @@ -253,8 +254,8 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { return null; } - final stateElement = stateType.element; - if (stateElement is! InterfaceElement) { + final stateElement = stateType.element3; + if (stateElement is! InterfaceElement2) { return null; } @@ -262,8 +263,8 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { .firstWhereOrNull(TypeCheckers.bloc.isExactlyType) ?.typeArguments .firstOrNull - ?.element; - if (eventElement is! InterfaceElement?) { + ?.element3; + if (eventElement is! InterfaceElement2?) { return null; } @@ -271,8 +272,8 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { .firstWhereOrNull(TypeCheckers.blocPresentation.isExactlyType) ?.typeArguments .elementAtOrNull(1) - ?.element; - if (presentationEventElement is! InterfaceElement?) { + ?.element3; + if (presentationEventElement is! InterfaceElement2?) { return null; } @@ -285,23 +286,21 @@ _BlocData? _maybeBlocData(ClassDeclaration clazz) { ); } -bool inSameFile(Element element1, Element element2) { - final file1 = element1.source?.uri; - final file2 = element2.source?.uri; +bool inSameFile(Element2 element1, Element2 element2) { + final library1 = element1.library2?.uri; + final library2 = element2.library2?.uri; - return file1 != null && file2 != null && file1 == file2; + return library1 != null && library2 != null && library1 == library2; } -extension TypeSubclasses on InterfaceElement { - Iterable get subclasses { +extension TypeSubclasses on InterfaceElement2 { + Iterable get subclasses { final typeChecker = TypeChecker.fromStatic(thisType); - return library.units - .expand((u) => u.classes) - .where( - (clazz) => - typeChecker.isAssignableFrom(clazz) && - !typeChecker.isExactly(clazz), - ); + return library2.classes.where( + (clazz) => + typeChecker.isAssignableFromType(clazz.thisType) && + !typeChecker.isExactlyType(clazz.thisType), + ); } } diff --git a/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart b/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart index d7cb3622..646ed7f6 100644 --- a/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart +++ b/packages/leancode_lint/lib/lints/add_cubit_suffix_for_cubits.dart @@ -40,8 +40,9 @@ class AddCubitSuffixForYourCubits extends DartLintRule { bool _hasCubitSuffix(String className) => className.endsWith('Cubit'); - bool _isCubitClass(ClassDeclaration node) => switch (node.declaredElement) { - final element? => TypeCheckers.cubit.isSuperOf(element), - _ => false, - }; + bool _isCubitClass(ClassDeclaration node) => + switch (node.declaredFragment?.element.thisType) { + final type? => TypeCheckers.cubit.isSuperTypeOf(type), + _ => false, + }; } diff --git a/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart b/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart index 41ea8191..542a3a11 100644 --- a/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart +++ b/packages/leancode_lint/lib/lints/avoid_single_child_in_multi_child_widget.dart @@ -38,10 +38,10 @@ class AvoidSingleChildInMultiChildWidgets extends DartLintRule { ) { context.registry.addInstanceCreationExpression((node) { final constructorName = node.constructorName.type; - if (constructorName.element case final typeElement?) { + if (constructorName.type case final type?) { // is it something we want to complain about? final match = _complain.firstWhereOrNull( - (e) => e.$2.isExactly(typeElement), + (e) => e.$2.isExactlyType(type), ); if (match == null) { return; @@ -49,7 +49,7 @@ class AvoidSingleChildInMultiChildWidgets extends DartLintRule { // does it have a children argument? var children = node.argumentList.arguments.firstWhereOrNull( - (e) => e.staticParameterElement?.name == match.$1, + (e) => e.correspondingParameter?.displayName == match.$1, ); if (children == null) { return; diff --git a/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart b/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart index d73820b0..00499936 100644 --- a/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart +++ b/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart @@ -1,4 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -21,8 +21,8 @@ class BlocClassModifiers extends DartLintRule { CustomLintContext context, ) { context.registry.addBloc((node, data) { - void checkHierarchy(InterfaceElement? element) { - if (element is! ClassElement || + void checkHierarchy(InterfaceElement2? element) { + if (element is! ClassElement2 || !inSameFile(data.blocElement, element)) { return; } @@ -31,19 +31,19 @@ class BlocClassModifiers extends DartLintRule { if (subclasses.isNotEmpty) { if (!element.isSealed) { - reporter.atElement( + reporter.atElement2( element, code, - arguments: [element.name, 'sealed'], + arguments: [element.displayName, 'sealed'], data: 'sealed', ); } } else { if (!element.isFinal) { - reporter.atElement( + reporter.atElement2( element, code, - arguments: [element.name, 'final'], + arguments: [element.displayName, 'final'], data: 'final', ); } diff --git a/packages/leancode_lint/lib/lints/bloc_const_constructors.dart b/packages/leancode_lint/lib/lints/bloc_const_constructors.dart index ef2c283c..36e82376 100644 --- a/packages/leancode_lint/lib/lints/bloc_const_constructors.dart +++ b/packages/leancode_lint/lib/lints/bloc_const_constructors.dart @@ -31,14 +31,23 @@ class BlocConstConstructors extends DartLintRule { }; for (final element in elements) { - if (element.unnamedConstructor case final unnamedConstructor? + if (element.unnamedConstructor2 case final unnamedConstructor? when !unnamedConstructor.isConst && inSameFile(data.blocElement, element)) { - reporter.atElement( - unnamedConstructor.isSynthetic ? element : unnamedConstructor, - code, - arguments: [element.name], - ); + if (unnamedConstructor.isSynthetic) { + reporter.atElement2( + element, + code, + arguments: [element.displayName], + ); + } else { + reporter.atOffset( + offset: unnamedConstructor.firstFragment.typeNameOffset!, + length: unnamedConstructor.firstFragment.typeName!.length, + errorCode: code, + arguments: [element.displayName], + ); + } } } }); diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart index 6d94f50a..b52ef19f 100644 --- a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart @@ -1,4 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -25,8 +25,8 @@ class BlocRelatedClassesEquatable extends DartLintRule { CustomLintContext context, ) { context.registry.addBloc((node, data) { - void check(InterfaceElement? element) { - if (element is! ClassElement || + void check(InterfaceElement2? element) { + if (element is! ClassElement2 || !inSameFile(data.blocElement, element)) { return; } @@ -34,10 +34,12 @@ class BlocRelatedClassesEquatable extends DartLintRule { final isEquatableMixin = element.mixins.any( TypeCheckers.equatableMixin.isExactlyType, ); - final isEquatable = TypeCheckers.equatable.isAssignableFrom(element); + final isEquatable = TypeCheckers.equatable.isAssignableFromType( + element.thisType, + ); if (!isEquatableMixin && !isEquatable) { - reporter.atElement(element, code, arguments: [element.name]); + reporter.atElement2(element, code, arguments: [element.displayName]); } } @@ -60,13 +62,12 @@ class AddMixin extends DartFix { reporter .createChangeBuilder(message: 'Add EquatableMixin', priority: 1) .addDartFileEdit( - (builder) => - builder - ..importLibrary(Uri.parse('package:equatable/equatable.dart')) - ..addSimpleInsertion( - analysisError.offset + analysisError.length, - ' with EquatableMixin', - ), + (builder) => builder + ..importLibrary(Uri.parse('package:equatable/equatable.dart')) + ..addSimpleInsertion( + analysisError.offset + analysisError.length, + ' with EquatableMixin', + ), ); } } diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart index 834ee0f2..b96407b4 100644 --- a/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart @@ -1,4 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -21,12 +21,12 @@ class BlocRelatedClassNaming extends DartLintRule { CustomLintContext context, ) { context.registry.addBloc((node, data) { - void checkClass(InterfaceElement? element, String type, String suffix) { + void checkClass(InterfaceElement2? element, String type, String suffix) { final expectedName = '${data.baseName}$suffix'; if (element != null && - element.name != expectedName && + element.displayName != expectedName && inSameFile(data.blocElement, element)) { - reporter.atElement( + reporter.atElement2( element, code, arguments: [node.name.lexeme, type, expectedName], diff --git a/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart b/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart index 45ed017b..60d0f5ed 100644 --- a/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart +++ b/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart @@ -1,4 +1,4 @@ -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/element2.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -21,14 +21,14 @@ class BlocSubclassesNaming extends DartLintRule { CustomLintContext context, ) { context.registry.addBloc((node, data) { - void check(InterfaceElement? element, String type) { + void check(InterfaceElement2? element, String type) { if (element != null && inSameFile(data.blocElement, element)) { for (final subtype in element.subclasses) { - if (!subtype.name.startsWith(element.name)) { - reporter.atElement( + if (!subtype.displayName.startsWith(element.displayName)) { + reporter.atElement2( subtype, code, - arguments: [node.name.lexeme, type, element.name], + arguments: [node.name.lexeme, type, element.displayName], ); } } diff --git a/packages/leancode_lint/lib/lints/constructor_parameters_and_fields_should_have_the_same_order.dart b/packages/leancode_lint/lib/lints/constructor_parameters_and_fields_should_have_the_same_order.dart index 9f67846f..da5c964d 100644 --- a/packages/leancode_lint/lib/lints/constructor_parameters_and_fields_should_have_the_same_order.dart +++ b/packages/leancode_lint/lib/lints/constructor_parameters_and_fields_should_have_the_same_order.dart @@ -108,7 +108,7 @@ class ConstructorParametersAndFieldsShouldHaveTheSameOrder } bool _isNotSuperFormal(FormalParameter parameter) => - !(parameter.declaredElement?.isSuperFormal ?? false); + !(parameter.declaredFragment?.element.isSuperFormal ?? false); bool _compareEffectiveNames( FieldDeclaration field, diff --git a/packages/leancode_lint/lib/lints/use_instead_type.dart b/packages/leancode_lint/lib/lints/use_instead_type.dart index 2c532c82..444b45a8 100644 --- a/packages/leancode_lint/lib/lints/use_instead_type.dart +++ b/packages/leancode_lint/lib/lints/use_instead_type.dart @@ -1,5 +1,5 @@ import 'package:analyzer/dart/ast/ast.dart'; -import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/type.dart'; import 'package:analyzer/error/error.dart' hide LintCode; import 'package:analyzer/error/listener.dart'; import 'package:custom_lint_builder/custom_lint_builder.dart'; @@ -52,29 +52,29 @@ abstract base class UseInsteadType extends DartLintRule { CustomLintContext context, ) { context.registry.addIdentifier((node) { - if (node.staticElement case final element?) { - _handleElement(reporter, element, node); + if (node.staticType case final type?) { + _handleElement(reporter, type, node); } }); context.registry.addNamedType((node) { - if (node.element case final element?) { - _handleElement(reporter, element, node); + if (node.type case final type?) { + _handleElement(reporter, type, node); } }); } - void _handleElement(ErrorReporter reporter, Element element, AstNode node) { + void _handleElement(ErrorReporter reporter, DartType type, AstNode node) { if (_isInHide(node)) { return; } for (final (preferredItemName, checker) in _checkers) { try { - if (checker.isExactly(element)) { + if (checker.isExactlyType(type)) { reporter.atNode( node, code, - arguments: [element.displayName, preferredItemName], + arguments: [type.getDisplayString(), preferredItemName], ); } } catch (err) { From c627c590450f2469bda2490f282d520b65b364c8 Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Wed, 13 Aug 2025 11:04:16 +0200 Subject: [PATCH 19/20] Add class modifier test for classes without subclasses --- .../lib/bloc_class_modifiers_test.dart | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart index 8b1d1ba2..5ad0bee5 100644 --- a/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart @@ -91,3 +91,62 @@ final class Test4StateInitial extends Test4State {} sealed class Test4Event {} final class Test4EventFoo extends Test4Event {} + +/////////////////////////////////////////////////////////////////////// + +// Bloc's state, event, and presentation event classes not final without descendants are flagged +class Test5Bloc extends Bloc + with BlocPresentationMixin { + Test5Bloc() : super(const Test5State(1)); +} + +class +// expect_lint: bloc_class_modifiers +Test5State { + const Test5State(this.x); + + final int x; +} + +class +// expect_lint: bloc_class_modifiers +Test5Event { + const Test5Event(this.value); + + final int value; +} + +class +// expect_lint: bloc_class_modifiers +Test5PresentationEvent { + const Test5PresentationEvent(this.value); + + final String value; +} + +/////////////////////////////////////////////////////////////////////// + +// Bloc's state, event, and presentation event classes that are final without descendants are not flagged +class Test6Bloc extends Bloc + with BlocPresentationMixin { + Test6Bloc() : super(const Test6State(1, 2)); +} + +final class Test6State { + const Test6State(this.x, this.y); + + final int x; + final int y; +} + +final class Test6Event { + const Test6Event(this.value); + + final int value; +} + +final class Test6PresentationEvent { + const Test6PresentationEvent(this.value); + + final String value; +} From 3db10b981a23c8fc8b8c7d961de2ee44a37a7bc6 Mon Sep 17 00:00:00 2001 From: Piotr Rogulski Date: Wed, 13 Aug 2025 12:10:47 +0200 Subject: [PATCH 20/20] Make fix classes private --- .../lib/lints/bloc_related_classes_equatable.dart | 4 ++-- packages/leancode_lint/lib/lints/use_equatable_mixin.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart index b52ef19f..713b5c63 100644 --- a/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart @@ -16,7 +16,7 @@ class BlocRelatedClassesEquatable extends DartLintRule { ); @override - List getFixes() => [AddMixin()]; + List getFixes() => [_AddMixin()]; @override void run( @@ -50,7 +50,7 @@ class BlocRelatedClassesEquatable extends DartLintRule { } } -class AddMixin extends DartFix { +class _AddMixin extends DartFix { @override void run( CustomLintResolver resolver, diff --git a/packages/leancode_lint/lib/lints/use_equatable_mixin.dart b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart index b2ad202d..9444f346 100644 --- a/packages/leancode_lint/lib/lints/use_equatable_mixin.dart +++ b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart @@ -16,7 +16,7 @@ class UseEquatableMixin extends DartLintRule { ); @override - List getFixes() => [ConvertToMixin()]; + List getFixes() => [_ConvertToMixin()]; @override void run( @@ -53,7 +53,7 @@ class UseEquatableMixin extends DartLintRule { } } -class ConvertToMixin extends DartFix { +class _ConvertToMixin extends DartFix { @override void run( CustomLintResolver resolver,