dart-lang / dart-lang/source_gen

SharedPartBuilders seemingly cannot use code generated by other SharedPartBuilders

Open
#787 5 comments 0 reactions 1 assignee Claimed by @davidmorgan View on GitHub
Dominant language
Dart
Stars
494
Forks
115
Avg merge
12h 13m
Merged PRs (30d)
3

Description

Short version: I've written a builder to generate ID types for a project that also uses json_serializable. My builder works fine, but json_serializable fails because it cannot resolve the types generated by my builder.

So in our project, we use extension types to define explicit ID types for models, like this:
```dart
extension type const UserId(int id) implements Object {
factory UserId.fromJson(int id) => UserId(id);
int toJson() => id;
static UserId? tryParse(String? value) {
final id = int.tryParse(value ?? '');
return id == null ? null : UserId(id);
}
}
```

We have a lot of these, so I decided to try to start generating them for improved dev experience and maintenance (also out of curiosity to try using source_gen). So I came up with a pretty simple builder which basically just outputs that code with the type name substituted. And it works fine! Very satisfying. However, when I have a model that has an `@IdType` *and* `@JsonSerializable`, json_serializable fails:
```
E json_serializable on lib/models/user.dart:
Could not generate `fromJson` code for `id`.
To support the type `InvalidType` you can:
* Use `JsonConverter`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonConverter-class.html
* Use `JsonKey` fields `fromJson` and `toJson`
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/fromJson.html
https://pub.dev/documentation/json_annotation/latest/json_annotation/JsonKey/toJson.html
package:impruvon_health/models/user.dart:21:16

21 │ final UserId id;
```

To be clear, the id types are usable by json_serializable without any issues when I define them by hand in the model file. It seems like json_serializable is not able to use intermediate generated parts from before the combining builder combines all the parts. It kind of makes sense that it's like this, but this is definitely a limitation given that it essentially bars you from writing any custom builders that need to interact with other custom builders, which is probably a lot of use cases.

fwiw I also tried forking json_serializable and adding `required_inputs: ['id_type.g.part']` to its `build.yaml` but that also didn't work. (this isn't actually a viable solution because we don't want to maintain a fork, just did this to try to understand the problem a bit more)

So:
A) Is there any reasonable workaround for this? I'm not enthusiastic about generating into some `.id.dart` file (and I'm not even sure that would work), would rather keep everything in `{model}.g.dart`. I suppose generating them all to some `models/ids.g.dart` would be viable but don't love it either.
B) Is there any improvement that could be made to source_gen (or build?) that could prevent this from being an issue? I would be happy to work on this but probably wouldn't know where to start.

Some extra context:
build.yaml from my builder package

```yaml
builders:
impruvon_builder_gen:
target: ":impruvon_builder_gen"
import: "package:impruvon_builder_gen/builder.dart"
builder_factories: ["idTypeBuilder"]
build_extensions: {".dart": ["id_type.g.part"]}
auto_apply: dependents
build_to: cache
applies_builders: ["source_gen:combining_builder"]
runs_before: ["json_serializable"]
```


builder.dart

```dart
import 'package:build/build.dart';
import 'package:impruvon_builder_gen/id_type_generator.dart';
import 'package:source_gen/source_gen.dart' show SharedPartBuilder;

Builder idTypeBuilder(BuilderOptions options) =>
SharedPartBuilder([IdTypeGenerator(options.config)], 'id_type');
```


id_type_generator.dart

```dart
import 'package:analyzer/dart/element/element2.dart'
show Element2, ClassElement2;
import 'package:build/build.dart' show BuildStep;
import 'package:impruvon_builder/impruvon_builder.dart';
import 'package:source_gen/source_gen.dart'
show ConstantReader, GeneratorForAnnotation, InvalidGenerationSourceError;

class IdTypeGenerator extends GeneratorForAnnotation {
final Map config;

IdTypeGenerator(this.config);

@override
String generateForAnnotatedElement(
Element2 element,
ConstantReader annotation,
BuildStep buildStep,
) {
if (element is! ClassElement2) {
throw InvalidGenerationSourceError(
'@IdType can only be used on classes. "$element" is not a class.',
element: element,
);
}

final typeName =
annotation.peek('name')?.stringValue ?? '${element.displayName}Id';

final code =
'''
extension type const $typeName(int id) implements Object {
factory $typeName.fromJson(int id) => $typeName(id);
int toJson() => id;
static $typeName? tryParse(String? value) {
final id = int.tryParse(value ?? '');
return id == null ? null : $typeName(id);
}
}
''';

return code;
}
}
```


Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.