From f3bf23deaf5d9a740bd75e127834eaf4588791e1 Mon Sep 17 00:00:00 2001 From: David Morgan Date: Fri, 8 Jul 2016 14:24:40 +0200 Subject: [PATCH] Refactor to objects. --- CHANGELOG.md | 4 + built_value/pubspec.yaml | 2 +- .../lib/built_value_generator.dart | 319 +----------------- .../lib/src/source_class.dart | 297 ++++++++++++++++ .../lib/src/source_class.g.dart | 160 +++++++++ .../lib/src/source_field.dart | 99 ++++++ .../lib/src/source_field.g.dart | 122 +++++++ built_value_generator/pubspec.yaml | 4 +- .../test/built_value_generator_test.dart | 29 ++ built_value_generator/tool/build.dart | 17 + example/pubspec.yaml | 6 +- 11 files changed, 737 insertions(+), 322 deletions(-) create mode 100644 built_value_generator/lib/src/source_class.dart create mode 100644 built_value_generator/lib/src/source_class.g.dart create mode 100644 built_value_generator/lib/src/source_field.dart create mode 100644 built_value_generator/lib/src/source_field.g.dart create mode 100644 built_value_generator/tool/build.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index a465b249..6b5770a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.1.2 + +- Refactor generator to split into logical classes. + ## 0.1.1 - Improve error output on failure to generate. diff --git a/built_value/pubspec.yaml b/built_value/pubspec.yaml index d313c834..5df1265a 100644 --- a/built_value/pubspec.yaml +++ b/built_value/pubspec.yaml @@ -1,5 +1,5 @@ name: built_value -version: 0.1.1 +version: 0.1.2 description: > Value types with builders. This library is the runtime dependency. authors: diff --git a/built_value_generator/lib/built_value_generator.dart b/built_value_generator/lib/built_value_generator.dart index abfda323..dfa58342 100644 --- a/built_value_generator/lib/built_value_generator.dart +++ b/built_value_generator/lib/built_value_generator.dart @@ -8,8 +8,7 @@ import 'dart:async'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; -import 'package:built_collection/built_collection.dart'; -import 'package:quiver/iterables.dart' show concat; +import 'package:built_value_generator/src/source_class.dart'; import 'package:source_gen/source_gen.dart'; /// Generator for Built Values. @@ -18,321 +17,9 @@ import 'package:source_gen/source_gen.dart'; class BuiltValueGenerator extends Generator { @override Future generate(Element element, BuildStep buildStep) async { - if (element is! ClassElement) { + if (element is! ClassElement || !SourceClass.needsBuiltValue(element)) { return null; } - final classElement = element as ClassElement; - final className = classElement.displayName; - - // TODO(davidmorgan): more exact type check. - if (!classElement.allSupertypes - .any((interfaceType) => interfaceType.name == 'Built')) { - return null; - } - - if (classElement.displayName.startsWith('_\$')) { - return null; - } - - final fields = getFields(classElement); - - final builderClassElement = - element.library.getType(classElement.displayName + 'Builder'); - final builderFields = getBuilderFields(builderClassElement); - final errors = concat([ - checkPart(classElement), - checkValueClass(classElement), - checkBuilderClass(className, builderClassElement), - checkFields(fields, builderFields), - ]); - - if (errors.isNotEmpty) { - throw _makeError(errors); - } - - return generateCode(className, fields, builderFields); - } - - Iterable checkPart(ClassElement classElement) { - final fileName = - classElement.library.source.shortName.replaceAll('.dart', ''); - final expectedCode = "part '$fileName.g.dart';"; - return classElement.library.source.contents.data.contains(expectedCode) - ? [] - : ['Import generated part: $expectedCode']; - } - - Iterable checkValueClass(ClassElement classElement) { - final result = []; - final name = classElement.displayName; - - if (!classElement.isAbstract) { - result.add('Make class abstract.'); - } - - final expectedConstructor = '$name._()'; - final constructors = classElement.constructors - .where((constructor) => !constructor.isFactory); - if (constructors.length != 1 || - constructors.single.isSynthetic || - !(constructors.single - .computeNode() - .toSource() - .startsWith(expectedConstructor))) { - result.add( - 'Make class have exactly one constructor: $expectedConstructor;'); - } - - final expectedFactory = - 'factory $name([updates(${name}Builder b)]) = _\$$name;'; - final factories = - classElement.constructors.where((constructor) => constructor.isFactory); - if (!factories.any( - (factory) => factory.computeNode().toSource() == expectedFactory)) { - result.add('Make class have factory: $expectedFactory'); - } - - return result; - } - - Iterable checkBuilderClass( - String className, ClassElement classElement) { - if (classElement == null) { - return ['Add abstract class: ${className}Builder']; - } - - final result = []; - final name = classElement.displayName; - - if (!classElement.isAbstract) { - result.add('Make builder class abstract'); - } - - final expectedConstructor = '$name._();'; - final constructors = classElement.constructors - .where((constructor) => !constructor.isFactory); - if (constructors.length != 1 || - constructors.single.isSynthetic || - constructors.single.computeNode().toSource() != expectedConstructor) { - result.add( - 'Make builder class have exactly one constructor: $expectedConstructor'); - } - - final expectedFactory = 'factory $name() = _\$$name;'; - final factories = - classElement.constructors.where((constructor) => constructor.isFactory); - if (factories.length != 1 || - factories.single.computeNode().toSource() != expectedFactory) { - result - .add('Make builder class have exactly one factory: $expectedFactory'); - } - - return result; - } - - List getFields(ClassElement classElement) { - final result = []; - for (final field in classElement.fields) { - if (!field.isStatic && - field.getter != null && - (field.getter.isAbstract || field.getter.isSynthetic)) { - result.add(field); - } - } - return result; - } - - List getBuilderFields(ClassElement classElement) { - final result = []; - if (classElement == null) { - return result; - } - for (final field in classElement.fields) { - if (!field.isStatic && field.getter != null) result.add(field); - } - return result; - } - - Iterable checkFields( - Iterable fields, Iterable builderFields) { - final result = []; - var checkFieldTypes = true; - - for (final field in fields) { - final fieldName = field.displayName; - if (field.getter == null || field.getter.isSynthetic) { - checkFieldTypes = false; - result.add('Make field $fieldName a getter.'); - } - } - - for (final field in builderFields) { - final fieldName = field.displayName; - if (field.getter == null || - field.getter.isAbstract || - !field.getter.isSynthetic) { - checkFieldTypes = false; - result.add('Make builder field $fieldName a normal field.'); - } - } - - final fieldNames = - new BuiltList(fields.map((field) => field.displayName)); - final builderFieldNames = - new BuiltList(builderFields.map((field) => field.displayName)); - if (fieldNames != builderFieldNames) { - result.add( - 'Make builder have exactly these fields: ' + fieldNames.join(', ')); - checkFieldTypes = false; - } - - if (checkFieldTypes) { - for (int i = 0; i != fields.length; ++i) { - final field = fields.elementAt(i); - final fieldType = field.getter.returnType.displayName; - final builderField = builderFields.elementAt(i); - final builderFieldType = builderField.getter.returnType.displayName; - if (fieldType != builderFieldType && - // TODO(davidmorgan): smarter check for builder types. - fieldType.replaceAll('Built', '') != - builderFieldType.replaceAll('Builder', '')) { - result.add( - 'Make builder field ${field.displayName} have type: $fieldType'); - } - } - } - - return result; - } - - String generateCode(String className, List fields, - List builderFields) { - final result = new StringBuffer(); - - final fieldNames = fields.map((field) => field.displayName); - - final nullableFields = fields.where((field) => field.getter.metadata.any( - (metadata) => metadata.constantValue.toStringValue() == 'nullable')); - final nullableFieldNames = nullableFields.map((field) => field.displayName); - - final buildableFields = builderFields.where( - (field) => field.getter.returnType.displayName.contains('Builder')); - final buildableFieldNames = - buildableFields.map((field) => field.displayName); - - result.writeln('class _\$$className extends $className {'); - for (final field in fields) { - final fieldName = field.displayName; - final fieldType = field.getter.returnType.displayName; - result.writeln('final $fieldType $fieldName;'); - } - - if (fieldNames.isEmpty) { - result.write('_\$$className._() : super._() {'); - } else { - result.write('_\$$className._({'); - result.write(fieldNames.map((name) => 'this.$name').join(', ')); - result.write('}) : super._() {'); - } - for (final fieldName in fieldNames) { - if (!nullableFieldNames.contains(fieldName)) { - result.writeln( - "if ($fieldName == null) throw new ArgumentError('null $fieldName');"); - } - } - result.write('}'); - - result.writeln('factory _\$$className([updates(${className}Builder b)]) ' - '=> (new ${className}Builder()..update(updates)).build();'); - result.writeln('$className rebuild(updates(${className}Builder b)) ' - '=> (toBuilder()..update(updates)).build();'); - result.writeln('_\$${className}Builder toBuilder() ' - '=> new _\$${className}Builder()..replace(this);'); - - result.writeln('bool operator==(other) {'); - result.writeln(' if (other is! $className) return false;'); - if (fields.length == 0) { - result.writeln('return true;'); - } else { - result.writeln('return'); - result.writeln(fieldNames - .map((fieldName) => '$fieldName == other.$fieldName') - .join('&&')); - result.writeln(';'); - } - result.writeln('}'); - - result.writeln('int get hashCode {'); - if (fields.length == 0) { - result.writeln('return ${className.hashCode};'); - } else { - result.writeln('return hashObjects(['); - result.writeln(fieldNames.join(', ')); - result.writeln(']);'); - } - result.writeln('}'); - - result.writeln('String toString() {'); - if (fields.length == 0) { - result.writeln("return '$className {}';"); - } else { - result.writeln("return '$className {'"); - result.writeln(fieldNames - .map((fieldName) => "'$fieldName=\${$fieldName.toString()}\\n'") - .join('')); - result.writeln("'}'"); - result.writeln(';'); - } - result.writeln('}'); - - result.writeln('}'); - - result - .writeln('class _\$${className}Builder extends ${className}Builder {'); - result.writeln(); - result.writeln('_\$${className}Builder() : super._();'); - - result.writeln('void replace(${className} other) {'); - result.writeln((fieldNames.map((name) { - return buildableFieldNames.contains(name) - ? 'super.$name = other.$name?.toBuilder();' - : 'super.$name = other.$name;'; - })) - .join('\n')); - result.writeln('}'); - - result.writeln('void update(updates(${className}Builder b)) {' - ' if (updates != null) updates(this); }'); - result.writeln('$className build() {'); - for (final field in builderFields) { - final fieldName = field.displayName; - if (!nullableFieldNames.contains(fieldName)) { - result.writeln("if ($fieldName == null) " - "throw new ArgumentError('null $fieldName');"); - } - } - result.writeln('return new _\$$className._('); - result.write(builderFields.map((field) { - final fieldName = field.displayName; - - return buildableFieldNames.contains(fieldName) - ? '$fieldName: $fieldName?.build()' - : '$fieldName: $fieldName'; - }).join(', ')); - result.write(');'); - result.writeln('}'); - result.writeln('}'); - - return result.toString(); - } - - InvalidGenerationSourceError _makeError(Iterable todos) { - final message = new StringBuffer( - 'Please make the following changes to use built_value:\n'); - for (var i = 0; i != todos.length; ++i) { - message.write('\n${i + 1}. ${todos.elementAt(i)}'); - } - - return new InvalidGenerationSourceError(message.toString()); + return new SourceClass.fromClassElement(element).generateCode(); } } diff --git a/built_value_generator/lib/src/source_class.dart b/built_value_generator/lib/src/source_class.dart new file mode 100644 index 00000000..9761404e --- /dev/null +++ b/built_value_generator/lib/src/source_class.dart @@ -0,0 +1,297 @@ +// 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/element/element.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; +import 'package:built_value_generator/src/source_field.dart'; +import 'package:quiver/iterables.dart'; +import 'package:source_gen/source_gen.dart'; + +part 'source_class.g.dart'; + +abstract class SourceClass implements Built { + String get name; + bool get hasBuilder; + BuiltList get fields; + + String get partStatement; + bool get hasPartStatement; + + bool get valueClassIsAbstract; + BuiltList get valueClassConstructors; + BuiltList get valueClassFactories; + + bool get builderClassIsAbstract; + BuiltList get builderClassConstructors; + BuiltList get builderClassFactories; + + SourceClass._(); + factory SourceClass([updates(SourceClassBuilder b)]) = _$SourceClass; + + factory SourceClass.fromClassElement(ClassElement classElement) { + final name = classElement.displayName; + final builderClassElement = classElement.library.getType(name + 'Builder'); + final hasBuilder = builderClassElement != null; + + final result = new SourceClassBuilder(); + result + ..name = name + ..hasBuilder = hasBuilder + ..partStatement = _getPartStatement(classElement) + ..hasPartStatement = _getHasPartStatement(classElement) + ..valueClassIsAbstract = classElement.isAbstract + ..valueClassConstructors.replace(classElement.constructors + .where((constructor) => + !constructor.isFactory && !constructor.isSynthetic) + .map((constructor) => constructor.computeNode().toSource())) + ..valueClassFactories.replace(classElement.constructors + .where((constructor) => constructor.isFactory) + .map((factory) => factory.computeNode().toSource())); + + if (hasBuilder) { + result + ..fields.replace( + SourceField.fromClassElements(classElement, builderClassElement)) + ..builderClassIsAbstract = builderClassElement.isAbstract + ..builderClassConstructors.replace(builderClassElement.constructors + .where((constructor) => + !constructor.isFactory && !constructor.isSynthetic) + .map((constructor) => constructor.computeNode().toSource())) + ..builderClassFactories.replace(builderClassElement.constructors + .where((constructor) => constructor.isFactory) + .map((factory) => factory.computeNode().toSource())); + } + + return result.build(); + } + + static String _getPartStatement(ClassElement classElement) { + final fileName = + classElement.library.source.shortName.replaceAll('.dart', ''); + return "part '$fileName.g.dart';"; + } + + static bool _getHasPartStatement(ClassElement classElement) { + final expectedCode = _getPartStatement(classElement); + return classElement.library.source.contents.data.contains(expectedCode); + } + + static bool needsBuiltValue(ClassElement classElement) { + // TODO(davidmorgan): more exact type check. + return classElement.allSupertypes + .any((interfaceType) => interfaceType.name == 'Built') && + !classElement.displayName.startsWith('_\$'); + } + + Iterable _computeErrors() { + return concat([ + _checkPart(), + _checkValueClass(), + _checkBuilderClass(), + _checkFieldList(), + concat(fields.map((field) => field.computeErrors())) + ]); + } + + Iterable _checkPart() { + return hasPartStatement ? [] : ['Import generated part: $partStatement']; + } + + Iterable _checkValueClass() { + final result = []; + + if (!valueClassIsAbstract) { + result.add('Make class abstract.'); + } + + final expectedConstructor = '$name._()'; + if (valueClassConstructors.length != 1 || + !(valueClassConstructors.single.startsWith(expectedConstructor))) { + result.add( + 'Make class have exactly one constructor: $expectedConstructor;'); + } + + final expectedFactory = + 'factory $name([updates(${name}Builder b)]) = _\$$name;'; + if (!valueClassFactories.any((factory) => factory == expectedFactory)) { + result.add( + 'Make class have factory: $expectedFactory -- got $valueClassFactories'); + } + + return result; + } + + Iterable _checkBuilderClass() { + final result = []; + + if (!hasBuilder) { + return ['Add abstract class: ${name}Builder']; + } + + if (!builderClassIsAbstract) { + result.add('Make builder class abstract.'); + } + + final expectedConstructor = '${name}Builder._()'; + if (builderClassConstructors.length != 1 || + !(builderClassConstructors.single.startsWith(expectedConstructor))) { + result.add( + 'Make builder class have exactly one constructor: $expectedConstructor;'); + } + + final expectedFactory = 'factory ${name}Builder() = _\$${name}Builder;'; + if (builderClassFactories.length != 1 || + builderClassFactories.single != expectedFactory) { + result + .add('Make builder class have exactly one factory: $expectedFactory'); + } + + return result; + } + + Iterable _checkFieldList() { + return fields.any((field) => !field.builderFieldExists) + ? [ + 'Make builder have exactly these fields: ' + + fields.map((field) => field.name).join(', ') + ] + : []; + } + + String generateCode() { + final errors = _computeErrors(); + if (errors.isNotEmpty) throw _makeError(errors); + + final result = new StringBuffer(); + + result.writeln('class _\$$name extends $name {'); + for (final field in fields) { + result.writeln('final ${field.type} ${field.name};'); + } + + if (fields.isEmpty) { + result.write('_\$$name._() : super._() {'); + } else { + result.write('_\$$name._({'); + result.write(fields.map((field) => 'this.${field.name}').join(', ')); + result.write('}) : super._() {'); + } + for (final field in fields.where((field) => !field.isNullable)) { + result.writeln("if (${field.name} == null) " + "throw new ArgumentError('null ${field.name}');"); + } + result.write('}'); + + result.writeln('factory _\$$name([updates(${name}Builder b)]) ' + '=> (new ${name}Builder()..update(updates)).build();'); + result.writeln('$name rebuild(updates(${name}Builder b)) ' + '=> (toBuilder()..update(updates)).build();'); + result.writeln('_\$${name}Builder toBuilder() ' + '=> new _\$${name}Builder()..replace(this);'); + + result.writeln('bool operator==(other) {'); + result.writeln(' if (other is! $name) return false;'); + if (fields.length == 0) { + result.writeln('return true;'); + } else { + result.writeln('return'); + result.writeln(fields + .map((field) => '${field.name} == other.${field.name}') + .join('&&')); + result.writeln(';'); + } + result.writeln('}'); + + result.writeln('int get hashCode {'); + if (fields.length == 0) { + result.writeln('return ${name.hashCode};'); + } else { + result.writeln('return hashObjects(['); + result.write(fields.map((field) => field.name).join(', ')); + result.writeln(']);'); + } + result.writeln('}'); + + result.writeln('String toString() {'); + if (fields.length == 0) { + result.writeln("return '$name {}';"); + } else { + result.writeln("return '$name {'"); + result.writeln(fields + .map((field) => "'${field.name}=\${${field.name}.toString()}\\n'") + .join('')); + result.writeln("'}'"); + result.writeln(';'); + } + result.writeln('}'); + + result.writeln('}'); + + result.writeln('class _\$${name}Builder extends ${name}Builder {'); + result.writeln(); + result.writeln('_\$${name}Builder() : super._();'); + + result.writeln('void replace(${name} other) {'); + result.writeln((fields.map((field) { + return field.isNestedBuilder + ? 'super.${field.name} = other.${field.name}?.toBuilder();' + : 'super.${field.name} = other.${field.name};'; + })) + .join('\n')); + result.writeln('}'); + + result.writeln('void update(updates(${name}Builder b)) {' + ' if (updates != null) updates(this); }'); + result.writeln('$name build() {'); + for (final field in fields.where((field) => !field.isNullable)) { + result.writeln("if (${field.name} == null) " + "throw new ArgumentError('null ${field.name}');"); + } + result.writeln('return new _\$$name._('); + result.write(fields.map((field) { + return field.isNestedBuilder + ? '${field.name}: ${field.name}?.build()' + : '${field.name}: ${field.name}'; + }).join(', ')); + result.write(');'); + result.writeln('}'); + result.writeln('}'); + + return result.toString(); + } +} + +abstract class SourceClassBuilder + implements Builder { + String name; + bool hasBuilder; + ListBuilder fields = new ListBuilder(); + + String partStatement; + bool hasPartStatement; + + bool valueClassIsAbstract; + ListBuilder valueClassConstructors = new ListBuilder(); + ListBuilder valueClassFactories = new ListBuilder(); + + bool builderClassIsAbstract = true; + ListBuilder builderClassConstructors = new ListBuilder(); + ListBuilder builderClassFactories = new ListBuilder(); + + SourceClassBuilder._(); + factory SourceClassBuilder() = _$SourceClassBuilder; +} + +InvalidGenerationSourceError _makeError(Iterable todos) { + final message = new 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)}'); + } + + return new InvalidGenerationSourceError(message.toString()); +} diff --git a/built_value_generator/lib/src/source_class.g.dart b/built_value_generator/lib/src/source_class.g.dart new file mode 100644 index 00000000..2e355e8b --- /dev/null +++ b/built_value_generator/lib/src/source_class.g.dart @@ -0,0 +1,160 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of built_value_generator.source_class; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class SourceClass +// ************************************************************************** + +class _$SourceClass extends SourceClass { + final String name; + final bool hasBuilder; + final BuiltList fields; + final String partStatement; + final bool hasPartStatement; + final bool valueClassIsAbstract; + final BuiltList valueClassConstructors; + final BuiltList valueClassFactories; + final bool builderClassIsAbstract; + final BuiltList builderClassConstructors; + final BuiltList builderClassFactories; + _$SourceClass._( + {this.name, + this.hasBuilder, + this.fields, + this.partStatement, + this.hasPartStatement, + this.valueClassIsAbstract, + this.valueClassConstructors, + this.valueClassFactories, + this.builderClassIsAbstract, + this.builderClassConstructors, + this.builderClassFactories}) + : super._() { + if (name == null) throw new ArgumentError('null name'); + if (hasBuilder == null) throw new ArgumentError('null hasBuilder'); + if (fields == null) throw new ArgumentError('null fields'); + if (partStatement == null) throw new ArgumentError('null partStatement'); + if (hasPartStatement == null) + throw new ArgumentError('null hasPartStatement'); + if (valueClassIsAbstract == null) + throw new ArgumentError('null valueClassIsAbstract'); + if (valueClassConstructors == null) + throw new ArgumentError('null valueClassConstructors'); + if (valueClassFactories == null) + throw new ArgumentError('null valueClassFactories'); + if (builderClassIsAbstract == null) + throw new ArgumentError('null builderClassIsAbstract'); + if (builderClassConstructors == null) + throw new ArgumentError('null builderClassConstructors'); + if (builderClassFactories == null) + throw new ArgumentError('null builderClassFactories'); + } + factory _$SourceClass([updates(SourceClassBuilder b)]) => + (new SourceClassBuilder()..update(updates)).build(); + SourceClass rebuild(updates(SourceClassBuilder b)) => + (toBuilder()..update(updates)).build(); + _$SourceClassBuilder toBuilder() => new _$SourceClassBuilder()..replace(this); + bool operator ==(other) { + if (other is! SourceClass) return false; + return name == other.name && + hasBuilder == other.hasBuilder && + fields == other.fields && + partStatement == other.partStatement && + hasPartStatement == other.hasPartStatement && + valueClassIsAbstract == other.valueClassIsAbstract && + valueClassConstructors == other.valueClassConstructors && + valueClassFactories == other.valueClassFactories && + builderClassIsAbstract == other.builderClassIsAbstract && + builderClassConstructors == other.builderClassConstructors && + builderClassFactories == other.builderClassFactories; + } + + int get hashCode { + return hashObjects([ + name, + hasBuilder, + fields, + partStatement, + hasPartStatement, + valueClassIsAbstract, + valueClassConstructors, + valueClassFactories, + builderClassIsAbstract, + builderClassConstructors, + builderClassFactories + ]); + } + + String toString() { + return 'SourceClass {' + 'name=${name.toString()}\n' + 'hasBuilder=${hasBuilder.toString()}\n' + 'fields=${fields.toString()}\n' + 'partStatement=${partStatement.toString()}\n' + 'hasPartStatement=${hasPartStatement.toString()}\n' + 'valueClassIsAbstract=${valueClassIsAbstract.toString()}\n' + 'valueClassConstructors=${valueClassConstructors.toString()}\n' + 'valueClassFactories=${valueClassFactories.toString()}\n' + 'builderClassIsAbstract=${builderClassIsAbstract.toString()}\n' + 'builderClassConstructors=${builderClassConstructors.toString()}\n' + 'builderClassFactories=${builderClassFactories.toString()}\n' + '}'; + } +} + +class _$SourceClassBuilder extends SourceClassBuilder { + _$SourceClassBuilder() : super._(); + void replace(SourceClass other) { + super.name = other.name; + super.hasBuilder = other.hasBuilder; + super.fields = other.fields?.toBuilder(); + super.partStatement = other.partStatement; + super.hasPartStatement = other.hasPartStatement; + super.valueClassIsAbstract = other.valueClassIsAbstract; + super.valueClassConstructors = other.valueClassConstructors?.toBuilder(); + super.valueClassFactories = other.valueClassFactories?.toBuilder(); + super.builderClassIsAbstract = other.builderClassIsAbstract; + super.builderClassConstructors = + other.builderClassConstructors?.toBuilder(); + super.builderClassFactories = other.builderClassFactories?.toBuilder(); + } + + void update(updates(SourceClassBuilder b)) { + if (updates != null) updates(this); + } + + SourceClass build() { + if (name == null) throw new ArgumentError('null name'); + if (hasBuilder == null) throw new ArgumentError('null hasBuilder'); + if (fields == null) throw new ArgumentError('null fields'); + if (partStatement == null) throw new ArgumentError('null partStatement'); + if (hasPartStatement == null) + throw new ArgumentError('null hasPartStatement'); + if (valueClassIsAbstract == null) + throw new ArgumentError('null valueClassIsAbstract'); + if (valueClassConstructors == null) + throw new ArgumentError('null valueClassConstructors'); + if (valueClassFactories == null) + throw new ArgumentError('null valueClassFactories'); + if (builderClassIsAbstract == null) + throw new ArgumentError('null builderClassIsAbstract'); + if (builderClassConstructors == null) + throw new ArgumentError('null builderClassConstructors'); + if (builderClassFactories == null) + throw new ArgumentError('null builderClassFactories'); + return new _$SourceClass._( + name: name, + hasBuilder: hasBuilder, + fields: fields?.build(), + partStatement: partStatement, + hasPartStatement: hasPartStatement, + valueClassIsAbstract: valueClassIsAbstract, + valueClassConstructors: valueClassConstructors?.build(), + valueClassFactories: valueClassFactories?.build(), + builderClassIsAbstract: builderClassIsAbstract, + builderClassConstructors: builderClassConstructors?.build(), + builderClassFactories: builderClassFactories?.build()); + } +} diff --git a/built_value_generator/lib/src/source_field.dart b/built_value_generator/lib/src/source_field.dart new file mode 100644 index 00000000..fe10cc14 --- /dev/null +++ b/built_value_generator/lib/src/source_field.dart @@ -0,0 +1,99 @@ +// 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_field; + +import 'package:analyzer/dart/element/element.dart'; +import 'package:built_collection/built_collection.dart'; +import 'package:built_value/built_value.dart'; + +part 'source_field.g.dart'; + +abstract class SourceField implements Built { + String get name; + String get type; + bool get isGetter; + bool get isNullable; + bool get builderFieldExists; + bool get builderFieldIsNormalField; + @nullable + String get typeInBuilder; + bool get isNestedBuilder; + + SourceField._(); + factory SourceField([updates(SourceFieldBuilder b)]) = _$SourceField; + + factory SourceField.fromFieldElements( + FieldElement fieldElement, FieldElement builderFieldElement) { + return new SourceField((b) => b + ..name = fieldElement.displayName + ..type = fieldElement.getter.returnType.displayName + ..typeInBuilder = builderFieldElement?.getter?.returnType?.displayName + ..isGetter = + fieldElement.getter != null && !fieldElement.getter.isSynthetic + ..isNullable = fieldElement.getter.metadata.any( + (metadata) => metadata.constantValue.toStringValue() == 'nullable') + ..builderFieldExists = builderFieldElement != null + ..builderFieldIsNormalField = builderFieldElement != null && + builderFieldElement.getter != null && + !builderFieldElement.getter.isAbstract && + builderFieldElement.getter.isSynthetic + ..isNestedBuilder = builderFieldElement?.getter?.returnType?.displayName + ?.contains('Builder') ?? + false); + } + + static BuiltList fromClassElements( + ClassElement classElement, ClassElement builderClassElement) { + final result = new ListBuilder(); + + for (final field in classElement.fields) { + if (!field.isStatic && + field.getter != null && + (field.getter.isAbstract || field.getter.isSynthetic)) { + final builderField = builderClassElement.getField(field.name); + result.add(new SourceField.fromFieldElements(field, builderField)); + } + } + + return result.build(); + } + + Iterable computeErrors() { + final result = []; + + if (!isGetter) { + result.add('Make field $name a getter.'); + } + + if (builderFieldExists && + type != typeInBuilder && + // TODO(davidmorgan): smarter check for builder types. + type.replaceAll('Built', '') != + typeInBuilder.replaceAll('Builder', '')) { + result.add('Make builder field $name have type: $type'); + } + + if (builderFieldExists && !builderFieldIsNormalField) { + result.add('Make builder field $name a normal field.'); + } + + return result; + } +} + +abstract class SourceFieldBuilder + implements Builder { + String name; + String type; + bool isGetter; + bool isNullable; + bool builderFieldExists; + bool builderFieldIsNormalField; + String typeInBuilder; + bool isNestedBuilder; + + SourceFieldBuilder._(); + factory SourceFieldBuilder() = _$SourceFieldBuilder; +} diff --git a/built_value_generator/lib/src/source_field.g.dart b/built_value_generator/lib/src/source_field.g.dart new file mode 100644 index 00000000..7f3ef206 --- /dev/null +++ b/built_value_generator/lib/src/source_field.g.dart @@ -0,0 +1,122 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of built_value_generator.source_field; + +// ************************************************************************** +// Generator: BuiltValueGenerator +// Target: abstract class SourceField +// ************************************************************************** + +class _$SourceField extends SourceField { + final String name; + final String type; + final bool isGetter; + final bool isNullable; + final bool builderFieldExists; + final bool builderFieldIsNormalField; + final String typeInBuilder; + final bool isNestedBuilder; + _$SourceField._( + {this.name, + this.type, + this.isGetter, + this.isNullable, + this.builderFieldExists, + this.builderFieldIsNormalField, + this.typeInBuilder, + this.isNestedBuilder}) + : super._() { + if (name == null) throw new ArgumentError('null name'); + if (type == null) throw new ArgumentError('null type'); + if (isGetter == null) throw new ArgumentError('null isGetter'); + if (isNullable == null) throw new ArgumentError('null isNullable'); + if (builderFieldExists == null) + throw new ArgumentError('null builderFieldExists'); + if (builderFieldIsNormalField == null) + throw new ArgumentError('null builderFieldIsNormalField'); + if (isNestedBuilder == null) + throw new ArgumentError('null isNestedBuilder'); + } + factory _$SourceField([updates(SourceFieldBuilder b)]) => + (new SourceFieldBuilder()..update(updates)).build(); + SourceField rebuild(updates(SourceFieldBuilder b)) => + (toBuilder()..update(updates)).build(); + _$SourceFieldBuilder toBuilder() => new _$SourceFieldBuilder()..replace(this); + bool operator ==(other) { + if (other is! SourceField) return false; + return name == other.name && + type == other.type && + isGetter == other.isGetter && + isNullable == other.isNullable && + builderFieldExists == other.builderFieldExists && + builderFieldIsNormalField == other.builderFieldIsNormalField && + typeInBuilder == other.typeInBuilder && + isNestedBuilder == other.isNestedBuilder; + } + + int get hashCode { + return hashObjects([ + name, + type, + isGetter, + isNullable, + builderFieldExists, + builderFieldIsNormalField, + typeInBuilder, + isNestedBuilder + ]); + } + + String toString() { + return 'SourceField {' + 'name=${name.toString()}\n' + 'type=${type.toString()}\n' + 'isGetter=${isGetter.toString()}\n' + 'isNullable=${isNullable.toString()}\n' + 'builderFieldExists=${builderFieldExists.toString()}\n' + 'builderFieldIsNormalField=${builderFieldIsNormalField.toString()}\n' + 'typeInBuilder=${typeInBuilder.toString()}\n' + 'isNestedBuilder=${isNestedBuilder.toString()}\n' + '}'; + } +} + +class _$SourceFieldBuilder extends SourceFieldBuilder { + _$SourceFieldBuilder() : super._(); + void replace(SourceField other) { + super.name = other.name; + super.type = other.type; + super.isGetter = other.isGetter; + super.isNullable = other.isNullable; + super.builderFieldExists = other.builderFieldExists; + super.builderFieldIsNormalField = other.builderFieldIsNormalField; + super.typeInBuilder = other.typeInBuilder; + super.isNestedBuilder = other.isNestedBuilder; + } + + void update(updates(SourceFieldBuilder b)) { + if (updates != null) updates(this); + } + + SourceField build() { + if (name == null) throw new ArgumentError('null name'); + if (type == null) throw new ArgumentError('null type'); + if (isGetter == null) throw new ArgumentError('null isGetter'); + if (isNullable == null) throw new ArgumentError('null isNullable'); + if (builderFieldExists == null) + throw new ArgumentError('null builderFieldExists'); + if (builderFieldIsNormalField == null) + throw new ArgumentError('null builderFieldIsNormalField'); + if (isNestedBuilder == null) + throw new ArgumentError('null isNestedBuilder'); + return new _$SourceField._( + name: name, + type: type, + isGetter: isGetter, + isNullable: isNullable, + builderFieldExists: builderFieldExists, + builderFieldIsNormalField: builderFieldIsNormalField, + typeInBuilder: typeInBuilder, + isNestedBuilder: isNestedBuilder); + } +} diff --git a/built_value_generator/pubspec.yaml b/built_value_generator/pubspec.yaml index d9124c7a..fbb90715 100644 --- a/built_value_generator/pubspec.yaml +++ b/built_value_generator/pubspec.yaml @@ -1,5 +1,5 @@ name: built_value_generator -version: 0.1.1 +version: 0.1.2 description: > Value types with builders. This library is the dev dependency. authors: @@ -13,7 +13,7 @@ dependencies: analyzer: '>=0.27.1 <0.28.0' build: '^0.3.0' built_collection: '^1.0.0' - built_value: '^0.1.1' + built_value: '^0.1.2' source_gen: '>=0.5.0+03 <0.6.0' quiver: '>=0.21.0 <0.22.0' diff --git a/built_value_generator/test/built_value_generator_test.dart b/built_value_generator/test/built_value_generator_test.dart index 4b846d3c..796eda7a 100644 --- a/built_value_generator/test/built_value_generator_test.dart +++ b/built_value_generator/test/built_value_generator_test.dart @@ -52,6 +52,19 @@ abstract class ValueBuilder extends Builder { }'''), contains("1. Make class have exactly one constructor: Value._();")); }); + test('suggests to add constructor when there is synthetic constructor', + () async { + expect(await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { +} +abstract class ValueBuilder extends Builder { + ValueBuilder._(); + factory ValueBuilder() = _\$ValueBuilder; +}'''), contains("1. Make class have exactly one constructor: Value._();")); + }); + test('allows code in constructor of value class', () async { expect( await generate('''library value; @@ -127,6 +140,22 @@ abstract class ValueBuilder extends Builder { "have exactly one constructor: ValueBuilder._();")); }); + test('suggests constructor for builder class with synthetic constructor', + () async { + expect( + await generate('''library value; +import 'package:built_value/built_value.dart'; +part 'value.g.dart'; +abstract class Value extends Built { + Value._(); + factory Value([updates(ValueBuilder b)]) = _\$Value; +} +abstract class ValueBuilder extends Builder { +}'''), + contains("1. Make builder class " + "have exactly one constructor: ValueBuilder._();")); + }); + test('suggests to add factory to builder class', () async { expect( await generate('''library value; diff --git a/built_value_generator/tool/build.dart b/built_value_generator/tool/build.dart new file mode 100644 index 00000000..7488278d --- /dev/null +++ b/built_value_generator/tool/build.dart @@ -0,0 +1,17 @@ +// 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. + +import 'dart:async'; + +import 'package:build/build.dart'; +import 'package:built_value_generator/built_value_generator.dart'; +import 'package:source_gen/source_gen.dart'; + +Future main(List args) async { + await build( + new PhaseGroup.singleAction( + new GeneratorBuilder([new BuiltValueGenerator()]), + new InputSet('built_value_generator', const ['lib/src/*.dart'])), + deleteFilesByDefault: true); +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 0fc7a9e0..c6877eda 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,5 @@ name: example -version: 0.1.1 +version: 0.1.2 description: > Just an example, not for publishing. authors: @@ -11,8 +11,8 @@ environment: dependencies: built_collection: '^1.0.0' - built_value: '^0.1.1' + built_value: '^0.1.2' dev_dependencies: - built_value_generator: '^0.1.1' + built_value_generator: '^0.1.2' test: any