diff --git a/hive_generator/lib/src/builder.dart b/hive_generator/lib/src/builder.dart index 7284a7289..edfd4f365 100644 --- a/hive_generator/lib/src/builder.dart +++ b/hive_generator/lib/src/builder.dart @@ -1,12 +1,58 @@ import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; class AdapterField { final int index; - final String name; - final DartType type; + final PropertyAccessorElement element; - AdapterField(this.index, this.name, this.type); + String get name => element.variable.name; + DartType get type => element.variable.type; + + bool get isNullable { + switch (element.variable.type?.nullabilitySuffix) { + case NullabilitySuffix.none: + return false; + default: + return true; + } + } + + AdapterField(this.index, this.element); + BuiltAdapterField toBuilt(bool nestedBuilders) => BuiltAdapterField( + index, + element, + nestedBuilders, + ); +} + +class BuiltAdapterField extends AdapterField { + DartType _type; + + @override + DartType get type => _type ?? super.type; + set type(DartType type) => _type = type; + + bool nestedBuilders; + + BuiltAdapterField( + int index, + PropertyAccessorElement element, + this.nestedBuilders, + ) : super(index, element); + + bool get hasNullableAnnotation => element.metadata.any((metadata) => + metadata.computeConstantValue()?.toStringValue() == 'nullable'); + + bool get hasNullableType => + element?.returnType?.nullabilitySuffix == NullabilitySuffix.question; + + @override + bool get isNullable => hasNullableAnnotation || hasNullableType; + + @override + BuiltAdapterField toBuilt(bool nestedBuilders) => + throw StateError('Already is an BuiltAdapterField'); } abstract class Builder { diff --git a/hive_generator/lib/src/class_builder.dart b/hive_generator/lib/src/class_builder.dart index 812702305..59f0858d7 100644 --- a/hive_generator/lib/src/class_builder.dart +++ b/hive_generator/lib/src/class_builder.dart @@ -7,8 +7,319 @@ import 'package:hive_generator/src/builder.dart'; import 'package:hive_generator/src/helper.dart'; import 'package:source_gen/source_gen.dart'; import 'package:dartx/dartx.dart'; +import 'package:built_value/built_value.dart' as bv; +import 'package:built_collection/built_collection.dart'; -class ClassBuilder extends Builder { +import 'type_adapter_generator.dart'; + +class ClassBuilder extends _ClassBuilderBase { + ClassBuilder( + ClassElement cls, + List getters, + List setters, + ) : super(cls, getters, setters); + + var builtValueChecker = const TypeChecker.fromRuntime(bv.BuiltValue); + + var builtChecker = const TypeChecker.fromRuntime(bv.Built); + var builtListChecker = const TypeChecker.fromRuntime(BuiltList); + var builtSetChecker = const TypeChecker.fromRuntime(BuiltSet); + var builtMapChecker = const TypeChecker.fromRuntime(BuiltMap); + + var builderChecker = const TypeChecker.fromRuntime(bv.Builder); + var listBuilderChecker = const TypeChecker.fromRuntime(ListBuilder); + var setBuilderChecker = const TypeChecker.fromRuntime(SetBuilder); + var mapBuilderChecker = const TypeChecker.fromRuntime(MapBuilder); + + bool get isThisBuilt => cls.interfaces.any(builtChecker.isExactlyType); + DartType get builderType => isThisBuilt + ? cls.interfaces + .singleWhere(builtChecker.isExactlyType) + .typeArguments + .last + : throw StateError( + 'Tried to find the builderType on ${cls.name}, which is not Built.'); + + bool _nestedBuildersFromAnnotation() { + final annotation = + cls.metadata.map((e) => e.computeConstantValue()).singleWhere( + (e) => builtValueChecker.isExactlyType(e.type), + orElse: () => null, + ); + if (annotation == null) { + return true; + } + final reader = ConstantReader(annotation); + final nestedBuilders = reader.read('nestedBuilders'); + if (nestedBuilders.isNull) { + return true; + } + return nestedBuilders.boolValue; + } + + @override + void buildReadConstructor(StringBuffer code) { + if (!isThisBuilt) { + return super.buildReadConstructor(code); + } + + String builderName; + List fields; + + // In case the builder is being generated, we assume it has the default + // name and fields + if (builderType?.isDynamic ?? true) { + builderName = '${cls.name}Builder'; + + // We want to set an builder instead of the built class depending on the + // @BuiltValue annotation, but we cant express this easily with DartType, + // so we pass this info to cast() + final nestedBuilders = _nestedBuildersFromAnnotation(); + + // The fields that need to be set on the cascade are the getters in the + // built class, because they have an corresponding setter in the builder. + fields = getters.map((field) => field.toBuilt(nestedBuilders)).toList(); + } else { + // The builder type was manually created, therefore we look it up for + // @HiveField annotations + final builderCls = builderType.element as ClassElement; + builderName = builderCls.name; + var gettersAndSetters = getAccessors(builderCls, builderCls.library); + + var setters = gettersAndSetters[1]; + verifyFieldIndices(setters); + + // The fields that need to be set on the cascade are the setters in the + // builder class. + // + // We do not need to look for nested fieldss in the annotation, as this + // information is contained in each setter's DartType in most cases, + // allowing for correct casting. + fields = setters.map((field) => field.toBuilt(false)).toList(); + + // The edge case is when the type is an Builder which is being generated. + // In this case we set nestedBuilders = true and the type to the Built + // type as a workaround. + final clsFieldMap = {for (final field in getters) field.index: field}; + for (final builderField in fields) { + final builtField = clsFieldMap[builderField.index]; + if (builtField == null) { + continue; + } + if (!builderField.type.isDynamic || builtField.type.isDynamic) { + continue; + } + // builderField is dynamic, while builtField isnt. this MAY be the edge + // case. To resolve it, we will check if builtField is an Built value. + // If so, this is the edge case + if (!isBuilt(builtField.type)) { + continue; + } + builderField + ..type = builtField.type + ..nestedBuilders = true; + } + } + + // Instantiate the builder + code.writeln(' return ($builderName()'); + + // Initialize the parameters using setters with cascades on the builder. + for (var field in fields) { + code.writeln('..${field.name} = ${cast( + field.type, + 'fields[${field.index}]', + nestedBuilders: field.nestedBuilders, + )}'); + } + + // Build the class + code.write(').build()'); + } + + String _castBuiltCollection( + DartType type, + String variable, { + bool nestedBuilders, + }) { + String builderConstructor; + String typeToBeCasted; + var castExpr = ''; + // Wether or not we should call build() on the end. + // + // This when the user annotated with nestedBuilders = false, so the Builder + // for that class expects an Built value, instead of a builder. + // + // This is not the case when either nestedBuilders is true or ommited, or + // when an custom builder was specified with an ListBuilder for example. + var shouldBeBuilt = isBuiltCollection(type) && (nestedBuilders != true); + + if (builtMapChecker.isExactlyType(type) || + mapBuilderChecker.isExactlyType(type)) { + builderConstructor = 'MapBuilder'; + typeToBeCasted = 'Map'; + castExpr = castMap(type); + } else { + typeToBeCasted = 'Iterable'; + castExpr = castIterable(type); + if (builtSetChecker.isExactlyType(type) || + setBuilderChecker.isExactlyType(type)) { + builderConstructor = 'SetBuilder'; + } + if (builtListChecker.isExactlyType(type) || + listBuilderChecker.isExactlyType(type)) { + builderConstructor = 'ListBuilder'; + } + } + check(builderConstructor != null && typeToBeCasted != null, + 'Unrecognized built_collection type ${_displayString(type)}'); + + final castedVariable = castExpr.isEmpty + ? '$variable as $typeToBeCasted' + : '($variable as $typeToBeCasted)$castExpr'; + + final buildExpression = '$builderConstructor<${_typeParamsString(type)}>' + '($castedVariable)' + '${shouldBeBuilt ? '.build()' : ''}'; + + return '$variable == null ? null : $buildExpression'; + } + + @override + String cast( + DartType type, + String variable, { + bool nestedBuilders = false, + }) { + if (!isBuiltOrBuiltCollection(type) && + !isBuilderOrCollectionBuilder(type)) { + // This value needs no special treatment. + return super.cast(type, variable); + } + + if ((isBuilt(type) && nestedBuilders) || isBuilder(type)) { + // We need to call .toBuilder(), because variable is always an Built + // value, but we need an Builder value. + return '($variable as ${_displayString(type)})?.toBuilder()'; + } + + if (isBuiltCollection(type) || isCollectionBuilder(type)) { + return _castBuiltCollection( + type, + variable, + nestedBuilders: nestedBuilders, + ); + } + + // We just need to cast the value. This happens when the type is of a Built + // value in a custom Builder which accepts the plain Built value instead of + // a builder, for example. + return '$variable as ${_displayString(type)}'; + } + + @override + String castIterable(DartType type) { + var paramType = type as ParameterizedType; + var arg = paramType.typeArguments.first; + if (isBuiltCollection(arg) || isCollectionBuilder(arg)) { + return '?.map((dynamic e)=> ' + '${cast(arg, 'e')})'; + } else if (isBuiltCollection(type) || isCollectionBuilder(type)) { + // Built collections use List.from and Map.from, so casting + // manually is not needed. + return ''; + } else { + return super.castIterable(type); + } + } + + @override + String castMap(DartType type) { + var paramType = type as ParameterizedType; + var arg1 = paramType.typeArguments[0]; + var arg2 = paramType.typeArguments[1]; + if (isBuiltCollection(arg1) || + isCollectionBuilder(arg1) || + isBuiltCollection(arg2) || + isCollectionBuilder(arg2)) { + return '?.map((dynamic k, dynamic v)=>' + 'MapEntry(${cast(arg1, 'k')},' + '${cast(arg2, 'v')}))'; + } + return super.castMap(type); + } + + bool isBuilt(DartType type) { + return builtChecker.isAssignableFromType(type); + } + + bool isBuiltCollection(DartType type) { + return builtListChecker.isExactlyType(type) || + builtSetChecker.isExactlyType(type) || + builtMapChecker.isExactlyType(type); + } + + bool isBuiltOrBuiltCollection(DartType type) { + return isBuilt(type) || isBuiltCollection(type); + } + + bool isBuilder(DartType type) { + return builderChecker.isAssignableFromType(type); + } + + bool isCollectionBuilder(DartType type) { + return listBuilderChecker.isExactlyType(type) || + setBuilderChecker.isExactlyType(type) || + mapBuilderChecker.isExactlyType(type); + } + + bool isBuilderOrCollectionBuilder(DartType type) { + return isBuilder(type) || isCollectionBuilder(type); + } + + String _typeParamsString(DartType type) { + var paramType = type as ParameterizedType; + var typeParams = paramType.typeArguments.map(_displayString); + return typeParams.join(', '); + } + + String _convertWritableBuiltMap(DartType type, String accessor) { + var paramType = type as ParameterizedType; + var arg1 = paramType.typeArguments[0]; + var arg2 = paramType.typeArguments[1]; + if (isBuiltCollection(arg1) || isBuiltCollection(arg2)) { + return '$accessor?.toMap()?.map' + '((k, v) => MapEntry(' + '${convertWritableValue(arg1, 'k')}, ' + '${convertWritableValue(arg2, 'v')}))'; + } + return '$accessor?.toMap()'; + } + + String _convertWritableBuiltIterable(DartType type, String accessor) { + var paramType = type as ParameterizedType; + var arg = paramType.typeArguments.single; + if (isBuiltCollection(arg)) { + return '$accessor?.toList()?.map' + '((e) => ' + '${convertWritableValue(arg, 'e')})'; + } + return '$accessor?.toList()'; + } + + @override + String convertWritableValue(DartType type, String accessor) { + if (!isBuiltCollection(type)) { + return super.convertWritableValue(type, accessor); + } + if (builtMapChecker.isExactlyType(type)) { + return _convertWritableBuiltMap(type, accessor); + } + return _convertWritableBuiltIterable(type, accessor); + } +} + +class _ClassBuilderBase extends Builder { var hiveListChecker = const TypeChecker.fromRuntime(HiveList); var listChecker = const TypeChecker.fromRuntime(List); var mapChecker = const TypeChecker.fromRuntime(Map); @@ -16,29 +327,20 @@ class ClassBuilder extends Builder { var iterableChecker = const TypeChecker.fromRuntime(Iterable); var uint8ListChecker = const TypeChecker.fromRuntime(Uint8List); - ClassBuilder( + _ClassBuilderBase( ClassElement cls, List getters, List setters) : super(cls, getters, setters); - @override - String buildRead() { - var code = StringBuffer(); - code.writeln(''' - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) - reader.readByte(): reader.read(), - }; - return ${cls.name}( - '''); + void buildReadConstructor(StringBuffer code) { + code.writeln(' return ${cls.name}('); var constr = cls.constructors.firstOrNullWhere((it) => it.name.isEmpty); check(constr != null, 'Provide an unnamed constructor.'); - // The remaining fields to initialize. + // The remaining fields to be set later var fields = setters.toList(); - for (var param in constr.parameters) { + for (var param in constr?.parameters ?? []) { var field = fields.firstOrNullWhere((it) => it.name == param.name); // Final fields field ??= getters.firstOrNullWhere((it) => it.name == param.name); @@ -46,7 +348,7 @@ class ClassBuilder extends Builder { if (param.isNamed) { code.write('${param.name}: '); } - code.writeln('${_cast(param.type, 'fields[${field.index}]')},'); + code.writeln('${cast(param.type, 'fields[${field.index}]')},'); fields.remove(field); } } @@ -57,22 +359,36 @@ class ClassBuilder extends Builder { // as initializing formals. We do so using cascades. for (var field in fields) { code.writeln( - '..${field.name} = ${_cast(field.type, 'fields[${field.index}]')}'); + '..${field.name} = ${cast(field.type, 'fields[${field.index}]')}'); } + } + + @override + String buildRead() { + var code = StringBuffer(); + code.writeln(''' + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) + reader.readByte(): reader.read(), + }; + '''); + + buildReadConstructor(code); code.writeln(';'); return code.toString(); } - String _cast(DartType type, String variable) { + String cast(DartType type, String variable) { if (hiveListChecker.isExactlyType(type)) { return '($variable as HiveList)?.castHiveList()'; } else if (iterableChecker.isAssignableFromType(type) && !isUint8List(type)) { - return '($variable as List)${_castIterable(type)}'; + return '($variable as List)${castIterable(type)}'; } else if (mapChecker.isExactlyType(type)) { - return '($variable as Map)${_castMap(type)}'; + return '($variable as Map)${castMap(type)}'; } else { return '$variable as ${_displayString(type)}'; } @@ -89,29 +405,29 @@ class ClassBuilder extends Builder { return uint8ListChecker.isExactlyType(type); } - String _castIterable(DartType type) { + String castIterable(DartType type) { var paramType = type as ParameterizedType; var arg = paramType.typeArguments.first; if (isMapOrIterable(arg) && !isUint8List(arg)) { - var cast = ''; + var castSuffix = ''; if (listChecker.isExactlyType(type)) { - cast = '?.toList()'; + castSuffix = '?.toList()'; } else if (setChecker.isExactlyType(type)) { - cast = '?.toSet()'; + castSuffix = '?.toSet()'; } - return '?.map((dynamic e)=> ${_cast(arg, 'e')})$cast'; + return '?.map((dynamic e)=> ${cast(arg, 'e')})$castSuffix'; } else { return '?.cast<${_displayString(arg)}>()'; } } - String _castMap(DartType type) { + String castMap(DartType type) { var paramType = type as ParameterizedType; var arg1 = paramType.typeArguments[0]; var arg2 = paramType.typeArguments[1]; if (isMapOrIterable(arg1) || isMapOrIterable(arg2)) { return '?.map((dynamic k, dynamic v)=>' - 'MapEntry(${_cast(arg1, 'k')},${_cast(arg2, 'v')}))'; + 'MapEntry(${cast(arg1, 'k')},${cast(arg2, 'v')}))'; } else { return '?.cast<${_displayString(arg1)}, ' '${_displayString(arg2)}>()'; @@ -124,7 +440,7 @@ class ClassBuilder extends Builder { code.writeln('writer'); code.writeln('..writeByte(${getters.length})'); for (var field in getters) { - var value = _convertIterable(field.type, 'obj.${field.name}'); + var value = convertWritableValue(field.type, 'obj.${field.name}'); code.writeln(''' ..writeByte(${field.index}) ..write($value)'''); @@ -134,7 +450,7 @@ class ClassBuilder extends Builder { return code.toString(); } - String _convertIterable(DartType type, String accessor) { + String convertWritableValue(DartType type, String accessor) { if (setChecker.isExactlyType(type) || iterableChecker.isExactlyType(type)) { return '$accessor?.toList()'; } else { diff --git a/hive_generator/lib/src/enum_class_builder.dart b/hive_generator/lib/src/enum_class_builder.dart new file mode 100644 index 000000000..22cd00e9a --- /dev/null +++ b/hive_generator/lib/src/enum_class_builder.dart @@ -0,0 +1,25 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:hive_generator/src/builder.dart'; + +class EnumClassBuilder extends Builder { + EnumClassBuilder( + ClassElement cls, + ) : super(cls, null, null); + + @override + String buildRead() { + // Read the name of the enum class from the single field + var code = StringBuffer() + ..writeln('return ${cls.name}.valueOf(reader.read() as String);'); + return code.toString(); + } + + @override + String buildWrite() { + // Write the name of the enum class as the single field. + var code = StringBuffer() + ..writeln('writer') // + ..writeln('..write(obj.name);'); + return code.toString(); + } +} diff --git a/hive_generator/lib/src/type_adapter_generator.dart b/hive_generator/lib/src/type_adapter_generator.dart index 9df457059..84e813319 100644 --- a/hive_generator/lib/src/type_adapter_generator.dart +++ b/hive_generator/lib/src/type_adapter_generator.dart @@ -1,13 +1,21 @@ import 'package:analyzer/dart/element/element.dart'; -import 'package:build/build.dart'; +import 'package:build/build.dart' hide Builder; import 'package:hive_generator/src/builder.dart'; import 'package:hive_generator/src/class_builder.dart'; import 'package:hive_generator/src/enum_builder.dart'; +import 'package:hive_generator/src/enum_class_builder.dart'; import 'package:hive_generator/src/helper.dart'; import 'package:source_gen/source_gen.dart'; import 'package:hive/hive.dart'; class TypeAdapterGenerator extends GeneratorForAnnotation { + /// The enum class type checker. It isn't from runtime because otherwise we + /// would have to depend on built_value. Altough [TypeChecker.fromUrl] is + /// not reccomended because of it's brittleness, this should not be a problem, + /// as this class is in the same url since it was added, 4 years ago. + var enumClassChecker = const TypeChecker.fromUrl( + 'package:built_value/built_value.dart#EnumClass'); + static String generateName(String typeName) { var adapterName = '${typeName}Adapter'.replaceAll(RegExp(r'[^A-Za-z0-9]+'), ''); @@ -36,9 +44,14 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { var typeId = getTypeId(annotation); var adapterName = getAdapterName(cls.name, annotation); - var builder = cls.isEnum - ? EnumBuilder(cls, getters) - : ClassBuilder(cls, getters, setters); + Builder builder; + if (cls.isEnum) { + builder = EnumBuilder(cls, getters); + } else if (enumClassChecker.isExactlyType(cls.supertype)) { + builder = EnumClassBuilder(cls); + } else { + builder = ClassBuilder(cls, getters, setters); + } return ''' class $adapterName extends TypeAdapter<${cls.name}> { @@ -75,72 +88,6 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { return element as ClassElement; } - Set getAllAccessorNames(ClassElement cls) { - var accessorNames = {}; - - var supertypes = cls.allSupertypes.map((it) => it.element); - for (var type in [cls, ...supertypes]) { - for (var accessor in type.accessors) { - if (accessor.isSetter) { - var name = accessor.name; - accessorNames.add(name.substring(0, name.length - 1)); - } else { - accessorNames.add(accessor.name); - } - } - } - - return accessorNames; - } - - List> getAccessors( - ClassElement cls, LibraryElement library) { - var accessorNames = getAllAccessorNames(cls); - - var getters = []; - var setters = []; - for (var name in accessorNames) { - var getter = cls.lookUpGetter(name, library); - if (getter != null) { - var getterAnn = - getHiveFieldAnn(getter.variable) ?? getHiveFieldAnn(getter); - if (getterAnn != null) { - var field = getter.variable; - getters.add(AdapterField(getterAnn.index, field.name, field.type)); - } - } - - var setter = cls.lookUpSetter('$name=', library); - if (setter != null) { - var setterAnn = - getHiveFieldAnn(setter.variable) ?? getHiveFieldAnn(setter); - if (setterAnn != null) { - var field = setter.variable; - setters.add(AdapterField(setterAnn.index, field.name, field.type)); - } - } - } - - return [getters, setters]; - } - - void verifyFieldIndices(List fields) { - for (var field in fields) { - check(field.index >= 0 || field.index <= 255, - 'Field numbers can only be in the range 0-255.'); - - for (var otherField in fields) { - if (otherField == field) continue; - if (otherField.index == field.index) { - throw HiveError( - 'Duplicate field number: ${field.index}. Fields "${field.name}" ' - 'and "${otherField.name}" have the same number.', - ); - } - } - } - } - String getAdapterName(String typeName, ConstantReader annotation) { var annAdapterName = annotation.read('adapterName'); if (annAdapterName.isNull) { @@ -158,3 +105,75 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { return annotation.read('typeId').intValue; } } + +Set _getAllAccessorNames(ClassElement cls) { + var accessorNames = {}; + + var supertypes = cls.allSupertypes.map((it) => it.element); + for (var type in [cls, ...supertypes]) { + for (var accessor in type.accessors) { + if (accessor.isSetter) { + var name = accessor.name; + accessorNames.add(name.substring(0, name.length - 1)); + } else { + accessorNames.add(accessor.name); + } + } + } + + return accessorNames; +} + +List> getAccessors( + ClassElement cls, LibraryElement library) { + var accessorNames = _getAllAccessorNames(cls); + + var getters = []; + var setters = []; + for (var name in accessorNames) { + var getter = cls.lookUpGetter(name, library); + if (getter != null) { + var getterAnn = + getHiveFieldAnn(getter.variable) ?? getHiveFieldAnn(getter); + if (getterAnn != null) { + var field = getter.variable; + getters.add(AdapterField( + getterAnn.index, + getter, + )); + } + } + + var setter = cls.lookUpSetter('$name=', library); + if (setter != null) { + var setterAnn = + getHiveFieldAnn(setter.variable) ?? getHiveFieldAnn(setter); + if (setterAnn != null) { + var field = setter.variable; + setters.add(AdapterField( + setterAnn.index, + setter, + )); + } + } + } + + return [getters, setters]; +} + +void verifyFieldIndices(List fields) { + for (var field in fields) { + check(field.index >= 0 || field.index <= 255, + 'Field numbers can only be in the range 0-255.'); + + for (var otherField in fields) { + if (otherField == field) continue; + if (otherField.index == field.index) { + throw HiveError( + 'Duplicate field number: ${field.index}. Fields "${field.name}" ' + 'and "${otherField.name}" have the same number.', + ); + } + } + } +} diff --git a/hive_generator/pubspec.yaml b/hive_generator/pubspec.yaml index 6e440e27e..c94c5565d 100644 --- a/hive_generator/pubspec.yaml +++ b/hive_generator/pubspec.yaml @@ -13,6 +13,8 @@ dependencies: hive: ">=1.3.0 <2.0.0" analyzer: ">=0.36.0 <2.0.0" dartx: ">=0.2.0 <1.0.0" + built_value: any + built_collection: any dev_dependencies: test: ^1.6.4