blob: 1784f698a54e6a803c57d1bd1e4c9f0040db5180 [file] [edit]
// 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.
import 'dart:async';
import 'package:dart_model/dart_model.dart';
// ignore: implementation_imports
import 'package:dart_model/src/macro_metadata.g.dart';
import 'package:macro/macro.dart';
import 'package:macro_service/macro_service.dart';
import 'templating.dart';
/// A macro equivalent to `package:built_value` value types.
class BuiltValue {
const BuiltValue();
}
/// A macro equivalent to `package:built_value` builders.
class BuiltValueBuilder {
const BuiltValueBuilder();
}
class BuiltValueImplementation
implements ClassTypesMacro, ClassDeclarationsMacro {
@override
MacroDescription get description => MacroDescription(
annotation: QualifiedName(
uri: 'package:_test_macros/built_value.dart',
name: 'BuiltValue',
),
runsInPhases: [1, 2],
);
@override
Future<void> buildTypesForClass(ClassTypesBuilder<Interface> builder) async {
final valueName = builder.model.qualifiedNameOf(builder.target.node)!;
final builderSimpleName = '${valueName.name}Builder';
builder.declareType(
builderSimpleName,
augmentation('''
@{{package:_test_macros/built_value.dart#BuiltValueBuilder}}()
class $builderSimpleName {}
'''),
);
}
@override
Future<void> buildDeclarationsForClass(
ClassDeclarationsBuilder builder,
) async {
final valueName = builder.model.qualifiedNameOf(builder.target.node)!;
final builderName = QualifiedName(
uri: valueName.uri,
name: '${valueName.name}Builder',
);
final fields =
builder.target.members.entries
.where((e) => e.value.properties.isField)
.toList();
final constructorParams = StringBuffer();
if (fields.isNotEmpty) {
constructorParams.write('{');
for (final field in fields) {
constructorParams.write('required this.${field.key},');
}
constructorParams.write('}');
}
final computeHash = StringBuffer('{{dart:core#Object}}.hashAll([');
for (final field in fields) {
computeHash.write('${field.key},');
}
computeHash.write('])');
final comparisons = StringBuffer();
for (final field in fields) {
comparisons.write('&& ${field.key} == other.${field.key}');
}
final toString = StringBuffer('${valueName.name}(');
for (final field in fields) {
toString.write('${field.key}: \$${field.key}');
if (field != fields.last) {
toString.write(', ');
}
}
toString.write(')');
builder.declareInType(
augmentation('''
factory ${valueName.name}([void Function(${builderName.code})? updates]) =>
(${builderName.code}()..update(updates)).build();
${valueName.name}._($constructorParams) {}
${builderName.code} toBuilder() => ${builderName.code}()..replace(this);
${valueName.code} rebuild(void Function(${builderName.code}) updates) =>
(toBuilder()..update(updates)).build();
{{dart:core#int}} get hashCode => $computeHash;
{{dart:core#bool}} operator==({{dart:core#Object}} other) =>
other is ${valueName.code}$comparisons;
{{dart:core#String}} toString() => '$toString';
'''),
);
}
}
class BuiltValueBuilderImplementation implements ClassDeclarationsMacro {
@override
MacroDescription get description => MacroDescription(
annotation: QualifiedName(
uri: 'package:_test_macros/built_value.dart',
name: 'BuiltValueBuilder',
),
runsInPhases: [2],
);
@override
Future<void> buildDeclarationsForClass(
ClassDeclarationsBuilder builder,
) async {
final builderName = builder.model.qualifiedNameOf(builder.target.node)!;
var valueShortName = builderName.name;
if (valueShortName.endsWith('Builder')) {
valueShortName = valueShortName.substring(
0,
valueShortName.length - 'Builder'.length,
);
} else {
throw StateError('Builder class should have name ending "Builder".');
}
final valueName = QualifiedName(uri: builderName.uri, name: valueShortName);
await builder.query(Query(target: valueName));
final valueInterface =
builder.model.uris[valueName.uri]!.scopes[valueName.name]!;
final fields =
valueInterface.members.entries
.where((e) => e.value.properties.isField)
.toList();
// Check which field types are annotated with `@BuiltValue`, meaning they
// should use nested Builders.
//
// First, find all the types and query for them.
// TODO(davidmorgan): there should be a way to do this in one query.
final fieldTypes = <String>{};
for (final field in fields) {
final qualifiedName = field.value.returnType.qualifiedName;
if (qualifiedName.uri != 'dart:core') {
fieldTypes.add(qualifiedName.asString);
}
}
for (final fieldType in fieldTypes) {
final qualifiedName = QualifiedName.parse(fieldType);
await builder.query(Query(target: qualifiedName));
}
// Now check which field types have the annotation.
final nestedBuilderTypes = <String>{};
for (final fieldType in fieldTypes) {
final qualifiedName = QualifiedName.parse(fieldType);
final fieldTypeAnnotations =
builder
.model
.uris[qualifiedName.uri]!
.scopes[qualifiedName.name]!
.metadataAnnotations;
for (final fieldTypeAnnotation in fieldTypeAnnotations) {
if (fieldTypeAnnotation.expression.type !=
ExpressionType.constructorInvocation) {
continue;
}
final constructorInvocation =
fieldTypeAnnotation.expression.asConstructorInvocation;
if (constructorInvocation.type.type !=
TypeAnnotationType.namedTypeAnnotation) {
continue;
}
final namedTypeAnnotation =
constructorInvocation.type.asNamedTypeAnnotation;
if (namedTypeAnnotation.reference.type !=
ReferenceType.classReference) {
continue;
}
final constructorReference =
namedTypeAnnotation.reference.asClassReference;
if (constructorReference.name != 'BuiltValue') {
continue;
}
nestedBuilderTypes.add(qualifiedName.asString);
}
}
final fieldDeclarations = StringBuffer();
for (final field in fields) {
final fieldTypeQualifiedName = field.value.returnType.qualifiedName;
if (nestedBuilderTypes.contains(fieldTypeQualifiedName.asString)) {
final fieldBuilderQualifiedName = QualifiedName(
uri: fieldTypeQualifiedName.uri,
name: '${fieldTypeQualifiedName.name}Builder',
);
fieldDeclarations.write(
'${fieldBuilderQualifiedName.code} ${field.key} = '
'${fieldBuilderQualifiedName.code}();',
);
} else {
fieldDeclarations.write(
'${fieldTypeQualifiedName.code}? ${field.key};',
);
}
}
final copyFields = StringBuffer();
for (final field in fields) {
final fieldTypeQualifiedName = field.value.returnType.qualifiedName;
if (nestedBuilderTypes.contains(fieldTypeQualifiedName.asString)) {
copyFields.write('this.${field.key} = other.${field.key}.toBuilder();');
} else {
copyFields.write('this.${field.key} = other.${field.key};');
}
}
final buildParams = StringBuffer();
for (final field in fields) {
final fieldTypeQualifiedName = field.value.returnType.qualifiedName;
if (nestedBuilderTypes.contains(fieldTypeQualifiedName.asString)) {
buildParams.write('${field.key}: ${field.key}.build(),');
} else {
final maybeNotNull = field.value.returnType.isNullable ? '' : '!';
buildParams.write('${field.key}: ${field.key}$maybeNotNull,');
}
}
builder.declareInType(
augmentation('''
$fieldDeclarations
void replace(${valueName.code} other) { $copyFields }
void update(void Function(${builderName.code})? updates) => updates?.call(this);
${valueName.code} build() => ${valueName.code}._($buildParams);
'''),
);
}
}
extension StaticTypeDescExtension on StaticTypeDesc {
bool get isNullable => type == StaticTypeDescType.nullableTypeDesc;
QualifiedName get qualifiedName {
return switch (type) {
StaticTypeDescType.namedTypeDesc => asNamedTypeDesc.name,
StaticTypeDescType.nullableTypeDesc =>
asNullableTypeDesc.inner.qualifiedName,
_ => throw ArgumentError(type),
};
}
}