Macro. Add JsonSerializable example.

It looks that the element model correct, but resolution is not yet.
I will work on improving resolution in following CLs.

Change-Id: I225db07bf243539638364b711f0c9c68550199f1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/347120
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Phil Quitslund <pquitslund@google.com>
Commit-Queue: Konstantin Shcheglov <scheglov@google.com>
This commit is contained in:
Konstantin Shcheglov 2024-01-18 18:54:49 +00:00 committed by Commit Queue
parent 53af406764
commit 6f82d639d8
4 changed files with 378 additions and 0 deletions

View file

@ -264,6 +264,7 @@ class ToSourceVisitor implements AstVisitor<void> {
@override
void visitConstructorDeclaration(ConstructorDeclaration node) {
_visitNodeList(node.metadata, separator: ' ', suffix: ' ');
_visitToken(node.augmentKeyword, suffix: ' ');
_visitToken(node.externalKeyword, suffix: ' ');
_visitToken(node.constKeyword, suffix: ' ');
_visitToken(node.factoryKeyword, suffix: ' ');

View file

@ -47,6 +47,11 @@ class MacroResolutionTest extends PubPackageResolutionTest {
'$testPackageLibPath/order.dart',
getMacroCode('order.dart'),
);
newFile(
'$testPackageLibPath/json_serializable.dart',
getMacroCode('example/json_serializable.dart'),
);
}
test_declareType_class() async {
@ -472,6 +477,27 @@ class A {}
]);
}
@FailingTest(reason: r'''
CompileTimeErrorCode.UNDEFINED_METHOD [77, 8, "The method 'fromJson' isn't defined for the type 'User'.", "Try correcting the name to the name of an existing method, or defining a method named 'fromJson'."]
CompileTimeErrorCode.FINAL_NOT_INITIALIZED [141, 3, "The final variable 'age' must be initialized.", "Try initializing the variable."]
CompileTimeErrorCode.FINAL_NOT_INITIALIZED [161, 4, "The final variable 'name' must be initialized.", "Try initializing the variable."]
''')
test_example_jsonSerializable() async {
await assertNoErrorsInCode(r'''
import 'json_serializable.dart';
void f(Map<String, Object?> json) {
User.fromJson(json);
}
@JsonSerializable()
class User {
final int age;
final String name;
}
''');
}
test_getResolvedLibrary_macroAugmentation_hasErrors() async {
newFile(
'$testPackageLibPath/append.dart',

View file

@ -0,0 +1,238 @@
// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
//
// SharedOptions=--enable-experiment=macros
// ignore_for_file: deprecated_member_use
// There is no public API exposed yet, the in-progress API lives here.
import 'package:_fe_analyzer_shared/src/macros/api.dart';
final _dartCore = Uri.parse('dart:core');
/// A macro applied to a fromJson constructor, which fills in the initializer list.
/*macro*/ class FromJson implements ConstructorDefinitionMacro {
const FromJson();
@override
Future<void> buildDefinitionForConstructor(ConstructorDeclaration constructor,
ConstructorDefinitionBuilder builder) async {
// TODO(scheglov): Validate we are running on a valid fromJson constructor.
// TODO(scheglov): support extending other classes.
final clazz = (await builder.typeDeclarationOf(constructor.definingType))
as ClassDeclaration;
var string = NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'String'));
var object = NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'Object'));
var mapStringObject = NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'Map'),
typeArguments: [string, object.asNullable]);
if (clazz.superclass != null &&
!await (await builder.resolve(
NamedTypeAnnotationCode(name: clazz.superclass!.identifier)))
.isExactly(await builder.resolve(object))) {
throw UnsupportedError(
'Serialization of classes that extend other classes is not supported.');
}
var fields = await builder.fieldsOf(clazz);
var jsonParam = constructor.positionalParameters.single.identifier;
builder.augment(initializers: [
for (var field in fields)
RawCode.fromParts([
field.identifier,
' = ',
await _convertFieldFromJson(
field, jsonParam, builder, mapStringObject),
]),
]);
}
// TODO(scheglov): Support nested collections.
Future<Code> _convertFieldFromJson(
FieldDeclaration field,
Identifier jsonParam,
DefinitionBuilder builder,
NamedTypeAnnotationCode mapStringObject) async {
var fieldType = field.type;
if (fieldType is! NamedTypeAnnotation) {
throw ArgumentError(
'Only fields with named types are allowed on serializable classes, '
'but `${field.identifier.name}` was not a named type.');
}
var fieldTypeDecl = await builder.declarationOf(fieldType.identifier);
while (fieldTypeDecl is TypeAliasDeclaration) {
var aliasedType = fieldTypeDecl.aliasedType;
if (aliasedType is! NamedTypeAnnotation) {
throw ArgumentError(
'Only fields with named types are allowed on serializable classes, '
'but `${field.identifier.name}` did not resolve to a named type.');
}
}
if (fieldTypeDecl is! ClassDeclaration) {
throw ArgumentError(
'Only classes are supported in field types for serializable classes, '
'but the field `${field.identifier.name}` does not have a class '
'type.');
}
var fieldConstructors = await builder.constructorsOf(fieldTypeDecl);
var fieldTypeFromJson = fieldConstructors
.firstWhereOrNull((c) => c.identifier.name == 'fromJson')
?.identifier;
if (fieldTypeFromJson != null) {
return RawCode.fromParts([
fieldTypeFromJson,
'(',
jsonParam,
'["${field.identifier.name}"] as ',
mapStringObject,
')',
]);
} else {
return RawCode.fromParts([
jsonParam,
// TODO(scheglov): support nested serializable types.
'["${field.identifier.name}"] as ',
field.type.code,
]);
}
}
}
// TODO(scheglov): Support collections, extending serializable classes, and more.
/*macro*/ class JsonSerializable implements ClassDeclarationsMacro {
const JsonSerializable();
@override
Future<void> buildDeclarationsForClass(
ClassDeclaration clazz, MemberDeclarationBuilder builder) async {
var constructors = await builder.constructorsOf(clazz);
if (constructors.any((c) => c.identifier.name == 'fromJson')) {
throw ArgumentError('There is already a `fromJson` constructor for '
'`${clazz.identifier.name}`, so one could not be added.');
}
var map = await builder.resolveIdentifier(_dartCore, 'Map');
var string = NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'String'));
var object = NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'Object'));
var mapStringObject = NamedTypeAnnotationCode(
name: map, typeArguments: [string, object.asNullable]);
// TODO(scheglov): This only works
var jsonSerializableUri =
clazz.library.uri.resolve('json_serializable.dart');
builder.declareInType(DeclarationCode.fromParts([
' @',
await builder.resolveIdentifier(jsonSerializableUri, 'FromJson'),
'()\n ',
clazz.identifier.name,
'.fromJson(',
mapStringObject,
' json);',
]));
builder.declareInType(DeclarationCode.fromParts([
' @',
await builder.resolveIdentifier(jsonSerializableUri, 'ToJson'),
'()\n ',
mapStringObject,
' toJson();',
]));
}
}
/// A macro applied to a toJson instance method, which fills in the body.
/*macro*/ class ToJson implements MethodDefinitionMacro {
const ToJson();
@override
Future<void> buildDefinitionForMethod(
MethodDeclaration method, FunctionDefinitionBuilder builder) async {
// TODO(scheglov): Validate we are running on a valid toJson method.
// TODO(scheglov): support extending other classes.
final clazz = (await builder.typeDeclarationOf(method.definingType))
as ClassDeclaration;
var object = await builder.resolve(NamedTypeAnnotationCode(
name: await builder.resolveIdentifier(_dartCore, 'Object')));
if (clazz.superclass != null &&
!await (await builder.resolve(
NamedTypeAnnotationCode(name: clazz.superclass!.identifier)))
.isExactly(object)) {
throw UnsupportedError(
'Serialization of classes that extend other classes is not supported.');
}
var fields = await builder.fieldsOf(clazz);
builder.augment(FunctionBodyCode.fromParts([
' => {',
for (var field in fields)
RawCode.fromParts([
'\n \'',
field.identifier.name,
'\'',
': ',
await _convertFieldToJson(field, builder),
',',
]),
'\n };',
]));
}
// TODO(scheglov): Support nested collections.
Future<Code> _convertFieldToJson(
FieldDeclaration field, DefinitionBuilder builder) async {
var fieldType = field.type;
if (fieldType is! NamedTypeAnnotation) {
throw ArgumentError(
'Only fields with named types are allowed on serializable classes, '
'but `${field.identifier.name}` was not a named type.');
}
var fieldTypeDecl = await builder.declarationOf(fieldType.identifier);
while (fieldTypeDecl is TypeAliasDeclaration) {
var aliasedType = fieldTypeDecl.aliasedType;
if (aliasedType is! NamedTypeAnnotation) {
throw ArgumentError(
'Only fields with named types are allowed on serializable classes, '
'but `${field.identifier.name}` did not resolve to a named type.');
}
}
if (fieldTypeDecl is! ClassDeclaration) {
throw ArgumentError(
'Only classes are supported in field types for serializable classes, '
'but the field `${field.identifier.name}` does not have a class '
'type.');
}
var fieldTypeMethods = await builder.methodsOf(fieldTypeDecl);
var fieldToJson = fieldTypeMethods
.firstWhereOrNull((c) => c.identifier.name == 'toJson')
?.identifier;
if (fieldToJson != null) {
return RawCode.fromParts([
field.identifier,
'.toJson()',
]);
} else {
// TODO(scheglov): Check that it is a valid type we can serialize.
return RawCode.fromParts([
field.identifier,
]);
}
}
}
extension _FirstWhereOrNull<T> on Iterable<T> {
T? firstWhereOrNull(bool Function(T) compare) {
for (var item in this) {
if (compare(item)) return item;
}
return null;
}
}

