diff --git a/packages/riverpod_lint/CHANGELOG.md b/packages/riverpod_lint/CHANGELOG.md index 5430b863e..1c2537011 100644 --- a/packages/riverpod_lint/CHANGELOG.md +++ b/packages/riverpod_lint/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased minor + +- Added `notifier_build`, a lint to catch when a Notifier has no `build` method (thansk to @LeonardoRosaa) + ## 2.0.4 - 2023-09-04 - `riverpod` upgraded to `2.4.0` diff --git a/packages/riverpod_lint/README.md b/packages/riverpod_lint/README.md index e87e7c4f1..011c4ace7 100644 --- a/packages/riverpod_lint/README.md +++ b/packages/riverpod_lint/README.md @@ -50,6 +50,7 @@ Riverpod_lint adds various warnings with quick fixes and refactoring options, su - [functional\_ref (riverpod\_generator only)](#functional_ref-riverpod_generator-only) - [notifier\_extends (riverpod\_generator only)](#notifier_extends-riverpod_generator-only) - [avoid\_ref\_inside\_state\_dispose](#avoid_ref_inside_state_dispose) + - [missed\_build\_method (riverpod\_generator only)](#notifier_build-riverpod_generator-only) - [All assists](#all-assists) - [Wrap widgets with a `Consumer`](#wrap-widgets-with-a-consumer) - [Wrap widgets with a `ProviderScope`](#wrap-widgets-with-a-providerscope) @@ -551,6 +552,29 @@ class _MyWidgetState extends ConsumerState { } ``` +### notifier_build (riverpod_generator only) + +Classes annotated by `@riverpod` must have the `build` method. + +**Good**: + +```dart +@riverpod +class Example extends _$Example { + + @overried + int build() => 0; +} +``` + +**Bad**: + +```dart +// No "build" method found +@riverpod +class Example extends _$Example {} +``` + ## All assists ### Wrap widgets with a `Consumer` diff --git a/packages/riverpod_lint/lib/riverpod_lint.dart b/packages/riverpod_lint/lib/riverpod_lint.dart index d1bcf803d..41091e91b 100644 --- a/packages/riverpod_lint/lib/riverpod_lint.dart +++ b/packages/riverpod_lint/lib/riverpod_lint.dart @@ -12,6 +12,7 @@ import 'src/lints/avoid_public_notifier_properties.dart'; import 'src/lints/avoid_ref_inside_state_dispose.dart'; import 'src/lints/functional_ref.dart'; import 'src/lints/missing_provider_scope.dart'; +import 'src/lints/notifier_build.dart'; import 'src/lints/notifier_extends.dart'; import 'src/lints/provider_dependencies.dart'; import 'src/lints/provider_parameters.dart'; @@ -33,6 +34,7 @@ class _RiverpodPlugin extends PluginBase { const ScopedProvidersShouldSpecifyDependencies(), const UnsupportedProviderValue(), const AvoidRefInsideStateDispose(), + const NotifierBuild(), // const AvoidDynamicProviders(), // // "Avoid passing providers as parameter to objects" // const AvoidExposingProviderRef(), diff --git a/packages/riverpod_lint/lib/src/lints/notifier_build.dart b/packages/riverpod_lint/lib/src/lints/notifier_build.dart new file mode 100644 index 000000000..eb04c2f67 --- /dev/null +++ b/packages/riverpod_lint/lib/src/lints/notifier_build.dart @@ -0,0 +1,90 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/error/error.dart'; +import 'package:analyzer/error/listener.dart'; +import 'package:custom_lint_builder/custom_lint_builder.dart'; +import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart'; + +import '../riverpod_custom_lint.dart'; + +const _buildMethodName = 'build'; + +class NotifierBuild extends RiverpodLintRule { + const NotifierBuild() : super(code: _code); + + static const _code = LintCode( + name: 'notifier_build', + problemMessage: + 'Classes annotated by `@riverpod` must have the `build` method', + ); + + @override + void run( + CustomLintResolver resolver, + ErrorReporter reporter, + CustomLintContext context, + ) { + context.registry.addClassDeclaration((node) { + final hasRiverpodAnnotation = node.metadata.where( + (element) { + final annotationElement = element.element; + + if (annotationElement == null || + annotationElement is! ExecutableElement) return false; + + return riverpodType.isExactlyType(annotationElement.returnType); + }, + ).isNotEmpty; + + if (!hasRiverpodAnnotation) return; + + final hasBuildMethod = node.members + .where((e) => e.declaredElement?.displayName == _buildMethodName) + .isNotEmpty; + + if (hasBuildMethod) return; + + reporter.reportErrorForToken(_code, node.name); + }); + } + + @override + List getFixes() => [ + AddBuildMethodFix(), + ]; +} + +class AddBuildMethodFix extends RiverpodFix { + @override + void run( + CustomLintResolver resolver, + ChangeReporter reporter, + CustomLintContext context, + AnalysisError analysisError, + List others, + ) { + context.registry.addClassDeclaration((node) { + if (!node.sourceRange.intersects(analysisError.sourceRange)) return; + + final changeBuilder = reporter.createChangeBuilder( + message: 'Add build method', + priority: 80, + ); + + changeBuilder.addDartFileEdit((builder) { + final offset = node.leftBracket.offset + 1; + + builder.addSimpleInsertion( + offset, + ''' + + @override + dynamic build() { + // TODO: implement build + throw UnimplementedError(); + } +''', + ); + }); + }); + } +} diff --git a/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.dart b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.dart new file mode 100644 index 000000000..27f5e0959 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.dart @@ -0,0 +1,8 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/// Fake Provider +typedef _$ExampleProvider1 = Object; + +@riverpod +// expect_lint: notifier_build +class ExampleProvider1 extends _$ExampleProvider1 {} diff --git a/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.json b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build_test.dart b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build_test.dart new file mode 100644 index 000000000..e1ca426dd --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/goldens/fixes/notifier_build_test.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:riverpod_lint/src/lints/notifier_build.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/analysis/utilities.dart'; + +import '../../golden.dart'; + +void main() { + testGolden( + 'Verify that @riverpod classes has the build method', + 'goldens/fixes/notifier_build.json', + () async { + const lint = NotifierBuild(); + final fix = lint.getFixes().single; + final file = File( + 'test/goldens/fixes/notifier_build.dart', + ).absolute; + + final result = await resolveFile2(path: file.path); + result as ResolvedUnitResult; + + final errors = await lint.testRun(result); + + final changes = await Future.wait([ + for (final error in errors) fix.testRun(result, error, errors), + ]); + + return changes.flattened; + }, + ); +} diff --git a/packages/riverpod_lint_flutter_test/test/goldens/lints/notifier_build.dart b/packages/riverpod_lint_flutter_test/test/goldens/lints/notifier_build.dart new file mode 100644 index 000000000..52ac1fe58 --- /dev/null +++ b/packages/riverpod_lint_flutter_test/test/goldens/lints/notifier_build.dart @@ -0,0 +1,17 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +/// Fake Provider +typedef _$ExampleProvider1 = Object; + +/// Fake Provider +typedef _$ExampleProvider = AutoDisposeNotifier; + +@riverpod +// expect_lint: notifier_build +class ExampleProvider1 extends _$ExampleProvider1 {} + +@riverpod +class ExampleProvider extends _$ExampleProvider { + @override + int build() => 0; +}