| // Copyright (c) 2016, 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/ast/ast.dart'; |
| import 'package:analyzer/dart/element/element.dart'; |
| import 'package:built_collection/built_collection.dart'; |
| import 'package:built_value/built_value.dart'; |
| import 'package:built_value_generator/src/fixes.dart'; |
| import 'package:built_value_generator/src/memoized_getter.dart'; |
| import 'package:built_value_generator/src/value_source_field.dart'; |
| import 'package:quiver/iterables.dart'; |
| import 'package:source_gen/source_gen.dart'; |
| |
| import 'dart_types.dart'; |
| |
| part 'value_source_class.g.dart'; |
| |
| const String _importWithSingleQuotes = |
| "import 'package:built_value/built_value.dart'"; |
| const String _importWithDoubleQuotes = |
| 'import "package:built_value/built_value.dart"'; |
| |
| abstract class ValueSourceClass |
| implements Built<ValueSourceClass, ValueSourceClassBuilder> { |
| ClassElement get element; |
| |
| factory ValueSourceClass(ClassElement element) => |
| _$ValueSourceClass._(element: element); |
| ValueSourceClass._(); |
| |
| @memoized |
| ParsedLibraryResult get parsedLibrary => |
| element.library.session.getParsedLibraryByElement(element.library); |
| |
| @memoized |
| String get name => element.displayName; |
| |
| /// Returns the class name for the generated implementation. If the manually |
| /// maintained class is private then we ignore the underscore here, to avoid |
| /// returning a class name starting `_$_`. |
| @memoized |
| String get implName => |
| name.startsWith('_') ? '_\$${name.substring(1)}' : '_\$$name'; |
| |
| @memoized |
| ClassElement get builderElement { |
| var result = element.library.getType(name + 'Builder'); |
| if (result == null) return null; |
| // If the builder is in a generated file, then we're analyzing _after_ code |
| // generation. Ignore it. This happens when running as an analyzer plugin. |
| if (result.source.fullName.endsWith('.g.dart')) return null; |
| return result; |
| } |
| |
| @memoized |
| bool get implementsBuilt => element.allSupertypes |
| .any((interfaceType) => interfaceType.name == 'Built'); |
| |
| @memoized |
| bool get extendsIsAllowed { |
| // Usually `extends` is not allowed. But, allow one special case: |
| // |
| // A `built_value` class may share code with a `const` class by extending |
| // a `const` base class. There's no other way to do this sharing because |
| // a `const` class is not allowed to use a mixin. |
| // |
| // To avoid causing problems for `built_value` the base class must be |
| // abstract, must have no fields, must have no abstract getters, and |
| // must not implement `operator==`, `hashCode` or `toString`. |
| // This means it _is_ allowed to have concrete getters as well as |
| // concrete and abstract methods. |
| |
| for (var supertype in [element.supertype] |
| ..addAll(element.supertype.element.allSupertypes)) { |
| if (DartTypes.getName(supertype) == 'Object') continue; |
| |
| // Base class must be abstract. |
| if (!supertype.element.isAbstract) return false; |
| |
| // Base class must have no fields. |
| if (supertype.element.fields |
| .any((field) => !field.isStatic && !field.isSynthetic)) { |
| return false; |
| } |
| |
| // Base class must have no abstract getters. |
| if (supertype.accessors.any((accessor) => |
| !accessor.isStatic && accessor.isGetter && accessor.isAbstract)) { |
| return false; |
| } |
| |
| // Base class must not implement operator==, hashCode or toString. |
| if (supertype.element.getMethod('hashCode') != null) return false; |
| if (supertype.element.getMethod('==') != null) return false; |
| if (supertype.element.getMethod('toString') != null) return false; |
| } |
| |
| return true; |
| } |
| |
| @memoized |
| BuiltValue get settings { |
| var annotations = element.metadata |
| .map((annotation) => annotation.computeConstantValue()) |
| .where((value) => DartTypes.getName(value?.type) == 'BuiltValue'); |
| if (annotations.isEmpty) return const BuiltValue(); |
| var annotation = annotations.single; |
| // If a field does not exist, that means an old `built_value` version; use |
| // the default. |
| return BuiltValue( |
| comparableBuilders: |
| annotation.getField('comparableBuilders')?.toBoolValue() ?? false, |
| instantiable: |
| annotation.getField('instantiable')?.toBoolValue() ?? true, |
| nestedBuilders: |
| annotation.getField('nestedBuilders')?.toBoolValue() ?? true, |
| autoCreateNestedBuilders: |
| annotation.getField('autoCreateNestedBuilders')?.toBoolValue() ?? |
| true, |
| generateBuilderOnSetField: |
| annotation.getField('generateBuilderOnSetField')?.toBoolValue() ?? |
| false, |
| defaultCompare: |
| annotation.getField('defaultCompare')?.toBoolValue() ?? true, |
| defaultSerialize: |
| annotation.getField('defaultSerialize')?.toBoolValue() ?? true, |
| wireName: annotation.getField('wireName')?.toStringValue()); |
| } |
| |
| @memoized |
| BuiltList<String> get genericParameters => |
| BuiltList<String>(element.typeParameters.map((e) => e.name)); |
| |
| @memoized |
| BuiltList<String> get genericBounds => |
| BuiltList<String>(element.typeParameters.map((element) { |
| var bound = element.bound; |
| if (bound == null) return ''; |
| return DartTypes.getName(bound); |
| })); |
| |
| @memoized |
| ClassDeclaration get classDeclaration { |
| return parsedLibrary.getElementDeclaration(element).node |
| as ClassDeclaration; |
| } |
| |
| @memoized |
| bool get hasBuilder => builderElement != null; |
| |
| @memoized |
| bool get hasBuilderInitializer => builderInitializer != null; |
| |
| @memoized |
| MethodElement get builderInitializer => |
| element.getMethod('_initializeBuilder'); |
| |
| @memoized |
| bool get hasBuilderFinalizer => builderFinalizer != null; |
| |
| @memoized |
| MethodElement get builderFinalizer => element.getMethod('_finalizeBuilder'); |
| |
| @memoized |
| String get builderParameters { |
| return builderElement.allSupertypes |
| .where((interfaceType) => interfaceType.name == 'Builder') |
| .single |
| .typeArguments |
| .map((type) => DartTypes.getName(type)) |
| .join(', '); |
| } |
| |
| @memoized |
| BuiltList<ValueSourceField> get fields => ValueSourceField.fromClassElements( |
| settings, parsedLibrary, element, builderElement); |
| |
| @memoized |
| String get source => |
| element.library.definingCompilationUnit.source.contents.data; |
| |
| @memoized |
| String get partStatement { |
| var fileName = element.library.source.shortName.replaceAll('.dart', ''); |
| return "part '$fileName.g.dart';"; |
| } |
| |
| @memoized |
| bool get hasPartStatement { |
| var expectedCode = partStatement; |
| return source.contains(expectedCode); |
| } |
| |
| @memoized |
| bool get hasBuiltValueImportWithShow { |
| // It would be more accurate to check using the AST, but this is |
| // potentially expensive. We already have the source for the "part of" |
| // check, use that. |
| return source.contains('$_importWithSingleQuotes show') || |
| source.contains('$_importWithDoubleQuotes show'); |
| } |
| |
| @memoized |
| bool get hasBuiltValueImportWithAs { |
| // It would be more accurate to check using the AST, but this is |
| // potentially expensive. We already have the source for the "part of" |
| // check, use that. |
| return source.contains('$_importWithSingleQuotes as') || |
| source.contains('$_importWithDoubleQuotes as'); |
| } |
| |
| @memoized |
| bool get valueClassIsAbstract => element.isAbstract; |
| |
| @memoized |
| BuiltList<ConstructorDeclaration> get valueClassConstructors => |
| BuiltList<ConstructorDeclaration>(element.constructors |
| .where((constructor) => |
| !constructor.isFactory && !constructor.isSynthetic) |
| .map((constructor) => |
| parsedLibrary.getElementDeclaration(constructor).node)); |
| |
| @memoized |
| BuiltList<ConstructorDeclaration> get valueClassFactories => |
| BuiltList<ConstructorDeclaration>(element.constructors |
| .where((constructor) => constructor.isFactory) |
| .map((factory) => parsedLibrary.getElementDeclaration(factory).node)); |
| |
| @memoized |
| bool get builderClassIsAbstract => builderElement.isAbstract; |
| |
| @memoized |
| BuiltList<String> get builderClassConstructors => |
| BuiltList<String>(builderElement.constructors |
| .where((constructor) => |
| !constructor.isFactory && !constructor.isSynthetic) |
| .map((constructor) => parsedLibrary |
| .getElementDeclaration(constructor) |
| .node |
| .toSource())); |
| |
| @memoized |
| BuiltList<String> get builderClassFactories => |
| BuiltList<String>(builderElement.constructors |
| .where((constructor) => constructor.isFactory) |
| .map((factory) => |
| parsedLibrary.getElementDeclaration(factory).node.toSource())); |
| |
| @memoized |
| BuiltList<MemoizedGetter> get memoizedGetters => |
| BuiltList<MemoizedGetter>(MemoizedGetter.fromClassElement(element)); |
| |
| /// Returns the `implements` clause for the builder. |
| /// |
| /// All builders implement `Builder`. |
| /// |
| /// Additionally, if the value class implements other value classes, the |
| /// builder implements the corresponding builders. |
| @memoized |
| BuiltList<String> get builderImplements => BuiltList<String>.build((b) => b |
| ..add('Builder<$name$_generics, ${name}Builder$_generics>') |
| ..addAll(element.interfaces |
| .where((interface) => needsBuiltValue(interface.element)) |
| .map((interface) { |
| final displayName = DartTypes.getName(interface); |
| if (!displayName.contains('<')) return displayName + 'Builder'; |
| final index = displayName.indexOf('<'); |
| return displayName.substring(0, index) + |
| 'Builder' + |
| displayName.substring(index); |
| }))); |
| |
| @memoized |
| bool get implementsHashCode => element.getGetter('hashCode') != null; |
| |
| @memoized |
| bool get implementsOperatorEquals => element.getMethod('==') != null; |
| |
| @memoized |
| bool get implementsToString { |
| // Check for any `toString` implementation apart from the one defined on |
| // `Object`. |
| var method = element.lookUpConcreteMethod('toString', element.library); |
| var clazz = method.enclosingElement; |
| return clazz is! ClassElement || clazz.name != 'Object'; |
| } |
| |
| @memoized |
| CompilationUnitElement get compilationUnit => |
| element.library.definingCompilationUnit; |
| |
| static bool needsBuiltValue(ClassElement classElement) { |
| // TODO(davidmorgan): more exact type check. |
| return !classElement.displayName.startsWith('_\$') && |
| (classElement.allSupertypes |
| .any((interfaceType) => interfaceType.name == 'Built') || |
| classElement.metadata |
| .map((annotation) => annotation.computeConstantValue()) |
| .any( |
| (value) => DartTypes.getName(value?.type) == 'BuiltValue')); |
| } |
| |
| Iterable<GeneratorError> computeErrors() { |
| return concat([ |
| _checkPart(), |
| _checkSettings(), |
| _checkValueClass(), |
| _checkBuilderClass(), |
| _checkFieldList(), |
| concat(fields.map((field) => field.computeErrors())) |
| ]); |
| } |
| |
| Iterable<GeneratorError> _checkPart() { |
| if (hasPartStatement) return []; |
| |
| var directives = (classDeclaration.parent as CompilationUnit).directives; |
| if (directives.isEmpty) { |
| return [ |
| GeneratorError((b) => b |
| ..message = 'Import generated part: $partStatement' |
| ..offset = 0 |
| ..length = 0 |
| ..fix = '$partStatement\n\n') |
| ]; |
| } else { |
| return [ |
| GeneratorError((b) => b |
| ..message = 'Import generated part: $partStatement' |
| ..offset = directives.last.offset + directives.last.length |
| ..length = 0 |
| ..fix = '\n\n$partStatement\n\n') |
| ]; |
| } |
| } |
| |
| Iterable<GeneratorError> _checkSettings() { |
| // Not allowed to have comparable builders with nested builders; this |
| // would break comparing because the nested builders may not be comparable. |
| // (Collection builders, in particularly, are definitely not comparable). |
| if (settings.comparableBuilders && settings.nestedBuilders) { |
| return [ |
| GeneratorError((b) => b |
| ..message = 'Set `nestedBuilders: false`' |
| ' in order to use `comparableBuilders: true`.') |
| ]; |
| } else { |
| return []; |
| } |
| } |
| |
| Iterable<GeneratorError> _checkValueClass() { |
| var result = <GeneratorError>[]; |
| |
| if (!valueClassIsAbstract) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Make class abstract.' |
| ..offset = classDeclaration.offset |
| ..length = 0 |
| ..fix = 'abstract ')); |
| } |
| |
| if (hasBuiltValueImportWithShow) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Stop using "show" when importing ' |
| '"package:built_value/built_value.dart". It prevents the ' |
| 'generated code from finding helper methods.')); |
| } |
| |
| if (hasBuiltValueImportWithAs) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Stop using "as" when importing ' |
| '"package:built_value/built_value.dart". It prevents the generated ' |
| 'code from finding helper methods.')); |
| } |
| |
| var implementsClause = classDeclaration.implementsClause; |
| var expectedInterface = 'Built<$name$_generics, ${name}Builder$_generics>'; |
| |
| var implementsClauseIsCorrect = implementsClause != null && |
| implementsClause.interfaces |
| .any((type) => type.toSource() == expectedInterface); |
| |
| // Built parameters need fixing if they are not as expected, unless 1) the |
| // class is marked `@BuiltValue(instantiable: false)` and 2) there is no |
| // case of the `Built` interface being implemented. This is to allow |
| // omitting the `Built` interface to work around having to implement the |
| // same interface twice with different type parameters. |
| var implementsClauseIsAllowedToBeIncorrect = !settings.instantiable && |
| (implementsClause == null || |
| !implementsClause.interfaces.any((type) => |
| type.toSource() == 'Built' || |
| type.toSource().startsWith('Built<'))); |
| |
| if (!implementsClauseIsCorrect && !implementsClauseIsAllowedToBeIncorrect) { |
| if (implementsClause == null) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Make class implement $expectedInterface.' |
| ..offset = classDeclaration.leftBracket.offset - 1 |
| ..length = 0 |
| ..fix = 'implements $expectedInterface')); |
| } else { |
| var found = false; |
| final interfaces = implementsClause.interfaces.map((type) { |
| if (type.name.name == 'Built') { |
| found = true; |
| return expectedInterface; |
| } else { |
| return type.toSource(); |
| } |
| }).toList(); |
| if (!found) interfaces.add(expectedInterface); |
| |
| result.add(GeneratorError((b) => b |
| ..message = 'Make class implement $expectedInterface.' |
| ..offset = implementsClause.offset |
| ..length = implementsClause.length |
| ..fix = 'implements ${interfaces.join(", ")}')); |
| } |
| } |
| |
| if (!extendsIsAllowed) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Stop class extending other classes. ' |
| 'Only "implements" and "extends Object with" are allowed.')); |
| } |
| |
| if (settings.instantiable) { |
| if (hasBuilderInitializer) { |
| if (!builderInitializer.isStatic || |
| !builderInitializer.returnType.isVoid || |
| builderInitializer.parameters.length != 1 || |
| parsedLibrary |
| .getElementDeclaration(builderInitializer.parameters[0]) |
| .node is! SimpleFormalParameter || |
| DartTypes.stripGenerics((parsedLibrary |
| .getElementDeclaration(builderInitializer.parameters[0]) |
| .node as SimpleFormalParameter) |
| .type |
| ?.toSource()) != |
| '${name}Builder') { |
| result.add(GeneratorError((b) => b |
| ..message = 'Fix _initializeBuilder signature: ' |
| 'static void _initializeBuilder(${name}Builder b)')); |
| } |
| } |
| if (hasBuilderFinalizer) { |
| if (!builderFinalizer.isStatic || |
| !builderFinalizer.returnType.isVoid || |
| builderFinalizer.parameters.length != 1 || |
| parsedLibrary |
| .getElementDeclaration(builderFinalizer.parameters[0]) |
| .node is! SimpleFormalParameter || |
| DartTypes.stripGenerics((parsedLibrary |
| .getElementDeclaration(builderFinalizer.parameters[0]) |
| .node as SimpleFormalParameter) |
| .type |
| ?.toSource()) != |
| '${name}Builder') { |
| result.add(GeneratorError((b) => b |
| ..message = 'Fix _finalizeBuilder signature: ' |
| 'static void _finalizeBuilder(${name}Builder b)')); |
| } |
| } |
| |
| final expectedConstructor = '$name._()'; |
| if (valueClassConstructors.isEmpty) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make class have exactly one constructor: $expectedConstructor;' |
| ..offset = classDeclaration.rightBracket.offset |
| ..length = 0 |
| ..fix = ' $expectedConstructor;\n')); |
| } else if (valueClassConstructors.length > 1) { |
| var found = false; |
| for (var constructor in valueClassConstructors) { |
| if (constructor.toSource().startsWith(expectedConstructor)) { |
| found = true; |
| } else { |
| result.add(GeneratorError((b) => b |
| ..message = 'Remove invalid constructor.' |
| ..offset = constructor.offset |
| ..length = constructor.length |
| ..fix = '')); |
| } |
| } |
| if (!found) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make class have exactly one constructor: $expectedConstructor;' |
| ..offset = classDeclaration.rightBracket.offset |
| ..length = 0 |
| ..fix = ' $expectedConstructor;\n')); |
| } |
| } else if (!(valueClassConstructors.single |
| .toSource() |
| .startsWith(expectedConstructor))) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make class have exactly one constructor: $expectedConstructor;' |
| ..offset = valueClassConstructors.single.offset |
| ..length = valueClassConstructors.single.length |
| ..fix = expectedConstructor + ';')); |
| } |
| } else { |
| if (valueClassConstructors.isNotEmpty) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Remove all constructors or remove "instantiable: false".')); |
| } |
| } |
| |
| if (settings.instantiable) { |
| if (!valueClassFactories.any( |
| (factory) => factory.toSource().contains('$implName$_generics'))) { |
| final exampleFactory = |
| 'factory $name([void Function(${name}Builder$_generics) updates]) = ' |
| '$implName$_generics;'; |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Add a factory so your class can be instantiated. Example:\n\n' |
| '$exampleFactory' |
| ..offset = classDeclaration.rightBracket.offset |
| ..length = 0 |
| ..fix = ' $exampleFactory\n')); |
| } |
| } else { |
| if (valueClassFactories.isNotEmpty) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Remove all factories or remove "instantiable: false".')); |
| } |
| } |
| |
| if (implementsHashCode) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Stop implementing hashCode; it will be generated for you.')); |
| } |
| |
| if (implementsOperatorEquals) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Stop implementing operator==; it will be generated for you.')); |
| } |
| |
| return result; |
| } |
| |
| Iterable<GeneratorError> _checkBuilderClass() { |
| var result = <GeneratorError>[]; |
| if (!hasBuilder) return result; |
| |
| if (!builderClassIsAbstract) { |
| result.add( |
| GeneratorError((b) => b..message = 'Make builder class abstract.')); |
| } |
| |
| if (settings.instantiable) { |
| final expectedBuilderParameters = |
| '$name$_generics, ${name}Builder$_generics'; |
| if (builderParameters != expectedBuilderParameters) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make builder class implement Builder<$expectedBuilderParameters>. ' |
| 'Currently: Builder<$builderParameters>')); |
| } |
| } |
| |
| if (settings.instantiable) { |
| final expectedConstructor = '${name}Builder._()'; |
| if (builderClassConstructors.length != 1 || |
| !(builderClassConstructors.single.startsWith(expectedConstructor))) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make builder class have exactly one constructor: $expectedConstructor;')); |
| } |
| } else { |
| if (builderClassConstructors.isNotEmpty) { |
| result.add(GeneratorError((b) => b |
| ..message = 'Remove all builder constructors ' |
| 'or remove "instantiable: false".')); |
| } |
| } |
| |
| if (settings.instantiable) { |
| final expectedFactory = |
| 'factory ${name}Builder() = _\$${name}Builder$_generics;'; |
| if (builderClassFactories.length != 1 || |
| builderClassFactories.single != expectedFactory) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Make builder class have exactly one factory: $expectedFactory')); |
| } |
| } else { |
| if (builderClassFactories.isNotEmpty) { |
| result.add(GeneratorError((b) => b |
| ..message = |
| 'Remove all builder factories or remove "instantiable: false".')); |
| } |
| } |
| |
| return result; |
| } |
| |
| Iterable<GeneratorError> _checkFieldList() { |
| if (!hasBuilder || !settings.instantiable) return <GeneratorError>[]; |
| return fields.any((field) => !field.builderFieldExists) |
| ? [ |
| GeneratorError((b) => b |
| ..message = 'Make builder have exactly these fields: ' + |
| fields.map((field) => field.name).join(', ')) |
| ] |
| : []; |
| } |
| |
| String get _generics => |
| genericParameters.isEmpty ? '' : '<' + genericParameters.join(', ') + '>'; |
| |
| String get _boundedGenerics => genericParameters.isEmpty |
| ? '' |
| : '<' + |
| zip(<Iterable>[genericParameters, genericBounds]).map((zipped) { |
| final parameter = zipped[0] as String; |
| final bound = zipped[1] as String; |
| return bound.isEmpty ? parameter : '$parameter extends $bound'; |
| }).join(', ') + |
| '>'; |
| |
| String generateCode() { |
| var errors = computeErrors(); |
| if (errors.isNotEmpty) throw _makeError(errors); |
| |
| var result = StringBuffer(); |
| if (settings.instantiable) result.write(_generateImpl()); |
| if (settings.instantiable) { |
| result.write(_generateBuilder()); |
| } else if (!hasBuilder) { |
| result.write(_generateAbstractBuilder()); |
| } |
| return result.toString(); |
| } |
| |
| /// Generates the value class implementation. |
| String _generateImpl() { |
| var result = StringBuffer(); |
| result.writeln('class $implName$_boundedGenerics ' |
| 'extends $name$_generics {'); |
| for (var field in fields) { |
| final type = field.typeInCompilationUnit(compilationUnit); |
| result.writeln('@override'); |
| result.writeln('final $type ${field.name};'); |
| } |
| for (var memoizedGetter in memoizedGetters) { |
| result.writeln('${memoizedGetter.returnType} __${memoizedGetter.name};'); |
| } |
| result.writeln(); |
| |
| // If there is a manually maintained builder we have to cast the "build()" |
| // result to the generated value class. If the builder is generated, that |
| // can return the right type directly and needs no cast. |
| var cast = hasBuilder ? 'as _\$$name$_generics' : ''; |
| result.writeln('factory $implName([' |
| 'void Function(${name}Builder$_generics) updates]) ' |
| '=> (new ${name}Builder$_generics()..update(updates)).build() $cast;'); |
| result.writeln(); |
| |
| if (fields.isEmpty) { |
| result.write('$implName._() : super._()'); |
| } else { |
| result.write('$implName._({'); |
| result.write(fields.map((field) => 'this.${field.name}').join(', ')); |
| result.write('}) : super._()'); |
| } |
| var requiredFields = fields.where((field) => !field.isNullable); |
| if (requiredFields.isEmpty && genericParameters.isEmpty) { |
| result.writeln(';'); |
| } else { |
| result.writeln('{'); |
| for (var field in requiredFields) { |
| result.writeln('if (${field.name} == null) {'); |
| result.writeln( |
| "throw new BuiltValueNullFieldError('$name', '${field.name}');"); |
| result.writeln('}'); |
| } |
| // If there are generic parameters, check they are not "dynamic". |
| if (genericParameters.isNotEmpty) { |
| for (var genericParameter in genericParameters) { |
| result.writeln('if ($genericParameter == dynamic) {'); |
| result.writeln('throw new BuiltValueMissingGenericsError(' |
| "'$name', '$genericParameter');"); |
| result.writeln('}'); |
| } |
| } |
| result.writeln(); |
| result.writeln('}'); |
| } |
| result.writeln(); |
| |
| for (var memoizedGetter in memoizedGetters) { |
| result.writeln('@override'); |
| result.writeln( |
| '${memoizedGetter.returnType} get ${memoizedGetter.name} =>'); |
| result.writeln( |
| '__${memoizedGetter.name} ??= super.${memoizedGetter.name};'); |
| result.writeln(); |
| } |
| |
| result.writeln('@override'); |
| result.writeln( |
| '$name$_generics rebuild(void Function(${name}Builder$_generics) updates) ' |
| '=> (toBuilder()..update(updates)).build();'); |
| result.writeln(); |
| |
| result.writeln('@override'); |
| if (hasBuilder) { |
| result.writeln('${implName}Builder$_generics toBuilder() ' |
| '=> new ${implName}Builder$_generics()..replace(this);'); |
| } else { |
| result.writeln('${name}Builder$_generics toBuilder() ' |
| '=> new ${name}Builder$_generics()..replace(this);'); |
| } |
| result.writeln(); |
| |
| result.write(_generateEqualsAndHashcode()); |
| |
| // Only generate toString() if there wasn't one already. |
| if (!implementsToString) { |
| result.writeln('@override'); |
| result.writeln('String toString() {'); |
| if (fields.isEmpty) { |
| result |
| .writeln("return newBuiltValueToStringHelper('$name').toString();"); |
| } else { |
| result.writeln("return (newBuiltValueToStringHelper('$name')"); |
| result.writeln(fields |
| .map((field) => "..add('${field.name}', ${field.name})") |
| .join('')); |
| result.writeln(").toString();"); |
| } |
| result.writeln('}'); |
| result.writeln(); |
| } |
| |
| result.writeln('}'); |
| return result.toString(); |
| } |
| |
| /// Generates the builder implementation. |
| String _generateBuilder() { |
| var result = StringBuffer(); |
| if (hasBuilder) { |
| result.writeln('class ${implName}Builder$_boundedGenerics ' |
| 'extends ${name}Builder$_generics {'); |
| } else { |
| result.writeln('class ${name}Builder$_boundedGenerics ' |
| 'implements ${builderImplements.join(", ")} {'); |
| } |
| |
| // Builder holds a reference to a value, copies from it lazily. |
| result.writeln('$implName$_generics _\$v;'); |
| result.writeln(''); |
| |
| if (hasBuilder) { |
| for (var field in fields) { |
| final type = field.typeInCompilationUnit(compilationUnit); |
| final typeInBuilder = field.builderElementTypeWithPrefix; |
| final name = field.name; |
| |
| if (field.isNestedBuilder) { |
| result.writeln('@override'); |
| result.writeln('$typeInBuilder get $name {' |
| '_\$this;'); |
| if (settings.autoCreateNestedBuilders) { |
| result.writeln('return super.$name ??= new $typeInBuilder();'); |
| } else { |
| result.writeln('return super.$name;'); |
| } |
| result.writeln('}'); |
| result.writeln('@override'); |
| result.writeln('set $name($typeInBuilder $name) {' |
| '_\$this;' |
| 'super.$name = $name;' |
| '}'); |
| } else { |
| result.writeln('@override'); |
| result.writeln('$typeInBuilder get $name {' |
| '_\$this;' |
| 'return super.$name;' |
| '}'); |
| result.writeln('@override'); |
| result.writeln('set $name($type $name) {' |
| '_\$this;' |
| 'super.$name = $name;' |
| '}'); |
| } |
| result.writeln(); |
| } |
| } else { |
| if (settings.generateBuilderOnSetField) { |
| result.writeln('void Function() onSet = () {};'); |
| result.writeln(); |
| } |
| |
| for (var field in fields) { |
| var type = field.typeInCompilationUnit(compilationUnit); |
| var typeInBuilder = field.typeInBuilder(compilationUnit); |
| var fieldType = field.isNestedBuilder ? typeInBuilder : type; |
| var name = field.name; |
| |
| // Field. |
| result.writeln('$fieldType _$name;'); |
| |
| // Getter. |
| result.writeln('$fieldType get $name =>'); |
| if (field.isNestedBuilder && settings.autoCreateNestedBuilders) { |
| result.writeln('_\$this._$name ??= new $typeInBuilder();'); |
| } else { |
| result.writeln('_\$this._$name;'); |
| } |
| |
| // Setter. |
| if (settings.generateBuilderOnSetField) { |
| result.writeln('set $name($fieldType $name) {' |
| '_\$this._$name = $name;' |
| 'onSet();' |
| '}'); |
| } else { |
| result.writeln('set $name($fieldType $name) =>' |
| '_\$this._$name = $name;'); |
| } |
| |
| result.writeln(); |
| } |
| } |
| result.writeln(); |
| |
| if (hasBuilder) { |
| result.writeln('${implName}Builder() : super._()'); |
| } else { |
| result.writeln('${name}Builder()'); |
| } |
| if (hasBuilderInitializer) { |
| result.writeln('{'); |
| result.writeln('$name._initializeBuilder(this);'); |
| result.writeln('}'); |
| } else { |
| result.writeln(';'); |
| } |
| result.writeln(''); |
| |
| // Getter for "this" that does lazy copying if needed. |
| if (fields.isNotEmpty) { |
| result.writeln('${name}Builder$_generics get _\$this {'); |
| result.writeln('if (_\$v != null) {'); |
| for (var field in fields) { |
| final name = field.name; |
| final nameInBuilder = hasBuilder ? 'super.$name' : '_$name'; |
| if (field.isNestedBuilder) { |
| result.writeln('$nameInBuilder = _\$v.$name?.toBuilder();'); |
| } else { |
| result.writeln('$nameInBuilder = _\$v.$name;'); |
| } |
| } |
| result.writeln('_\$v = null;'); |
| result.writeln('}'); |
| result.writeln('return this;'); |
| result.writeln('}'); |
| } |
| |
| result.writeln('@override'); |
| if (builderImplements.length > 1) { |
| // If we're overriding `replace` from other builders, tell the analyzer |
| // that this builder only accepts values of exactly the right type, by |
| // marking the value `covariant`. |
| |
| if (builderImplements.length > 2) { |
| // Add this `ignore` as a workaround for analyzer issue: |
| // https://github.com/dart-lang/sdk/issues/32025 |
| result.writeln('// ignore: override_on_non_overriding_method'); |
| } |
| result.writeln('void replace(covariant $name$_generics other) {'); |
| } else { |
| result.writeln('void replace($name$_generics other) {'); |
| } |
| |
| result.writeln('if (other == null) {'); |
| result.writeln("throw new ArgumentError.notNull('other');"); |
| result.writeln('}'); |
| result.writeln('_\$v = other as $implName$_generics;'); |
| result.writeln('}'); |
| |
| result.writeln('@override'); |
| result.writeln( |
| 'void update(void Function(${name}Builder$_generics) updates) {' |
| ' if (updates != null) updates(this); }'); |
| result.writeln(); |
| |
| result.writeln('@override'); |
| result.writeln('$implName$_generics build() {'); |
| |
| if (hasBuilderFinalizer) { |
| result.writeln('$name._finalizeBuilder(this);'); |
| } |
| |
| // Construct a map from field to how it's built. If it's a normal field, |
| // this is just the field name; if it's a nested builder, this is an |
| // invocation of the nested builder taking into account nullability. |
| var fieldBuilders = <String, String>{}; |
| fields.forEach((field) { |
| final name = field.name; |
| if (!field.isNestedBuilder) { |
| fieldBuilders[name] = name; |
| } else if (!field.isNullable) { |
| // If not nullable, go via the public accessor, which instantiates |
| // if needed. |
| fieldBuilders[name] = '$name.build()'; |
| } else if (hasBuilder) { |
| // Otherwise access the private field: in super if there's a manually |
| // maintained builder. |
| fieldBuilders[name] = 'super.$name?.build()'; |
| } else { |
| // Or, directly if there is no manually maintained builder. |
| fieldBuilders[name] = '_$name?.build()'; |
| } |
| }); |
| |
| // If there are nested builders then wrap the build in a try/catch so we |
| // can add information should a nested builder fail. |
| var needsTryCatchOnBuild = |
| fieldBuilders.keys.any((field) => fieldBuilders[field] != field); |
| |
| if (needsTryCatchOnBuild) { |
| result.writeln('$implName$_generics _\$result;'); |
| result.writeln('try {'); |
| } else { |
| result.write('final '); |
| } |
| result.writeln('_\$result = _\$v ?? '); |
| result.writeln('new $implName$_generics._('); |
| result.write(fieldBuilders.keys |
| .map((field) => '$field: ${fieldBuilders[field]}') |
| .join(',')); |
| result.writeln(');'); |
| |
| if (needsTryCatchOnBuild) { |
| // Handle errors by re-running all nested builders; if there's an error |
| // in a nested builder then throw with more information. Otherwise, |
| // just rethrow. |
| result.writeln('} catch (_) {'); |
| result.writeln('String _\$failedField;'); |
| result.writeln('try {'); |
| result.write(fieldBuilders.keys.map((field) { |
| final fieldBuilder = fieldBuilders[field]; |
| if (fieldBuilder == field) return ''; |
| return "_\$failedField = '$field'; $fieldBuilder;"; |
| }).join('\n')); |
| |
| result.writeln('} catch (e) {'); |
| result.writeln('throw new BuiltValueNestedFieldError(' |
| "'$name', _\$failedField, e.toString());"); |
| result.writeln('}'); |
| result.writeln('rethrow;'); |
| result.writeln('}'); |
| } |
| |
| // Set _$v to the built value, so it will be lazily copied if needed. |
| result.writeln('replace(_\$result);'); |
| result.writeln('return _\$result;'); |
| result.writeln('}'); |
| |
| if (settings.comparableBuilders) { |
| result.write(_generateEqualsAndHashcode(forBuilder: true)); |
| } |
| |
| result.writeln('}'); |
| |
| return result.toString(); |
| } |
| |
| String _generateEqualsAndHashcode({bool forBuilder = false}) { |
| var result = StringBuffer(); |
| |
| var comparedFields = fields |
| .where( |
| (field) => field.builtValueField.compare ?? settings.defaultCompare) |
| .toList(); |
| var comparedFunctionFields = |
| comparedFields.where((field) => field.isFunctionType).toList(); |
| result.writeln('@override'); |
| result.writeln('bool operator==(Object other) {'); |
| result.writeln(' if (identical(other, this)) return true;'); |
| |
| if (comparedFunctionFields.isNotEmpty) { |
| result.writeln(' final _\$dynamicOther = other as dynamic;'); |
| } |
| result.writeln(' return other is $name${forBuilder ? 'Builder' : ''}'); |
| if (comparedFields.isNotEmpty) { |
| result.writeln('&&'); |
| result.writeln(comparedFields.map((field) { |
| var nameOrThisDotName = |
| field.name == 'other' ? 'this.other' : field.name; |
| return field.isFunctionType |
| ? '$nameOrThisDotName == _\$dynamicOther.${field.name}' |
| : '$nameOrThisDotName == other.${field.name}'; |
| }).join('&&')); |
| } |
| result.writeln(';'); |
| result.writeln('}'); |
| result.writeln(); |
| |
| result.writeln('@override'); |
| result.writeln('int get hashCode {'); |
| |
| if (comparedFields.isEmpty) { |
| result.writeln('return ${name.hashCode};'); |
| } else { |
| result.writeln(r'return $jf('); |
| result.writeln(r'$jc(' * comparedFields.length); |
| // Use a different seed for builders than for values, so they do not have |
| // identical hashCodes if the values are identical. |
| result.writeln(forBuilder ? '1, ' : '0, '); |
| result.write( |
| comparedFields.map((field) => '${field.name}.hashCode').join('), ')); |
| result.writeln('));'); |
| } |
| result.writeln('}'); |
| result.writeln(); |
| |
| return result.toString(); |
| } |
| |
| /// Generates an abstract builder with just abstract setters and getters. |
| String _generateAbstractBuilder() { |
| var result = StringBuffer(); |
| |
| if (implementsBuilt) { |
| result.writeln('abstract class ${name}Builder$_boundedGenerics ' |
| 'implements ${builderImplements.join(", ")} {'); |
| } else { |
| // The "Built" interface has been omitted to work around dart2js issue |
| // https://github.com/dart-lang/sdk/issues/14729. So, we can't implement |
| // "Builder". Add the methods explicitly. We can however implement any |
| // other built_value interfaces. |
| var interfaces = builderImplements.skip(1).toList(); |
| result.writeln('abstract class ${name}Builder$_boundedGenerics ' |
| '${interfaces.isEmpty ? '' : 'implements ' + interfaces.join(', ')}' |
| '{'); |
| |
| // Add `covariant` if we're implementing one or more parent builders. |
| result.writeln('void replace(${interfaces.isEmpty ? '' : 'covariant '}' |
| '$name$_generics other);'); |
| result.writeln( |
| 'void update(void Function(${name}Builder$_generics) updates);'); |
| } |
| |
| for (var field in fields) { |
| final typeInBuilder = field.typeInBuilder(compilationUnit); |
| final name = field.name; |
| |
| result.writeln('$typeInBuilder get $name;'); |
| result.writeln('set $name($typeInBuilder $name);'); |
| result.writeln(); |
| } |
| |
| result.writeln('}'); |
| return result.toString(); |
| } |
| } |
| |
| InvalidGenerationSourceError _makeError(Iterable<GeneratorError> todos) { |
| var message = |
| StringBuffer('Please make the following changes to use BuiltValue:\n'); |
| for (var i = 0; i != todos.length; ++i) { |
| message.write('\n${i + 1}. ${todos.elementAt(i).message}'); |
| } |
| |
| return InvalidGenerationSourceError(message.toString()); |
| } |