View file

@ -4175,6 +4175,119 @@ class MacroExampleTest extends MacroElementsBaseTest {
@override
bool get keepLinkingLibraries => true;
test_jsonSerializable() async {
_addExampleMacro('json_serializable.dart');
final library = await buildLibrary(r'''
import 'json_serializable.dart';
@JsonSerializable()
class A {
final int foo;
final int bar;
}
''');
configuration
..withReferences = true
..withMetadata = false;
checkElementText(library, r'''
library
reference: self
imports
package:test/json_serializable.dart
definingUnit
reference: self
classes
class A @60
reference: self::@class::A
augmentation: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A
fields
final foo @76
reference: self::@class::A::@field::foo
type: int
final bar @93
reference: self::@class::A::@field::bar
type: int
accessors
synthetic get foo @-1
reference: self::@class::A::@getter::foo
returnType: int
synthetic get bar @-1
reference: self::@class::A::@getter::bar
returnType: int
augmented
fields
self::@class::A::@field::bar
self::@class::A::@field::foo
constructors
self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@constructorAugmentation::fromJson
accessors
self::@class::A::@getter::bar
self::@class::A::@getter::foo
methods
self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@methodAugmentation::toJson
augmentationImports
package:test/test.macro.dart
reference: self::@augmentation::package:test/test.macro.dart
macroGeneratedCode
---
library augment 'test.dart';
import 'package:test/json_serializable.dart' as prefix0;
import 'dart:core' as prefix1;
augment class A {
@prefix0.FromJson()
A.fromJson(prefix1.Map<prefix1.String, prefix1.Object?> json);
@prefix0.ToJson()
prefix1.Map<prefix1.String, prefix1.Object?> toJson();
augment A.fromJson(prefix1.Map<prefix1.String, prefix1.Object?> json, ) : this.foo = json["foo"] as prefix1.int,
this.bar = json["bar"] as prefix1.int{}
augment prefix1.Map<prefix1.String, prefix1.Object?> toJson() => {
'foo': this.foo,
'bar': this.bar,
};
}
---
imports
package:test/json_serializable.dart as prefix0 @78
dart:core as prefix1 @109
definingUnit
reference: self::@augmentation::package:test/test.macro.dart
classes
augment class A @133
reference: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A
augmentationTarget: self::@class::A
constructors
fromJson @163
reference: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@constructor::fromJson
periodOffset: 162
nameEnd: 171
parameters
requiredPositional json @217
type: Map<String, Object?>
augmentation: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@constructorAugmentation::fromJson
augment fromJson @313
reference: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@constructorAugmentation::fromJson
periodOffset: 312
nameEnd: 321
parameters
requiredPositional json @367
type: Map<String, Object?>
augmentationTarget: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@constructor::fromJson
methods
abstract toJson @291
reference: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@method::toJson
returnType: Map<String, Object?>
augmentation: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@methodAugmentation::toJson
augment toJson @512
reference: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@methodAugmentation::toJson
returnType: Map<String, Object?>
augmentationTarget: self::@augmentation::package:test/test.macro.dart::@classAugmentation::A::@method::toJson
''');
}
test_observable() async {
_addExampleMacro('observable.dart');