diff --git a/packages/leancode_lint/README.md b/packages/leancode_lint/README.md index 5abb6522..6dd9685a 100644 --- a/packages/leancode_lint/README.md +++ b/packages/leancode_lint/README.md @@ -413,6 +413,203 @@ class MyWidget extends StatelessWidget { 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 +.. 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 7ca4026a..d4a3a9a4 100644 --- a/packages/leancode_lint/lib/helpers.dart +++ b/packages/leancode_lint/lib/helpers.dart @@ -1,9 +1,11 @@ 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/element2.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; -import 'package:analyzer/error/error.dart' as error; +import 'package:analyzer/error/error.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( @@ -71,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([ - TypeChecker.fromName('HookBuilder', packageName: 'flutter_hooks'), - TypeChecker.fromName('HookConsumer', packageName: 'hooks_riverpod'), - ]).isExactly(classElement); + TypeCheckers.hookBuilder, + TypeCheckers.hookConsumer, + ]).isExactlyType(type); if (!isHookBuilder) { return null; } @@ -125,14 +127,15 @@ 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'), - ]).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() @@ -176,34 +179,31 @@ extension LintRuleNodeRegistryExtensions on LintRuleNodeRegistry { } }); addClassDeclaration((node) { - final element = node.declaredElement; - if (element == null) { + final thisType = node.declaredFragment?.element.thisType; + if (thisType == null) { return; } const checker = TypeChecker.any([ - TypeChecker.fromName('HookWidget', packageName: 'flutter_hooks'), - TypeChecker.fromName( - 'HookConsumerWidget', - packageName: 'hooks_riverpod', - ), + TypeCheckers.hookWidget, + TypeCheckers.hookConsumerWidget, ]); 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; } @@ -218,6 +218,90 @@ 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); + } + }); + } +} + +typedef _BlocData = ({ + String baseName, + InterfaceElement2 blocElement, + InterfaceElement2 stateElement, + InterfaceElement2? eventElement, + InterfaceElement2? presentationEventElement, +}); + +_BlocData? _maybeBlocData(ClassDeclaration clazz) { + final blocElement = clazz.declaredFragment?.element; + + if (blocElement == null || + !TypeCheckers.blocBase.isAssignableFromType(blocElement.thisType)) { + return null; + } + + final baseName = clazz.name.lexeme.replaceAll(RegExp(r'(Cubit|Bloc)$'), ''); + + final stateType = blocElement.allSupertypes + .firstWhere(TypeCheckers.blocBase.isExactlyType) + .typeArguments + .singleOrNull; + if (stateType == null) { + return null; + } + + final stateElement = stateType.element3; + if (stateElement is! InterfaceElement2) { + return null; + } + + final eventElement = blocElement.allSupertypes + .firstWhereOrNull(TypeCheckers.bloc.isExactlyType) + ?.typeArguments + .firstOrNull + ?.element3; + if (eventElement is! InterfaceElement2?) { + return null; + } + + final presentationEventElement = blocElement.mixins + .firstWhereOrNull(TypeCheckers.blocPresentation.isExactlyType) + ?.typeArguments + .elementAtOrNull(1) + ?.element3; + if (presentationEventElement is! InterfaceElement2?) { + return null; + } + + return ( + baseName: baseName, + blocElement: blocElement, + stateElement: stateElement, + eventElement: eventElement, + presentationEventElement: presentationEventElement, + ); +} + +bool inSameFile(Element2 element1, Element2 element2) { + final library1 = element1.library2?.uri; + final library2 = element2.library2?.uri; + + return library1 != null && library2 != null && library1 == library2; +} + +extension TypeSubclasses on InterfaceElement2 { + Iterable get subclasses { + final typeChecker = TypeChecker.fromStatic(thisType); + return library2.classes.where( + (clazz) => + typeChecker.isAssignableFromType(clazz.thisType) && + !typeChecker.isExactlyType(clazz.thisType), + ); + } } bool isExpressionExactlyType( @@ -300,8 +384,8 @@ class ChangeWidgetNameFix extends DartFix { CustomLintResolver resolver, ChangeReporter reporter, CustomLintContext context, - error.AnalysisError analysisError, - List errors, + AnalysisError analysisError, + List errors, ) { reporter .createChangeBuilder(message: 'Replace with $widgetName', priority: 1) diff --git a/packages/leancode_lint/lib/leancode_lint.dart b/packages/leancode_lint/lib/leancode_lint.dart index c55ba4eb..7b841112 100644 --- a/packages/leancode_lint/lib/leancode_lint.dart +++ b/packages/leancode_lint/lib/leancode_lint.dart @@ -5,6 +5,11 @@ 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_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'; 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'; @@ -12,6 +17,7 @@ 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_align.dart'; import 'package:leancode_lint/lints/use_design_system_item.dart'; +import 'package:leancode_lint/lints/use_equatable_mixin.dart'; import 'package:leancode_lint/lints/use_padding.dart'; PluginBase createPlugin() => _Linter(); @@ -30,6 +36,12 @@ class _Linter extends PluginBase { const AvoidSingleChildInMultiChildWidgets(), const UseAlign(), const UsePadding(), + const BlocRelatedClassNaming(), + const UseEquatableMixin(), + const BlocSubclassesNaming(), + const BlocClassModifiers(), + const BlocRelatedClassesEquatable(), + const BlocConstConstructors(), ]; @override 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 44ac12c1..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 @@ -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. @@ -39,11 +40,9 @@ 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), - _ => 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 bee624cc..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 @@ -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. @@ -18,27 +19,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 @@ -49,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; @@ -60,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 new file mode 100644 index 00000000..00499936 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_class_modifiers.dart @@ -0,0 +1,60 @@ +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'; +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(InterfaceElement2? element) { + if (element is! ClassElement2 || + !inSameFile(data.blocElement, element)) { + return; + } + + final subclasses = element.subclasses; + + if (subclasses.isNotEmpty) { + if (!element.isSealed) { + reporter.atElement2( + element, + code, + arguments: [element.displayName, 'sealed'], + data: 'sealed', + ); + } + } else { + if (!element.isFinal) { + reporter.atElement2( + element, + code, + arguments: [element.displayName, 'final'], + data: 'final', + ); + } + } + + subclasses.forEach(checkHierarchy); + } + + checkHierarchy(data.stateElement); + checkHierarchy(data.eventElement); + checkHierarchy(data.presentationEventElement); + }); + } +} 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..36e82376 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_const_constructors.dart @@ -0,0 +1,55 @@ +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.unnamedConstructor2 case final unnamedConstructor? + when !unnamedConstructor.isConst && + inSameFile(data.blocElement, element)) { + 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 new file mode 100644 index 00000000..713b5c63 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_equatable.dart @@ -0,0 +1,73 @@ +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'; +import 'package:leancode_lint/common_type_checkers.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()]; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addBloc((node, data) { + void check(InterfaceElement2? element) { + if (element is! ClassElement2 || + !inSameFile(data.blocElement, element)) { + return; + } + + final isEquatableMixin = element.mixins.any( + TypeCheckers.equatableMixin.isExactlyType, + ); + final isEquatable = TypeCheckers.equatable.isAssignableFromType( + element.thisType, + ); + + if (!isEquatableMixin && !isEquatable) { + reporter.atElement2(element, code, arguments: [element.displayName]); + } + } + + 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/lib/lints/bloc_related_classes_naming.dart b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart new file mode 100644 index 00000000..b96407b4 --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_related_classes_naming.dart @@ -0,0 +1,46 @@ +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'; +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(InterfaceElement2? element, String type, String suffix) { + final expectedName = '${data.baseName}$suffix'; + if (element != null && + element.displayName != expectedName && + inSameFile(data.blocElement, element)) { + reporter.atElement2( + element, + code, + arguments: [node.name.lexeme, type, expectedName], + ); + } + } + + checkClass(data.stateElement, 'state', 'State'); + checkClass(data.eventElement, 'event', 'Event'); + checkClass( + data.presentationEventElement, + 'presentation event', + data.eventElement == null ? 'Event' : 'PresentationEvent', + ); + }); + } +} 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..60d0f5ed --- /dev/null +++ b/packages/leancode_lint/lib/lints/bloc_subclasses_naming.dart @@ -0,0 +1,43 @@ +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'; +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(InterfaceElement2? element, String type) { + if (element != null && inSameFile(data.blocElement, element)) { + for (final subtype in element.subclasses) { + if (!subtype.displayName.startsWith(element.displayName)) { + reporter.atElement2( + subtype, + code, + arguments: [node.name.lexeme, type, element.displayName], + ); + } + } + } + } + + check(data.stateElement, 'state'); + check(data.eventElement, 'event'); + check(data.presentationEventElement, 'presentation event'); + }); + } +} 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_equatable_mixin.dart b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart new file mode 100644 index 00000000..9444f346 --- /dev/null +++ b/packages/leancode_lint/lib/lints/use_equatable_mixin.dart @@ -0,0 +1,74 @@ +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() + : super( + code: const LintCode( + name: 'prefer_equatable_mixin', + problemMessage: + 'The class {0} should mix in EquatableMixin instead of extending Equatable.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @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', + ), + ); + } +} 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) { 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..5ad0bee5 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_class_modifiers_test.dart @@ -0,0 +1,152 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors + +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 {} + +/////////////////////////////////////////////////////////////////////// + +// 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; +} 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(); +} 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..7095cf8a --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_related_class_naming_test.dart @@ -0,0 +1,100 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors + +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'; + +// Invalid state name is flagged +class Test1Cubit extends Cubit { + Test1Cubit() : super(WrongStateName()); +} + +// expect_lint: bloc_related_class_naming +final class WrongStateName {} + +////////////////////////////////////////////////////////////////////////// + +// Invalid bloc event name is flagged +class Test2Bloc extends Bloc { + Test2Bloc() : super(Test2State()); +} + +// expect_lint: bloc_related_class_naming +final class WrongEventName {} + +final class Test2State {} + +////////////////////////////////////////////////////////////////////////// + +// Invalid presentation event name is flagged +class Test3Bloc extends Bloc + with BlocPresentationMixin { + Test3Bloc() : super(Test3State()); +} + +final class Test3Event {} + +final class Test3State {} + +// expect_lint: bloc_related_class_naming +final class WrongPresentationEventName {} + +////////////////////////////////////////////////////////////////////////// + +// Invalid names of enums are flagged +class Test4Bloc extends Bloc + with BlocPresentationMixin { + Test4Bloc() : 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 } + +/////////////////////////////////////////////////////////////////////// + +// No lint when names are correct +class Correct1Cubit extends Cubit + with BlocPresentationMixin { + Correct1Cubit() : super(Correct1State()); +} + +final class Correct1State {} + +final class Correct1Event {} + +////////////////////////////////////////////////////////////////////////// + +// No lint when names are correct, with event disambiguation +class Correct2Bloc extends Bloc + with BlocPresentationMixin { + Correct2Bloc() : super(Correct2State()); +} + +final class Correct2Event {} + +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 when class is from another package +class AnotherPackageBloc extends Bloc + with BlocPresentationMixin { + AnotherPackageBloc() : super(0); +} 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/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..5db7f5d1 --- /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, bloc_const_constructors + +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 } 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..6f4de360 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/bloc_subclasses_naming_test.dart @@ -0,0 +1,97 @@ +// for tests +// ignore_for_file: bloc_related_classes_equatable, bloc_const_constructors + +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 {} 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..fdb99ae8 --- /dev/null +++ b/packages/leancode_lint/test/lints_test_app/lib/prefer_equatable_mixin_test.dart @@ -0,0 +1,26 @@ +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 104b2efc..74740f57 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: @@ -153,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 d4e96e26..732ef6c0 100644 --- a/packages/leancode_lint/test/lints_test_app/pubspec.yaml +++ b/packages/leancode_lint/test/lints_test_app/pubspec.yaml @@ -9,6 +9,8 @@ environment: sdk: '>=3.7.0 <4.0.0' dependencies: + bloc_presentation: ^1.1.0 + equatable: ^2.0.7 flutter: sdk: flutter flutter_bloc: ^9.1.1