diff --git a/.gitignore b/.gitignore index 45f567845b9..a8d4eeba80d 100644 --- a/.gitignore +++ b/.gitignore @@ -110,4 +110,7 @@ mason-lock.json !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock \ No newline at end of file +!/dev/ci/**/Gemfile.lock + +# Custom_lint related. +custom_lint.log \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 1a4bf97ef81..b5ba73882cc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -7,6 +7,9 @@ analyzer: strict-inference: true strict-raw-types: true + plugins: + - custom_lint + errors: close_sinks: ignore missing_required_param: error diff --git a/packages/bloc_lint/.gitignore b/packages/bloc_lint/.gitignore new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/bloc_lint/CHANGELOG.md b/packages/bloc_lint/CHANGELOG.md new file mode 100644 index 00000000000..d76fd5ec1b0 --- /dev/null +++ b/packages/bloc_lint/CHANGELOG.md @@ -0,0 +1,11 @@ +## 0.1.0 - 2024-11-10 + +- Initial release +- New rules implemented: + - avoid_public_methods_on_bloc + - avoid_public_properties_on_bloc_and_cubit + - event_base_class_suffix + - prefer_multi_bloc_listener + - prefer_multi_bloc_provider + - prefer_multi_repository_provider + - state_base_class_suffix \ No newline at end of file diff --git a/packages/bloc_lint/LICENSE b/packages/bloc_lint/LICENSE new file mode 100644 index 00000000000..9951aa6771d --- /dev/null +++ b/packages/bloc_lint/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Valentin Michalak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/bloc_lint/README.md b/packages/bloc_lint/README.md new file mode 100644 index 00000000000..111a69cfefc --- /dev/null +++ b/packages/bloc_lint/README.md @@ -0,0 +1,40 @@ +Bloc Lint +=== + +Custom linter rules for Flutter projects using the [bloc library](https://bloclibrary.dev/). This package is based on +[dart_custom_lint](https://github.com/invertase/dart_custom_lint) package. + +## Usage + +Add the following to your `pubspec.yaml` file: + +```yaml +dev_dependencies: + custom_lint: ^0.7.0 + bloc_lint: ^0.1.0 +``` + +Add the following to your `analysis_options.yaml` file: + +```yaml +analyzer: + plugins: + - custom_lint +``` + +That's it! After running pub get (and possibly restarting their IDE), users should now see our custom lints in their +Dart files. You can also run `dart pub custom_lint` to run the linter in your CLI. + +## Tests + +The example folder contains a dart project to unit test the rules. (see custom_lint readme for more info) + +## Implemented Rules + +- [avoid_public_methods_on_bloc](doc/rules/avoid_public_methods_on_bloc.md) +- [avoid_public_properties_on_bloc_and_cubit](doc/rules/avoid_public_properties_on_bloc_and_cubit.md) +- [event_base_class_suffix](doc/rules/event_base_class_suffix.md) +- [prefer_multi_bloc_listener](doc/rules/prefer_multi_bloc_listener.md) +- [prefer_multi_bloc_provider](doc/rules/prefer_multi_bloc_provider.md) +- [prefer_multi_repository_provider](doc/rules/prefer_multi_repository_provider.md) +- [state_base_class_suffix](doc/rules/state_base_class_suffix.md) \ No newline at end of file diff --git a/packages/bloc_lint/doc/rules/avoid_public_methods_on_bloc.md b/packages/bloc_lint/doc/rules/avoid_public_methods_on_bloc.md new file mode 100644 index 00000000000..46f35d13c4f --- /dev/null +++ b/packages/bloc_lint/doc/rules/avoid_public_methods_on_bloc.md @@ -0,0 +1,27 @@ +avoid_public_method_on_bloc +=== +severity: WARNING + +Avoid public methods on `Bloc` classes. + +## Example: + +❌ **BAD**: + +```dart +class MyBloc extends Bloc { + MyBloc(super.initialState); + + void test() {} +} +``` + +✅ **GOOD**: + +```dart +class MyBloc extends Bloc { + MyBloc(super.initialState); + + void _test() {} +} +``` \ No newline at end of file diff --git a/packages/bloc_lint/doc/rules/avoid_public_properties_on_bloc_and_cubit.md b/packages/bloc_lint/doc/rules/avoid_public_properties_on_bloc_and_cubit.md new file mode 100644 index 00000000000..0e7068ba73d --- /dev/null +++ b/packages/bloc_lint/doc/rules/avoid_public_properties_on_bloc_and_cubit.md @@ -0,0 +1,38 @@ +avoid_public_properties_on_bloc_and_cubit +=== +severity: WARNING + +Avoid public properties on `Bloc` and `Cubit`, prefer emit state or use private value. + +## Example: + +❌ **BAD**: + +```dart +class MyCubit extends Cubit { + MyCubit() : super(0); + + int value = 1; +} +``` + +✅ **GOOD**: +```dart +class MyCubit extends Cubit { + MyCubit() : super(0); + + void init() { + emit(1); + } +} +``` + +or + +```dart +class MyCubit extends Cubit { + MyCubit() : super(0); + + int _value = 1; +} +``` diff --git a/packages/bloc_lint/doc/rules/event_base_class_suffix.md b/packages/bloc_lint/doc/rules/event_base_class_suffix.md new file mode 100644 index 00000000000..7f5c83e4bc2 --- /dev/null +++ b/packages/bloc_lint/doc/rules/event_base_class_suffix.md @@ -0,0 +1,29 @@ +event_base_class_suffix +=== +severity: WARNING + +The base event class should always be suffixed by `Event`. + +## Example: + +❌ **BAD**: + +```dart +@immutable +sealed class CounterData {} + +final class CounterStarted extends CounterData {} +``` + +✅ **GOOD**: + +```dart +@immutable +sealed class CounterEvent {} + +final class CounterStarted extends CounterEvent {} +``` + +## Additional Resources + +- [Bloc Library Documentation: Naming Conventions / Event Conventions](https://bloclibrary.dev/naming-conventions/#event-conventions) diff --git a/packages/bloc_lint/doc/rules/prefer_multi_bloc_listener.md b/packages/bloc_lint/doc/rules/prefer_multi_bloc_listener.md new file mode 100644 index 00000000000..00e1052794f --- /dev/null +++ b/packages/bloc_lint/doc/rules/prefer_multi_bloc_listener.md @@ -0,0 +1,38 @@ +prefer-multi-bloc-listener +=== +severity: WARNING + +Warns when a `BlocListener` can be replaced by a `MultiBlocListener`. + +## Example: + +❌ **BAD**: + +```dart +BlocListener( + listener: (BuildContext context, state) {}, + child: BlocListener( + listener: (BuildContext context, state) {}, + ), +); +``` + +✅ **GOOD**: + +```dart +MultiBlocListener( + listeners: [ + BlocListener( + listener: (BuildContext context, state) {}, + ), + BlocListener( + listener: (BuildContext context, state) {}, + ), + ], + child: Container(), +); +``` + +## Additional Resources + +- [Bloc Library Documentation: Flutter Bloc Concepts / MultiBlocListener](https://bloclibrary.dev/flutter-bloc-concepts/#multibloclistener) \ No newline at end of file diff --git a/packages/bloc_lint/doc/rules/prefer_multi_bloc_provider.md b/packages/bloc_lint/doc/rules/prefer_multi_bloc_provider.md new file mode 100644 index 00000000000..2c8ccf15703 --- /dev/null +++ b/packages/bloc_lint/doc/rules/prefer_multi_bloc_provider.md @@ -0,0 +1,35 @@ +prefer-multi-bloc-provider +=== +severity: WARNING + +Warns when a `BlocProvider` can be replaced by a `MultiBlocProvider`. + +## Example: + +❌ **BAD**: + +```dart +BlocProvider( + create: (context) => BlocA(), + child: BlocProvider( + create: (context) => BlocB(), + child: Widget(), + ), +); +``` + +✅ **GOOD**: + +```dart +MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => BlocA()), + BlocProvider(create: (context) => BlocB()), + ], + child: Widget(), +); +``` + +## Additional Resources + +- [Bloc Library Documentation: Flutter Bloc Concepts / MultiBlocProvider](https://bloclibrary.dev/flutter-bloc-concepts/#multiblocprovider) \ No newline at end of file diff --git a/packages/bloc_lint/doc/rules/prefer_multi_repository_provider.md b/packages/bloc_lint/doc/rules/prefer_multi_repository_provider.md new file mode 100644 index 00000000000..4f25455b2ea --- /dev/null +++ b/packages/bloc_lint/doc/rules/prefer_multi_repository_provider.md @@ -0,0 +1,34 @@ +prefer-multi-repository-provider +=== +severity: WARNING + +Warns when a `RepositoryProvider` can be replaced by a `MultiRepositoryProvider`. + +## Example: + +❌ **BAD**: + +```dart +RepositoryProvider( + create: (_) => RepositoryA(), + child: RepositoryProvider( + create: (_) => RepositoryB(), + ), +); +``` + +✅ **GOOD**: + +```dart +MultiRepositoryProvider( + providers: [ + RepositoryProvider(create: (_) => RepositoryA()), + RepositoryProvider(create: (_) => RepositoryB()), + ], + child: Container(), +); +``` + +## Additional Resources + +- [Bloc Library Documentation: Flutter Bloc Concepts / MultiRepositoryProvider](https://bloclibrary.dev/flutter-bloc-concepts/#multirepositoryprovider) \ No newline at end of file diff --git a/packages/bloc_lint/doc/rules/state_base_class_suffix.md b/packages/bloc_lint/doc/rules/state_base_class_suffix.md new file mode 100644 index 00000000000..eabeb9c2fe6 --- /dev/null +++ b/packages/bloc_lint/doc/rules/state_base_class_suffix.md @@ -0,0 +1,28 @@ +state_base_class_suffix +=== +severity: WARNING + +The base state class should always be suffixed by `State`. + +## Example: + +❌ **BAD**: + +```dart +@immutable +sealed class HomepageData {} + +final class HomepageInitial extends HomepageData {} +``` + +✅ **GOOD**: + +```dart +sealed class HomepageState {} + +final class HomepageInitial extends HomepageState {} +``` + +## Additional Resources + +- [Bloc Library Documentation: Naming Conventions / State Conventions](https://bloclibrary.dev/naming-conventions/#state-conventions) diff --git a/packages/bloc_lint/example/analysis_option.yaml b/packages/bloc_lint/example/analysis_option.yaml new file mode 100644 index 00000000000..5ca8227fe83 --- /dev/null +++ b/packages/bloc_lint/example/analysis_option.yaml @@ -0,0 +1,3 @@ +analyzer: + plugins: + - custom_lint \ No newline at end of file diff --git a/packages/bloc_lint/example/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule_test.dart b/packages/bloc_lint/example/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule_test.dart new file mode 100644 index 00000000000..5f117e52471 --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule_test.dart @@ -0,0 +1,10 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AvoidPublicMethodOnBlocBloc extends Bloc { + AvoidPublicMethodOnBlocBloc(super.initialState); + + // expect_lint: avoid_public_methods_on_bloc + void publicMethod() {} + + void _privateMethod() {} +} diff --git a/packages/bloc_lint/example/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule_test.dart b/packages/bloc_lint/example/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule_test.dart new file mode 100644 index 00000000000..4576496bdad --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class TestPublicPropertiesCubit extends Cubit { + TestPublicPropertiesCubit() : super(0); + + // expect_lint: avoid_public_properties_on_bloc_and_cubit + int value = 0; + String _privateValue = 'abc'; +} + +class TestPublicPropertiesBloc extends Bloc { + TestPublicPropertiesBloc() : super(0); + + // expect_lint: avoid_public_properties_on_bloc_and_cubit + int value = 0; + String _privateValue = 'abc'; +} diff --git a/packages/bloc_lint/example/lib/lint_rules/event_base_class_suffix_lint_rule_test_bloc.dart b/packages/bloc_lint/example/lib/lint_rules/event_base_class_suffix_lint_rule_test_bloc.dart new file mode 100644 index 00000000000..66e142b0d92 --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/event_base_class_suffix_lint_rule_test_bloc.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +class EventBaseClassSuffixLintRuleTestBloc extends Bloc< +// expect_lint: event_base_class_suffix + EventBaseClassSuffixLintRuleTestError, + EventBaseClassSuffixLintRuleTestState> { + EventBaseClassSuffixLintRuleTestBloc() + : super(EventBaseClassSuffixLintRuleTestInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} + +@immutable +sealed class EventBaseClassSuffixLintRuleTestError {} + +@immutable +sealed class EventBaseClassSuffixLintRuleTestState {} + +final class EventBaseClassSuffixLintRuleTestInitial + extends EventBaseClassSuffixLintRuleTestState {} diff --git a/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_listener_lint_rule_test.dart b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_listener_lint_rule_test.dart new file mode 100644 index 00000000000..234502c13d7 --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_listener_lint_rule_test.dart @@ -0,0 +1,26 @@ +import 'package:bloc_lint_example/sample_cubit/cubit_a_cubit.dart'; +import 'package:bloc_lint_example/sample_cubit/cubit_b_cubit.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +void main() { + final expectPreferMultiBlocListener = BlocListener( + listener: (BuildContext context, state) {}, + // expect_lint: prefer_multi_bloc_listener + child: BlocListener( + listener: (BuildContext context, state) {}, + ), + ); + + final dontExpectPreferMultiBlocListener = MultiBlocListener( + listeners: [ + BlocListener( + listener: (BuildContext context, state) {}, + ), + BlocListener( + listener: (BuildContext context, state) {}, + ), + ], + child: Container(), + ); +} diff --git a/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_provider_lint_rule_test.dart b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_provider_lint_rule_test.dart new file mode 100644 index 00000000000..7b2150ce618 --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_bloc_provider_lint_rule_test.dart @@ -0,0 +1,22 @@ +import 'package:bloc_lint_example/sample_cubit/cubit_a_cubit.dart'; +import 'package:bloc_lint_example/sample_cubit/cubit_b_cubit.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +void main() { + final expectPreferMultiBlocProvider = BlocProvider( + create: (_) => ACubit(), + // expect_lint: prefer_multi_bloc_provider + child: BlocProvider( + create: (_) => BCubit(), + ), + ); + + final dontExpectPreferMultiBlocProvider = MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => ACubit()), + BlocProvider(create: (_) => BCubit()), + ], + child: Container(), + ); +} diff --git a/packages/bloc_lint/example/lib/lint_rules/prefer_multi_repository_provider_lint_rule_test.dart b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_repository_provider_lint_rule_test.dart new file mode 100644 index 00000000000..a5084b961a7 --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/prefer_multi_repository_provider_lint_rule_test.dart @@ -0,0 +1,22 @@ +import 'package:bloc_lint_example/sample_repository/repository_a.dart'; +import 'package:bloc_lint_example/sample_repository/repository_b.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +void main() { + final expectPreferMultiRepositoryProvider = RepositoryProvider( + create: (_) => RepositoryA(), + // expect_lint: prefer_multi_repository_provider + child: RepositoryProvider( + create: (_) => RepositoryB(), + ), + ); + + final dontExpectPreferMultiBlocProvider = MultiRepositoryProvider( + providers: [ + RepositoryProvider(create: (_) => RepositoryA()), + RepositoryProvider(create: (_) => RepositoryB()), + ], + child: Container(), + ); +} diff --git a/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_bloc.dart b/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_bloc.dart new file mode 100644 index 00000000000..40ae597dc6e --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_bloc.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +class StateBaseClassSuffixLintRuleTestBloc extends Bloc< + StateBaseClassSuffixLintRuleTestEvent, +// expect_lint: state_base_class_suffix + StateBaseClassSuffixLintRuleTestError> { + StateBaseClassSuffixLintRuleTestBloc() + : super(StateBaseClassSuffixLintRuleTestInitial()) { + on((event, emit) { + // TODO: implement event handler + }); + } +} + +@immutable +sealed class StateBaseClassSuffixLintRuleTestEvent {} + +@immutable +sealed class StateBaseClassSuffixLintRuleTestError {} + +final class StateBaseClassSuffixLintRuleTestInitial + extends StateBaseClassSuffixLintRuleTestError {} diff --git a/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_cubit.dart b/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_cubit.dart new file mode 100644 index 00000000000..7d599a9371b --- /dev/null +++ b/packages/bloc_lint/example/lib/lint_rules/state_base_class_suffix_lint_rule_test_cubit.dart @@ -0,0 +1,14 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +class StateBaseClassSuffixLintRuleTestCubit // expect_lint: state_base_class_suffix + extends Cubit { + StateBaseClassSuffixLintRuleTestCubit() + : super(StateBaseClassSuffixLintRuleTestInitial()); +} + +@immutable +sealed class StateBaseClassSuffixLintRuleTest {} + +final class StateBaseClassSuffixLintRuleTestInitial + extends StateBaseClassSuffixLintRuleTest {} diff --git a/packages/bloc_lint/example/lib/sample_cubit/cubit_a_cubit.dart b/packages/bloc_lint/example/lib/sample_cubit/cubit_a_cubit.dart new file mode 100644 index 00000000000..52de6451285 --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_cubit/cubit_a_cubit.dart @@ -0,0 +1,8 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'cubit_a_state.dart'; + +class ACubit extends Cubit { + ACubit() : super(AInitial()); +} diff --git a/packages/bloc_lint/example/lib/sample_cubit/cubit_a_state.dart b/packages/bloc_lint/example/lib/sample_cubit/cubit_a_state.dart new file mode 100644 index 00000000000..1b36558b18a --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_cubit/cubit_a_state.dart @@ -0,0 +1,6 @@ +part of 'cubit_a_cubit.dart'; + +@immutable +sealed class AState {} + +final class AInitial extends AState {} diff --git a/packages/bloc_lint/example/lib/sample_cubit/cubit_b_cubit.dart b/packages/bloc_lint/example/lib/sample_cubit/cubit_b_cubit.dart new file mode 100644 index 00000000000..1ec1b77b625 --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_cubit/cubit_b_cubit.dart @@ -0,0 +1,8 @@ +import 'package:bloc/bloc.dart'; +import 'package:meta/meta.dart'; + +part 'cubit_b_state.dart'; + +class BCubit extends Cubit { + BCubit() : super(BInitial()); +} diff --git a/packages/bloc_lint/example/lib/sample_cubit/cubit_b_state.dart b/packages/bloc_lint/example/lib/sample_cubit/cubit_b_state.dart new file mode 100644 index 00000000000..256118c676d --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_cubit/cubit_b_state.dart @@ -0,0 +1,6 @@ +part of 'cubit_b_cubit.dart'; + +@immutable +sealed class BState {} + +final class BInitial extends BState {} diff --git a/packages/bloc_lint/example/lib/sample_repository/repository_a.dart b/packages/bloc_lint/example/lib/sample_repository/repository_a.dart new file mode 100644 index 00000000000..0df0d54993c --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_repository/repository_a.dart @@ -0,0 +1,3 @@ +class RepositoryA { + final String name = 'RepositoryA'; +} \ No newline at end of file diff --git a/packages/bloc_lint/example/lib/sample_repository/repository_b.dart b/packages/bloc_lint/example/lib/sample_repository/repository_b.dart new file mode 100644 index 00000000000..95fb5fdf0d4 --- /dev/null +++ b/packages/bloc_lint/example/lib/sample_repository/repository_b.dart @@ -0,0 +1,3 @@ +class RepositoryB { + final String name = 'RepositoryB'; +} diff --git a/packages/bloc_lint/example/pubspec.yaml b/packages/bloc_lint/example/pubspec.yaml new file mode 100644 index 00000000000..10e818cc043 --- /dev/null +++ b/packages/bloc_lint/example/pubspec.yaml @@ -0,0 +1,12 @@ +name: 'bloc_lint_example' +description: 'A project to test bloc_lint rules.' +environment: + sdk: ">=3.3.3 <4.0.0" + +dependencies: + bloc_lint: + path: '../' + custom_lint: ^0.7.0 + flutter: + sdk: flutter + flutter_bloc: ^8.1.6 diff --git a/packages/bloc_lint/lib/bloc_lint.dart b/packages/bloc_lint/lib/bloc_lint.dart new file mode 100644 index 00000000000..2c4ab272c1a --- /dev/null +++ b/packages/bloc_lint/lib/bloc_lint.dart @@ -0,0 +1,23 @@ +import 'package:bloc_lint/lint_rules/avoid_public_methods_on_bloc_lint_rule.dart'; +import 'package:bloc_lint/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule.dart'; +import 'package:bloc_lint/lint_rules/event_base_class_suffix.dart'; +import 'package:bloc_lint/lint_rules/prefer_multi_bloc_listener_lint_rule.dart'; +import 'package:bloc_lint/lint_rules/prefer_multi_bloc_provider_lint_rule.dart'; +import 'package:bloc_lint/lint_rules/prefer_multi_repository_provider_lint_rule.dart'; +import 'package:bloc_lint/lint_rules/state_base_class_suffix.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +PluginBase createPlugin() => _BlocLintPlugin(); + +class _BlocLintPlugin extends PluginBase { + @override + List getLintRules(CustomLintConfigs configs) => [ + const AvoidPublicMethodsOnBlocLintRule(), + const AvoidPublicPropertiesOnBlocAndCubitLintRule(), + const EventBaseClassSuffix(), + const PreferMultiBlocListenerLintRule(), + const PreferMultiBlocProviderLintRule(), + const PreferMultiRepositoryProviderLintRule(), + const StateBaseClassSuffix(), + ]; +} diff --git a/packages/bloc_lint/lib/bloc_lint_constants.dart b/packages/bloc_lint/lib/bloc_lint_constants.dart new file mode 100644 index 00000000000..e566b3c9fe5 --- /dev/null +++ b/packages/bloc_lint/lib/bloc_lint_constants.dart @@ -0,0 +1,4 @@ +class BlocLintConstants { + static const String blocClass = 'Bloc'; + static const String cubitClass = 'Cubit'; +} diff --git a/packages/bloc_lint/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule.dart b/packages/bloc_lint/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule.dart new file mode 100644 index 00000000000..8239cd59af3 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/avoid_public_methods_on_bloc_lint_rule.dart @@ -0,0 +1,38 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:bloc_lint/bloc_lint_constants.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class AvoidPublicMethodsOnBlocLintRule extends DartLintRule { + const AvoidPublicMethodsOnBlocLintRule() + : super( + code: const LintCode( + name: 'avoid_public_methods_on_bloc', + problemMessage: 'Avoid public methods on Bloc classes.', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addClassDeclaration((node) { + final superClass = node.extendsClause?.superclass.element?.name; + + if (superClass == BlocLintConstants.blocClass) { + for (final member in node.members) { + if (member is MethodDeclaration) { + final isPublic = !member.name.lexeme.startsWith('_'); + if (isPublic) { + reporter.atToken(member.name, code); + } + } + } + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule.dart b/packages/bloc_lint/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule.dart new file mode 100644 index 00000000000..96db0c63988 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/avoid_public_properties_on_bloc_and_cubit_lint_rule.dart @@ -0,0 +1,42 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:bloc_lint/bloc_lint_constants.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class AvoidPublicPropertiesOnBlocAndCubitLintRule extends DartLintRule { + const AvoidPublicPropertiesOnBlocAndCubitLintRule() + : super( + code: const LintCode( + name: 'avoid_public_properties_on_bloc_and_cubit', + problemMessage: + '''Avoid public properties on Bloc and Cubit besides state.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addClassDeclaration((node) { + final superClass = node.extendsClause?.superclass.element?.name; + + if (superClass == BlocLintConstants.blocClass || + superClass == BlocLintConstants.cubitClass) { + for (final member in node.members) { + if (member is FieldDeclaration) { + for (final variable in member.fields.variables) { + final isPublic = !variable.name.lexeme.startsWith('_'); + if (isPublic) { + reporter.atToken(variable.name, code); + } + } + } + } + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/event_base_class_suffix.dart b/packages/bloc_lint/lib/lint_rules/event_base_class_suffix.dart new file mode 100644 index 00000000000..48861d0c26f --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/event_base_class_suffix.dart @@ -0,0 +1,56 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:bloc_lint/bloc_lint_constants.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class EventBaseClassSuffix extends DartLintRule { + const EventBaseClassSuffix() + : super( + code: const LintCode( + name: 'event_base_class_suffix', + problemMessage: + '''The base event class should always be suffixed by 'Event'.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const _ignoredType = [ + 'int', + 'double', + 'num', + 'String', + 'bool', + 'List', + 'Set', + 'Map', + ]; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addExtendsClause((node) { + final superClass = node.superclass; + TypeAnnotation? stateNode; + + if (superClass.element?.name == BlocLintConstants.blocClass) { + stateNode = superClass.typeArguments?.arguments[0]; + } + if (stateNode == null) { + return; + } + final stateType = stateNode.type as InterfaceType?; + if (stateType == null || _ignoredType.contains(stateType.element.name)) { + return; + } + + if (stateType.element.name.endsWith('Event') == false) { + reporter.atNode(stateNode, code); + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_listener_lint_rule.dart b/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_listener_lint_rule.dart new file mode 100644 index 00000000000..2c36cb4d0a4 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_listener_lint_rule.dart @@ -0,0 +1,39 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class PreferMultiBlocListenerLintRule extends DartLintRule { + const PreferMultiBlocListenerLintRule() + : super( + code: const LintCode( + name: 'prefer_multi_bloc_listener', + problemMessage: + '''Consider using MultiBlocListener instead of multiple BlocListener instances.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const _className = 'BlocListener'; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addInstanceCreationExpression((node) { + if (node.constructorName.type.type?.element?.name == _className) { + var parent = node.parent; + while (parent != null) { + if (parent is InstanceCreationExpression && + parent.constructorName.type.type?.element?.name == _className) { + reporter.atNode(node, code); + break; + } + parent = parent.parent; + } + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_provider_lint_rule.dart b/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_provider_lint_rule.dart new file mode 100644 index 00000000000..0fc9ff823c4 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/prefer_multi_bloc_provider_lint_rule.dart @@ -0,0 +1,39 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class PreferMultiBlocProviderLintRule extends DartLintRule { + const PreferMultiBlocProviderLintRule() + : super( + code: const LintCode( + name: 'prefer_multi_bloc_provider', + problemMessage: + '''Consider using MultiBlocProvider instead of multiple BlocProvider instances.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const _className = 'BlocProvider'; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addInstanceCreationExpression((node) { + if (node.constructorName.type.type?.element?.name == _className) { + var parent = node.parent; + while (parent != null) { + if (parent is InstanceCreationExpression && + parent.constructorName.type.type?.element?.name == _className) { + reporter.atNode(node, code); + break; + } + parent = parent.parent; + } + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/prefer_multi_repository_provider_lint_rule.dart b/packages/bloc_lint/lib/lint_rules/prefer_multi_repository_provider_lint_rule.dart new file mode 100644 index 00000000000..aead2d6fb47 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/prefer_multi_repository_provider_lint_rule.dart @@ -0,0 +1,39 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class PreferMultiRepositoryProviderLintRule extends DartLintRule { + const PreferMultiRepositoryProviderLintRule() + : super( + code: const LintCode( + name: 'prefer_multi_repository_provider', + problemMessage: + '''Consider using MultiRepositoryProvider instead of multiple RepositoryProvider instances.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const _className = 'RepositoryProvider'; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addInstanceCreationExpression((node) { + if (node.constructorName.type.type?.element?.name == _className) { + var parent = node.parent; + while (parent != null) { + if (parent is InstanceCreationExpression && + parent.constructorName.type.type?.element?.name == _className) { + reporter.atNode(node, code); + break; + } + parent = parent.parent; + } + } + }); + } +} diff --git a/packages/bloc_lint/lib/lint_rules/state_base_class_suffix.dart b/packages/bloc_lint/lib/lint_rules/state_base_class_suffix.dart new file mode 100644 index 00000000000..d73771901b5 --- /dev/null +++ b/packages/bloc_lint/lib/lint_rules/state_base_class_suffix.dart @@ -0,0 +1,58 @@ +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:bloc_lint/bloc_lint_constants.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; + +class StateBaseClassSuffix extends DartLintRule { + const StateBaseClassSuffix() + : super( + code: const LintCode( + name: 'state_base_class_suffix', + problemMessage: + '''The base state class should always be suffixed by 'State'.''', + errorSeverity: ErrorSeverity.WARNING, + ), + ); + + static const _ignoredType = [ + 'int', + 'double', + 'num', + 'String', + 'bool', + 'List', + 'Set', + 'Map', + ]; + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addExtendsClause((node) { + final superClass = node.superclass; + TypeAnnotation? stateNode; + + if (superClass.element?.name == BlocLintConstants.cubitClass) { + stateNode = superClass.typeArguments?.arguments[0]; + } else if (superClass.element?.name == BlocLintConstants.blocClass) { + stateNode = superClass.typeArguments?.arguments[1]; + } + if (stateNode == null) { + return; + } + final stateType = stateNode.type as InterfaceType?; + if (stateType == null || _ignoredType.contains(stateType.element.name)) { + return; + } + + if (stateType.element.name.endsWith('State') == false) { + reporter.atNode(stateNode, code); + } + }); + } +} diff --git a/packages/bloc_lint/pubspec.yaml b/packages/bloc_lint/pubspec.yaml new file mode 100644 index 00000000000..f0c154e94d0 --- /dev/null +++ b/packages/bloc_lint/pubspec.yaml @@ -0,0 +1,13 @@ +name: bloc_lint +version: 0.1.0 +description: Set of lint rules for the Flutter BLoC library. +repository: https://github.com/felangel/bloc/tree/master/packages/bloc_lint +issue_tracker: https://github.com/felangel/bloc/issues + +environment: + sdk: ">=3.3.3 <4.0.0" + +dependencies: + analyzer: ^6.7.0 + analyzer_plugin: ^0.11.3 + custom_lint_builder: ^0.7.0