Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve notifier error handling #3208

Merged
merged 1 commit into from
Dec 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/riverpod_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Unreleased patch

- Improved error handling if:
- a Notifier has no default constructor
- a Notifier has has a default constructor but with required parameters
- a Notifier is abstract

## 2.3.9 - 2023-11-27

- `riverpod_annotation` upgraded to `2.3.3`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart';
import '../models.dart';
import '../riverpod_generator.dart';
import '../validation.dart';
import 'template.dart';

String providerNameFor(
Expand Down Expand Up @@ -81,7 +82,10 @@ class ClassBasedProviderTemplate extends Template {
'Expected a class-based provider with no parameter',
);
}

validateClassBasedProvider(provider);
}

final ClassBasedProviderDeclaration provider;
final String notifierTypedefName;
final String hashFn;
Expand Down
3 changes: 3 additions & 0 deletions packages/riverpod_generator/lib/src/templates/family.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart';

import '../models.dart';
import '../riverpod_generator.dart';
import '../validation.dart';
import 'class_based_provider.dart';
import 'parameters.dart';
import 'template.dart';
Expand Down Expand Up @@ -121,6 +122,8 @@ ${parameters.map((e) => ' ${e.name}: ${e.name},\n').join()}
required String hashFn,
required BuildYamlOptions options,
}) {
validateClassBasedProvider(provider);

var leading = '';
if (!provider.annotation.element.keepAlive) {
leading = 'AutoDispose';
Expand Down
31 changes: 31 additions & 0 deletions packages/riverpod_generator/lib/src/validation.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import 'package:collection/collection.dart';
import 'package:riverpod_analyzer_utils/riverpod_analyzer_utils.dart';
import 'package:source_gen/source_gen.dart';

void validateClassBasedProvider(ClassBasedProviderDeclaration provider) {
// Assert that the class is not abstract
if (provider.node.abstractKeyword != null) {
throw InvalidGenerationSourceError(
'`@riverpod` can only be used on concrete classes.',
element: provider.node.declaredElement,
);
}

// Assert that the provider has a default constructor
final constructor = provider.node.declaredElement!.constructors
.firstWhereOrNull((e) => e.isDefaultConstructor);
if (constructor == null) {
throw InvalidGenerationSourceError(
'The class ${provider.node.name} must have a default constructor.',
element: provider.node.declaredElement,
);
}

// Assert that the default constructor can be called with no parameter
if (constructor.parameters.any((e) => e.isRequired)) {
throw InvalidGenerationSourceError(
'The default constructor of ${provider.node.name} must have not have required parameters.',
element: constructor,
);
}
}
147 changes: 147 additions & 0 deletions packages/riverpod_generator/test/error_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import 'dart:convert';
import 'dart:io';

import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:path/path.dart' as path;
import 'package:riverpod_generator/src/riverpod_generator.dart';
import 'package:source_gen/source_gen.dart';
import 'package:test/test.dart';

void main() {
group('Notifiers', () {
group('with arguments', () {
test('should throw if the class is abstract', () async {
const source = r'''
import 'package:riverpod_annotation/riverpod_annotation.dart';

@riverpod
abstract class MyNotifier extends _$MyNotifier {
@override
int build(int a) => 0;
}
''';

await expectLater(
() => compile(source),
throwsA(isA<InvalidGenerationSourceError>()),
);
});

test('should throw if there is no default constructor', () async {
const source = r'''
import 'package:riverpod_annotation/riverpod_annotation.dart';

@riverpod
class MyNotifier extends _$MyNotifier {
MyNotifier._();

@override
int build(int a) => 0;
}
''';

await expectLater(
() => compile(source),
throwsA(isA<InvalidGenerationSourceError>()),
);
});

test('should throw if the default constructor has required parameters',
() async {
const source = r'''
import 'package:riverpod_annotation/riverpod_annotation.dart';

@riverpod
class MyNotifier extends _$MyNotifier {
MyNotifier(int a);

@override
int build(int a) => 0;
}
''';

await expectLater(
() => compile(source),
throwsA(isA<InvalidGenerationSourceError>()),
);
});
});

group('without arguments', () {
test('should throw if the class is abstract', () async {
const source = r'''
import 'package:riverpod_annotation/riverpod_annotation.dart';

@riverpod
abstract class MyNotifier extends _$MyNotifier {
@override
int build() => 0;
}
''';

await expectLater(
() => compile(source),
throwsA(isA<InvalidGenerationSourceError>()),
);
});
});
});
}

void tearDownTmp() {
addTearDown(() {
final tmp = Directory('.dart_tool/test');
if (tmp.existsSync()) {
tmp.deleteSync(recursive: true);
}
});
}

File createTmpFile(String filePath) {
tearDownTmp();

final file = File(path.join('.dart_tool', 'test', filePath));
file.createSync(recursive: true);

return file;
}

Future<String> compile(String source) async {
final generator = RiverpodGenerator(const {});

final main = createTmpFile('lib/main.dart')..writeAsStringSync(source);
final pubspec = createTmpFile('pubspec.yaml')..writeAsStringSync('''
name: test_app

environment:
sdk: ">=3.0.0 <4.0.0"

dependencies:
riverpod_annotation: ^2.3.3
''');

await runPubGet(pubspec.parent);

final result = await resolveFile2(path: main.absolute.path);

result as ResolvedUnitResult;

return generator.generateForUnit([result.unit]);
}

Future<void> runPubGet(Directory parent) async {
final process = await Process.start(
'dart',
['pub', 'get'],
workingDirectory: parent.path,
);

final exitCode = await process.exitCode;
if (exitCode != 0) {
throw Exception(
'flutter pub get failed with exit code $exitCode\n'
'${await process.stdout.transform(utf8.decoder).join()}',
);
}
}
Loading