mirror of
https://github.com/dart-lang/sdk
synced 2024-11-02 15:01:30 +00:00
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:
parent
53af406764
commit
6f82d639d8
4 changed files with 378 additions and 0 deletions
|
@ -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: ' ');
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
|
||||
|
|
Loading…
Reference in a new issue