blob: 85654cd216f58cdc69967235e8819656ca24187f [file] [log] [blame]
// Copyright (c) 2015, Google Inc. 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.
library built_value_generator.source_class;
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/element/element.dart';
import 'package:analyzer/dart/element/type.dart';
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value_generator/src/enum_source_class.dart';
import 'package:built_value_generator/src/enum_source_field.dart';
import 'package:built_value_generator/src/fields.dart' show collectFields;
import 'package:built_value_generator/src/serializer_source_field.dart';
import 'package:built_value_generator/src/strings.dart';
import 'package:built_value_generator/src/value_source_class.dart';
import 'dart_types.dart';
part 'serializer_source_class.g.dart';
abstract class SerializerSourceClass
implements Built<SerializerSourceClass, SerializerSourceClassBuilder> {
ClassElement get element;
@nullable
ClassElement get builderElement;
factory SerializerSourceClass(ClassElement element) =>
_$SerializerSourceClass._(
element: element,
builderElement:
element.library.getType(element.displayName + 'Builder'));
SerializerSourceClass._();
@memoized
ParsedLibraryResult get parsedLibrary =>
element.library.session.getParsedLibraryByElement(element.library);
// TODO(davidmorgan): share common code in a nicer way.
@memoized
BuiltValue get builtValueSettings => ValueSourceClass(element).settings;
@memoized
BuiltValueSerializer get serializerSettings {
var serializerFields =
element.fields.where((field) => field.name == 'serializer').toList();
if (serializerFields.isEmpty) return const BuiltValueSerializer();
var serializerField = serializerFields.single;
if (serializerField.getter == null) return const BuiltValueSerializer();
var annotations = serializerField.getter.metadata
.map((annotation) => annotation.computeConstantValue())
.where((value) =>
DartTypes.getName(value?.type) == 'BuiltValueSerializer');
if (annotations.isEmpty) return const BuiltValueSerializer();
var annotation = annotations.single;
// If a field does not exist, that means an old `built_value` version; use
// the default.
return BuiltValueSerializer(
custom: annotation.getField('custom')?.toBoolValue() ?? false,
serializeNulls:
annotation.getField('serializeNulls')?.toBoolValue() ?? false);
}
// TODO(davidmorgan): share common code in a nicer way.
@memoized
BuiltValueEnum get enumClassSettings =>
EnumSourceClass(parsedLibrary, element).settings;
@memoized
String get name => element.name;
@memoized
String get wireName {
if (isBuiltValue) {
return builtValueSettings.wireName ?? name;
} else if (isEnumClass) {
return enumClassSettings.wireName ?? name;
} else {
return name;
}
}
@memoized
String get serializerDeclaration {
var serializerFields =
element.fields.where((field) => field.name == 'serializer').toList();
if (serializerFields.isEmpty) return '';
var serializerField = serializerFields.single;
var result = parsedLibrary
.getElementDeclaration(serializerField.getter)
?.node
?.toSource() ??
'';
// Strip off annotations.
if (result.startsWith('@')) {
// First thing after the annotations should be `static`.
if (result.contains(' static ')) {
result = result.substring(result.indexOf(' static ') + 1);
}
}
return result;
}
@memoized
BuiltList<String> get genericParameters =>
BuiltList<String>(element.typeParameters.map((e) => e.name));
@memoized
BuiltList<String> get genericBounds =>
BuiltList<String>(element.typeParameters
.map((element) => (element.bound ?? '').toString()));
// TODO(davidmorgan): better check.
@memoized
bool get isBuiltValue =>
element.allSupertypes
.map((type) => type.name)
.any((name) => name.startsWith('Built')) &&
!element.name.startsWith(r'_$') &&
element.fields.any((field) => field.name == 'serializer');
// TODO(davidmorgan): better check.
@memoized
bool get isEnumClass =>
element.allSupertypes
.map((type) => type.name)
.any((name) => name == 'EnumClass') &&
!element.name.startsWith(r'_$') &&
element.fields.any((field) => field.name == 'serializer');
@memoized
BuiltList<SerializerSourceField> get fields {
var result = ListBuilder<SerializerSourceField>();
for (var fieldElement in collectFields(element)) {
final builderFieldElement =
builderElement?.getField(fieldElement.displayName);
final sourceField = SerializerSourceField(
builtValueSettings, parsedLibrary, fieldElement, builderFieldElement);
if (sourceField.isSerializable) {
result.add(sourceField);
}
}
return result.build();
}
/// Returns all the serializable classes used, transitively, by fields of
/// this class.
@memoized
BuiltSet<SerializerSourceClass> get fieldClasses =>
_fieldClassesWith(BuiltSet<SerializerSourceClass>());
/// Returns all the serializable classes used, transitively, by fields of
/// this class.
///
/// [initialClasses] may be empty, or may include classes that are already
/// being evaluated. These will not be re-evaluated, preventing infinite
/// recursion when there is a loop in field types. For example, when `FooA`
/// has a field of type `FooB`, and `FooB `has a field of type `FooA`.
BuiltSet<SerializerSourceClass> _fieldClassesWith(
BuiltSet<SerializerSourceClass> initialClasses) {
var result = initialClasses.toBuilder();
for (var fieldElement in collectFields(element)) {
if (fieldElement.isStatic) continue;
if (fieldElement.setter != null) continue;
// Type is not fully specified, ignore.
if (fieldElement.type.element is! ClassElement) continue;
// Also find classes used as generic parameters; for example a field
// of type List<Foo> means we need to be able to serialize Foo.
if (fieldElement.type is ParameterizedType) {
for (var type
in (fieldElement.type as ParameterizedType).typeArguments) {
// Type is not fully specified, ignore.
if (type.element is! ClassElement) continue;
final sourceClass =
SerializerSourceClass(type.element as ClassElement);
if (sourceClass != this &&
!result.build().contains(sourceClass) &&
sourceClass.isSerializable) {
result.add(sourceClass);
result.replace(sourceClass._fieldClassesWith(result.build()));
}
}
}
final sourceClass =
SerializerSourceClass(fieldElement.type.element as ClassElement);
if (sourceClass != this &&
!result.build().contains(sourceClass) &&
sourceClass.isSerializable) {
result.add(sourceClass);
result.replace(sourceClass._fieldClassesWith(result.build()));
}
}
return result.build();
}
@memoized
CompilationUnitElement get compilationUnit =>
element.library.definingCompilationUnit;
Iterable<String> computeErrors() {
var result = <String>[];
if (!serializerSettings.custom) {
final camelCaseName =
name.substring(0, 1).toLowerCase() + name.substring(1);
final expectedSerializerDeclaration =
'static Serializer<$name> get serializer => '
'_\$${camelCaseName}Serializer;';
if (serializerDeclaration != expectedSerializerDeclaration) {
result.add('Declare $name.serializer as: '
'$expectedSerializerDeclaration got $serializerDeclaration');
}
}
for (var field in fields) {
result.addAll(field.computeErrors());
}
return result;
}
@memoized
bool get isSerializable => isBuiltValue || isEnumClass;
@memoized
bool get needsGeneratedSerializer => !serializerSettings.custom;
String generateTransitiveSerializerAdder() {
return '..add($name.serializer)';
}
/// Returns a string with `addBuilderFactory` calls to add the needed builder
/// factories. Specify the [compilationUnit] the code will run in, so the
/// code can be generated correctly (with or without import prefixes).
String generateBuilderFactoryAdders(CompilationUnitElement compilationUnit) {
return fields
.where((field) =>
field.needsBuilder &&
field
.generateFullType(
compilationUnit, genericParameters.toBuiltSet())
.startsWith('const '))
.map((field) =>
'..addBuilderFactory(${field.generateFullType(compilationUnit)}, '
' () => ${field.generateBuilder()})')
.join('\n');
}
String generateSerializerDeclaration() {
var camelCaseName = _toCamelCase(name);
return 'Serializer<$name> '
'_\$${camelCaseName}Serializer = '
'new _\$${name}Serializer();';
}
String generateSerializer() {
if (isBuiltValue) {
return '''
class _\$${name}Serializer implements StructuredSerializer<$name> {
@override
final Iterable<Type> types = const [$name, _\$$name];
@override
final String wireName = '${escapeString(wireName)}';
@override
Iterable<Object> serialize(Serializers serializers, $name object,
{FullType specifiedType = FullType.unspecified}) {
${fields.isEmpty ? 'return <Object>[];' : '''
${_generateGenericsSerializerPreamble()}
final result = <Object>[${_generateRequiredFieldSerializers()}];
${_generateNullableFieldSerializers()}
return result;
'''}
}
@override
$name deserialize(Serializers serializers, Iterable<Object> serialized,
{FullType specifiedType = FullType.unspecified}) {
${_generateGenericsSerializerPreamble()}
${fields.isEmpty ? 'return ${_generateNewBuilder()}.build();' : '''
final result = ${_generateNewBuilder()};
final iterator = serialized.iterator;
while (iterator.moveNext()) {
final key = iterator.current as String;
iterator.moveNext();
final dynamic value = iterator.current;
${serializerSettings.serializeNulls ? 'if (value == null) continue;' : ''}'''
'''switch (key) {
${_generateFieldDeserializers()}
}
}
return result.build();
'''}
}
}
''';
} else if (isEnumClass) {
final wireNameMapping = BuiltMap<String, String>.build((b) => element
.fields
.where((field) => field.isConst && field.isStatic)
.forEach((field) {
final enumSourceField = EnumSourceField(parsedLibrary, field);
if (enumSourceField.settings.wireName != null) {
b[field.name] = enumSourceField.settings.wireName;
}
}));
if (wireNameMapping.isEmpty) {
// No wire names. Just use the enum names directly.
return '''
class _\$${name}Serializer implements PrimitiveSerializer<$name> {
@override
final Iterable<Type> types = const <Type>[$name];
@override
final String wireName = '${escapeString(wireName)}';
@override
Object serialize(Serializers serializers, $name object,
{FullType specifiedType = FullType.unspecified}) =>
object.name;
@override
$name deserialize(Serializers serializers, Object serialized,
{FullType specifiedType = FullType.unspecified}) =>
$name.valueOf(serialized as String);
}''';
} else {
// Generate maps between enum names and wire names.
final toWire = '''
static const Map<String, String> _toWire = const <String, String>{
${wireNameMapping.keys.map((key) => "'$key': '${wireNameMapping[key]}',").join('\n')}
};''';
final fromWire = '''
static const Map<String, String> _fromWire = const <String, String>{
${wireNameMapping.keys.map((key) => "'${wireNameMapping[key]}': '$key',").join('\n')}
};''';
return '''
class _\$${name}Serializer implements PrimitiveSerializer<$name> {
$toWire
$fromWire
@override
final Iterable<Type> types = const <Type>[$name];
@override
final String wireName = '${escapeString(wireName)}';
@override
Object serialize(Serializers serializers, $name object,
{FullType specifiedType = FullType.unspecified}) =>
_toWire[object.name] ?? object.name;
@override
$name deserialize(Serializers serializers, Object serialized,
{FullType specifiedType = FullType.unspecified}) =>
$name.valueOf(_fromWire[serialized] ?? serialized as String);
}''';
}
} else {
throw UnsupportedError('not serializable');
}
}
String _generateNewBuilder() {
var parameters = _genericParametersUsedInFields;
if (parameters.isEmpty) return 'new ${name}Builder()';
var boundsOrObject = genericBounds
.map((bound) => bound.isEmpty ? 'Object' : bound)
.join(', ');
return 'isUnderspecified ? '
'new ${name}Builder<$boundsOrObject>() : '
'serializers.newBuilder(specifiedType) as ${name}Builder';
}
BuiltList<String> get _genericParametersUsedInFields => BuiltList<String>(
genericParameters.where((parameter) => fields.any((field) =>
field.rawType == parameter ||
field.type.contains(RegExp('[<, \n]$parameter[>, \n]')))));
String _generateGenericsSerializerPreamble() {
var parameters = _genericParametersUsedInFields;
if (parameters.isEmpty) return '';
var result = StringBuffer();
result.writeln('final isUnderspecified = specifiedType.isUnspecified || '
'specifiedType.parameters.isEmpty;');
result.writeln(
'if (!isUnderspecified) serializers.expectBuilder(specifiedType);');
for (var parameter in parameters) {
final index = genericParameters.indexOf(parameter);
result.writeln('final parameter$parameter = '
'isUnderspecified '
'? FullType.object : '
'specifiedType.parameters[$index];');
}
result.writeln();
return result.toString();
}
String _generateRequiredFieldSerializers() {
return fields
.where((field) => !field.isNullable)
.map((field) => "'${escapeString(field.wireName)}', "
'serializers.serialize(object.${field.name}, '
'specifiedType: '
'${field.generateFullType(compilationUnit, genericParameters.toBuiltSet())}),')
.join('');
}
String _generateNullableFieldSerializers() {
return fields.where((field) => field.isNullable).map((field) {
var serializeField = '''serializers.serialize(
object.${field.name},
specifiedType:
${field.generateFullType(compilationUnit, genericParameters.toBuiltSet())})''';
// By default, omit nulls; but if we were asked to include nulls, just
// write them.
if (serializerSettings.serializeNulls) {
return '''
result.add('${escapeString(field.wireName)}');
if (object.${field.name} == null) {
result.add(null);
} else {
result.add($serializeField);
}''';
} else {
return '''
if (object.${field.name} != null) {
result
..add('${escapeString(field.wireName)}')
..add($serializeField);
}''';
}
}).join('');
}
/// Gets a map from generic parameter to its bound.
///
/// 'Object' is substituted where there is no bound.
BuiltMap<String, String> get _genericBoundsAsMap {
var genericBoundsOrObject =
genericBounds.map((bound) => bound.isEmpty ? 'Object' : bound).toList();
var result = MapBuilder<String, String>();
for (var i = 0; i != genericParameters.length; ++i) {
result[genericParameters[i]] = genericBoundsOrObject[i];
}
return result.build();
}
String _generateFieldDeserializers() {
return fields.map((field) {
final fullType = field.generateFullType(
compilationUnit, genericParameters.toBuiltSet());
final cast = field.generateCast(compilationUnit, _genericBoundsAsMap);
if (field.builderFieldUsesNestedBuilder) {
return '''
case '${escapeString(field.wireName)}':
result.${field.name}.replace(serializers.deserialize(
value, specifiedType: $fullType) $cast);
break;
''';
} else {
return '''
case '${escapeString(field.wireName)}':
result.${field.name} = serializers.deserialize(
value, specifiedType: $fullType) $cast;
break;
''';
}
}).join('');
}
static String _toCamelCase(String name) {
var result = '';
var upperCase = false;
var firstCharacter = true;
for (var char in name.split('')) {
if (char == '_') {
upperCase = true;
} else {
result += firstCharacter
? char.toLowerCase()
: (upperCase ? char.toUpperCase() : char);
upperCase = false;
firstCharacter = false;
}
}
return result;
}
}