From fb18f4278eaaf37d105be6ae44e965ed374f5128 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 20:29:51 +0000 Subject: [PATCH 1/4] feat: add HiveConverter for custom field conversion in adapters Add a JsonConverter-style API for transforming field values during TypeAdapter generation. Converters can be applied via field/class annotations, AdapterSpec.converters, or GenerateAdapters.converters, including generic converters with inferred type arguments. Co-authored-by: Rexios --- hive/CHANGELOG.md | 5 + hive/lib/hive.dart | 1 + .../src/annotations/generate_adapters.dart | 20 +- hive/lib/src/annotations/hive_converter.dart | 62 +++ hive/pubspec.yaml | 2 +- hive_generator/CHANGELOG.md | 6 + .../example/lib/hive/hive_adapters.dart | 38 +- .../example/lib/hive/hive_adapters.g.dart | 45 +++ .../example/lib/hive/hive_adapters.g.yaml | 10 +- .../example/lib/hive/hive_registrar.g.dart | 2 + .../src/adapter_builder/adapter_builder.dart | 12 +- .../class_adapter_builder.dart | 127 +++++- .../lib/src/generator/adapters_generator.dart | 2 + .../src/generator/type_adapter_generator.dart | 11 +- .../lib/src/helper/hive_converter_helper.dart | 377 ++++++++++++++++++ .../src/model/revived_generate_adapter.dart | 27 +- hive_generator/pubspec.yaml | 2 +- hive_generator/test/hive_converter_test.dart | 239 +++++++++++ hive_generator/test/test_utils.dart | 31 +- 19 files changed, 985 insertions(+), 34 deletions(-) create mode 100644 hive/lib/src/annotations/hive_converter.dart create mode 100644 hive_generator/lib/src/helper/hive_converter_helper.dart create mode 100644 hive_generator/test/hive_converter_test.dart diff --git a/hive/CHANGELOG.md b/hive/CHANGELOG.md index 586c7665..666a7218 100644 --- a/hive/CHANGELOG.md +++ b/hive/CHANGELOG.md @@ -1,3 +1,8 @@ +## 2.20.0 + +- Adds `HiveConverter` for custom field conversion during TypeAdapter generation (similar to `json_serializable`'s `JsonConverter`) +- Adds `converters` parameter to `GenerateAdapters` and `AdapterSpec` + ## 2.19.3 - IsolatedHive: Handles stale send ports on hot restart diff --git a/hive/lib/hive.dart b/hive/lib/hive.dart index e18697ab..25f25541 100644 --- a/hive/lib/hive.dart +++ b/hive/lib/hive.dart @@ -11,6 +11,7 @@ export 'src/box_collection/box_collection_stub.dart' export 'src/object/hive_object.dart' show HiveObject, HiveObjectMixin; export 'src/annotations/generate_adapters.dart'; +export 'src/annotations/hive_converter.dart'; export 'src/annotations/hive_field.dart'; export 'src/annotations/hive_type.dart'; export 'src/binary/binary_reader.dart'; diff --git a/hive/lib/src/annotations/generate_adapters.dart b/hive/lib/src/annotations/generate_adapters.dart index 26f8ccc4..4bbdae1d 100644 --- a/hive/lib/src/annotations/generate_adapters.dart +++ b/hive/lib/src/annotations/generate_adapters.dart @@ -1,3 +1,4 @@ +import 'package:hive_ce/src/annotations/hive_converter.dart'; import 'package:meta/meta.dart'; /// Annotation to generate TypeAdapters for the given [specs] @@ -9,6 +10,7 @@ class GenerateAdapters { this.specs, { this.firstTypeId = 0, this.reservedTypeIds = const {}, + this.converters = const [], }); // coverage:ignore-end @@ -22,6 +24,12 @@ class GenerateAdapters { /// /// These type ids will be skipped during generation final Set reservedTypeIds; + + /// A list of [HiveConverter]s to apply when generating adapters for [specs] + /// + /// These converters are used when a matching converter is not found on the + /// field or class. See [HiveConverter] for details. + final List converters; } /// Configuration that specifies the generation of a TypeAdapter @@ -29,7 +37,10 @@ class GenerateAdapters { class AdapterSpec { /// Constructor // coverage:ignore-start - const AdapterSpec({this.ignoredFields = const {}}); + const AdapterSpec({ + this.ignoredFields = const {}, + this.converters = const [], + }); // coverage:ignore-end /// Fields that should be ignored @@ -37,4 +48,11 @@ class AdapterSpec { /// This should only be used to simplify migrations from `HiveType` /// annotations. Model classes should only contain fields to be persisted. final Set ignoredFields; + + /// A list of [HiveConverter]s to apply when generating an adapter for [T] + /// + /// These converters take precedence over [GenerateAdapters.converters], but + /// are overridden by converters placed on the class or field. See + /// [HiveConverter] for details. + final List converters; } diff --git a/hive/lib/src/annotations/hive_converter.dart b/hive/lib/src/annotations/hive_converter.dart new file mode 100644 index 00000000..eb4ab2ca --- /dev/null +++ b/hive/lib/src/annotations/hive_converter.dart @@ -0,0 +1,62 @@ +import 'package:meta/meta.dart'; + +/// Implement this class to provide custom converters for a specific [Type]. +/// +/// [T] is the data type you'd like to convert to and from. +/// +/// [S] is the type of the value stored in Hive. It must be a type Hive can +/// write natively (such as [String], [int], [List], [Set], or [Map]) or a type +/// with a registered [TypeAdapter]. +/// +/// [HiveConverter]s can be placed either on the class: +/// +/// ```dart +/// class MyHiveConverter extends HiveConverter { +/// const MyHiveConverter(); +/// +/// @override +/// Value fromHive(String hive) => Value(hive); +/// +/// @override +/// String toHive(Value object) => object.toString(); +/// } +/// +/// @GenerateAdapters([AdapterSpec()]) +/// @MyHiveConverter() +/// class Example { +/// final Value property; +/// } +/// ``` +/// +/// or on a property: +/// +/// ```dart +/// @GenerateAdapters([AdapterSpec()]) +/// class Example { +/// @MyHiveConverter() +/// final Value property; +/// } +/// ``` +/// +/// Or finally, passed to the [GenerateAdapters] or [AdapterSpec] annotation: +/// +/// ```dart +/// @GenerateAdapters( +/// [AdapterSpec()], +/// converters: [MyHiveConverter()], +/// ) +/// class Example { +/// final Value property; +/// } +/// ``` +@immutable +abstract class HiveConverter { + /// Constructor + const HiveConverter(); + + /// Convert a value read from Hive into [T]. + T fromHive(S hive); + + /// Convert a [T] value into a value Hive can write. + S toHive(T object); +} diff --git a/hive/pubspec.yaml b/hive/pubspec.yaml index f26c47a9..472ec6e0 100644 --- a/hive/pubspec.yaml +++ b/hive/pubspec.yaml @@ -1,6 +1,6 @@ name: hive_ce description: Hive Community Edition - A spiritual continuation of Hive v2 -version: 2.19.3 +version: 2.20.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive topics: diff --git a/hive_generator/CHANGELOG.md b/hive_generator/CHANGELOG.md index 515dd0af..4793e6cf 100644 --- a/hive_generator/CHANGELOG.md +++ b/hive_generator/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.12.0 + +- Adds support for `HiveConverter` in adapter generation (similar to `json_serializable`'s `JsonConverter`) +- Converters can be placed on fields, classes, `AdapterSpec.converters`, or `GenerateAdapters.converters` +- Supports generic converters with inferred type arguments (e.g. `WrappedListConverter`) + ## 1.11.3 - Upgrades `analyzer` to `14.0.0` diff --git a/hive_generator/example/lib/hive/hive_adapters.dart b/hive_generator/example/lib/hive/hive_adapters.dart index 51079626..fd6cb8e6 100644 --- a/hive_generator/example/lib/hive/hive_adapters.dart +++ b/hive_generator/example/lib/hive/hive_adapters.dart @@ -1,15 +1,31 @@ import 'package:hive_ce/hive_ce.dart'; import 'package:meta/meta.dart'; -@GenerateAdapters([ - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), - AdapterSpec(), -], firstTypeId: 50) +@GenerateAdapters( + [ + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + AdapterSpec(), + ], + firstTypeId: 50, + converters: [EpochDateTimeConverter()], +) part 'hive_adapters.g.dart'; +/// Example converter matching json_serializable's JsonConverter pattern +class EpochDateTimeConverter implements HiveConverter { + const EpochDateTimeConverter(); + + @override + DateTime fromHive(int hive) => DateTime.fromMillisecondsSinceEpoch(hive); + + @override + int toHive(DateTime object) => object.millisecondsSinceEpoch; +} + @immutable class ClassSpec1 { final int value; @@ -41,3 +57,11 @@ enum EnumSpec { EnumSpec get getter => EnumSpec.value2; } + +@immutable +class ClassSpec5 { + final DateTime time; + final DateTime? optionalTime; + + const ClassSpec5(this.time, this.optionalTime); +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.dart b/hive_generator/example/lib/hive/hive_adapters.g.dart index 040c2730..72af63cc 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.dart +++ b/hive_generator/example/lib/hive/hive_adapters.g.dart @@ -182,3 +182,48 @@ class ClassSpec4Adapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class ClassSpec5Adapter extends TypeAdapter { + @override + final typeId = 55; + + @override + ClassSpec5 read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return ClassSpec5( + const EpochDateTimeConverter().fromHive(fields[0] as int), + fields[1] == null + ? null + : const EpochDateTimeConverter().fromHive(fields[1] as int), + ); + } + + @override + void write(BinaryWriter writer, ClassSpec5 obj) { + writer + ..writeByte(2) + ..writeByte(0) + ..write(const EpochDateTimeConverter().toHive(obj.time)) + ..writeByte(1) + ..write( + obj.optionalTime == null + ? null + : const EpochDateTimeConverter().toHive( + obj.optionalTime as DateTime, + ), + ); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClassSpec5Adapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.yaml b/hive_generator/example/lib/hive/hive_adapters.g.yaml index 21b64bc5..964502d1 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.yaml +++ b/hive_generator/example/lib/hive/hive_adapters.g.yaml @@ -1,7 +1,7 @@ # Generated by Hive CE # Manual modifications may be necessary for certain migrations # Check in to version control -nextTypeId: 55 +nextTypeId: 56 types: ClassSpec1: typeId: 50 @@ -43,3 +43,11 @@ types: typeId: 54 nextIndex: 0 fields: {} + ClassSpec5: + typeId: 55 + nextIndex: 2 + fields: + time: + index: 0 + optionalTime: + index: 1 diff --git a/hive_generator/example/lib/hive/hive_registrar.g.dart b/hive_generator/example/lib/hive/hive_registrar.g.dart index 2d9a86a4..63b7fd04 100644 --- a/hive_generator/example/lib/hive/hive_registrar.g.dart +++ b/hive_generator/example/lib/hive/hive_registrar.g.dart @@ -16,6 +16,7 @@ extension HiveRegistrar on HiveInterface { registerAdapter(ClassSpec2Adapter()); registerAdapter(ClassSpec3Adapter()); registerAdapter(ClassSpec4Adapter()); + registerAdapter(ClassSpec5Adapter()); registerAdapter(ConstructorDefaultsAdapter()); registerAdapter(EmptyClassAdapter()); registerAdapter(Enum1Adapter()); @@ -34,6 +35,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface { registerAdapter(ClassSpec2Adapter()); registerAdapter(ClassSpec3Adapter()); registerAdapter(ClassSpec4Adapter()); + registerAdapter(ClassSpec5Adapter()); registerAdapter(ConstructorDefaultsAdapter()); registerAdapter(EmptyClassAdapter()); registerAdapter(Enum1Adapter()); diff --git a/hive_generator/lib/src/adapter_builder/adapter_builder.dart b/hive_generator/lib/src/adapter_builder/adapter_builder.dart index ac1c0271..c67e9004 100644 --- a/hive_generator/lib/src/adapter_builder/adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/adapter_builder.dart @@ -49,12 +49,20 @@ abstract class AdapterBuilder { /// TODO: Document this! final List setters; + /// Converters from [AdapterSpec.converters] + final List specConverters; + + /// Converters from [GenerateAdapters.converters] + final List globalConverters; + /// TODO: Document this! const AdapterBuilder( this.cls, - this.getters, [ + this.getters, { this.setters = const [], - ]); + this.specConverters = const [], + this.globalConverters = const [], + }); /// TODO: Document this! String buildRead(); diff --git a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart index 10cd15b3..5322806f 100644 --- a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart @@ -12,6 +12,7 @@ import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; import 'package:hive_ce_generator/src/helper/helper.dart'; import 'package:source_gen/source_gen.dart'; +import 'package:hive_ce_generator/src/helper/hive_converter_helper.dart'; import 'package:hive_ce_generator/src/helper/type_helper.dart'; /// TODO: Document this! @@ -19,9 +20,11 @@ class ClassAdapterBuilder extends AdapterBuilder { /// TODO: Document this! const ClassAdapterBuilder( super.cls, - super.getters, + super.getters, { super.setters, - ); + super.specConverters, + super.globalConverters, + }); /// [TypeChecker] for [HiveList]. final hiveListChecker = @@ -101,7 +104,7 @@ class ClassAdapterBuilder extends AdapterBuilder { String _value(DartType type, AdapterField field) { final variable = 'fields[${field.index}]'; - final value = _cast(type, variable); + final value = _cast(type, variable, field); final annotationDefaultIsNull = field.annotationDefault?.isNull ?? true; final constructorDefaultIsNull = field.constructorDefault == null; @@ -120,17 +123,22 @@ class ClassAdapterBuilder extends AdapterBuilder { return '$variable == null ? $defaultValue : $value'; } - String _cast(DartType type, String variable) { + String _cast(DartType type, String variable, AdapterField field) { + final converter = _converterFor(type, field); + if (converter != null) { + return _fromHive(converter, type, variable); + } + final suffix = _suffixFromType(type); if (hiveListChecker.isAssignableFromType(type)) { return '($variable as HiveList$suffix)$suffix.castHiveList()'; } else if (setChecker.isAssignableFromType(type)) { - return '($variable as Set$suffix)${_castIterable(type)}'; + return '($variable as Set$suffix)${_castIterable(type, field)}'; } else if (iterableChecker.isAssignableFromType(type) && !isUint8List(type)) { - return '($variable as List$suffix)${_castIterable(type)}'; + return '($variable as List$suffix)${_castIterable(type, field)}'; } else if (mapChecker.isAssignableFromType(type)) { - return '($variable as Map$suffix)${_castMap(type)}'; + return '($variable as Map$suffix)${_castMap(type, field)}'; } else if (type.isDartCoreInt) { return '($variable as num$suffix)$suffix.toInt()'; } else if (type.isDartCoreDouble) { @@ -140,6 +148,26 @@ class ClassAdapterBuilder extends AdapterBuilder { } } + String _fromHive( + HiveConverterMatch converter, + DartType targetType, + String variable, + ) { + final hiveType = converter.hiveType.getDisplayString(); + final access = converter.accessString; + + final targetIsNullable = + targetType.nullabilitySuffix == NullabilitySuffix.question; + final hiveIsNullable = + converter.hiveType.nullabilitySuffix == NullabilitySuffix.question; + + if (targetIsNullable && !hiveIsNullable) { + return '$variable == null ? null : $access.fromHive($variable as $hiveType)'; + } + + return '$access.fromHive($variable as $hiveType)'; + } + /// TODO: Document this! bool isMapOrIterable(DartType type) { return iterableChecker.isAssignableFromType(type) || @@ -151,11 +179,12 @@ class ClassAdapterBuilder extends AdapterBuilder { return uint8ListChecker.isExactlyType(type); } - String _castIterable(DartType type) { + String _castIterable(DartType type, AdapterField field) { final paramType = type as ParameterizedType; final arg = paramType.typeArguments.first; final suffix = _accessorSuffixFromType(type); - if (isMapOrIterable(arg) && !isUint8List(arg)) { + if (isMapOrIterable(arg) && !isUint8List(arg) || + _converterFor(arg, field) != null) { var cast = ''; // Using assignable because Set? is not exactly Set if (setChecker.isAssignableFromType(type)) { @@ -165,20 +194,23 @@ class ClassAdapterBuilder extends AdapterBuilder { cast = '.toList()'; } - return '$suffix.map((e) => ${_cast(arg, 'e')})$cast'; + return '$suffix.map((e) => ${_cast(arg, 'e', field)})$cast'; } else { return '$suffix.cast<${arg.getPrefixedDisplayString(cls.library)}>()'; } } - String _castMap(DartType type) { + String _castMap(DartType type, AdapterField field) { final paramType = type as ParameterizedType; final arg1 = paramType.typeArguments[0]; final arg2 = paramType.typeArguments[1]; final suffix = _accessorSuffixFromType(type); - if (isMapOrIterable(arg1) || isMapOrIterable(arg2)) { + if (isMapOrIterable(arg1) || + isMapOrIterable(arg2) || + _converterFor(arg1, field) != null || + _converterFor(arg2, field) != null) { return '$suffix.map((dynamic k, dynamic v)=>' - 'MapEntry(${_cast(arg1, 'k')},${_cast(arg2, 'v')}))'; + 'MapEntry(${_cast(arg1, 'k', field)},${_cast(arg2, 'v', field)}))'; } else { return '$suffix.cast<${arg1.getPrefixedDisplayString(cls.library)}, ' '${arg2.getPrefixedDisplayString(cls.library)}>()'; @@ -195,12 +227,79 @@ class ClassAdapterBuilder extends AdapterBuilder { for (final field in getters) { code.writeln(''' ..writeByte(${field.index}) - ..write(obj.${field.name})'''); + ..write(${_writeValue(field.type, 'obj.${field.name}', field)})'''); } code.writeln(';'); return code.toString(); } + + String _writeValue(DartType type, String expression, AdapterField field) { + final converter = _converterFor(type, field); + if (converter != null) { + return _toHive(converter, type, expression); + } + + if (setChecker.isAssignableFromType(type) || + (iterableChecker.isAssignableFromType(type) && !isUint8List(type))) { + final paramType = type as ParameterizedType; + final arg = paramType.typeArguments.first; + final inner = _writeValue(arg, 'e', field); + if (inner != 'e') { + final suffix = _accessorSuffixFromType(type); + if (setChecker.isAssignableFromType(type)) { + return '$expression$suffix.map((e) => $inner).toSet()'; + } + return '$expression$suffix.map((e) => $inner).toList()'; + } + } else if (mapChecker.isAssignableFromType(type)) { + final paramType = type as ParameterizedType; + final arg1 = paramType.typeArguments[0]; + final arg2 = paramType.typeArguments[1]; + final key = _writeValue(arg1, 'k', field); + final value = _writeValue(arg2, 'v', field); + if (key != 'k' || value != 'v') { + final suffix = _accessorSuffixFromType(type); + return '$expression$suffix.map((dynamic k, dynamic v) => ' + 'MapEntry($key, $value))'; + } + } + + return expression; + } + + String _toHive( + HiveConverterMatch converter, + DartType targetType, + String expression, + ) { + final access = converter.accessString; + final targetIsNullable = + targetType.nullabilitySuffix == NullabilitySuffix.question; + final fieldIsNullable = + converter.fieldType.nullabilitySuffix == NullabilitySuffix.question; + + if (targetIsNullable && !fieldIsNullable) { + final nonNullType = converter.fieldType.getDisplayString(); + return '$expression == null ' + '? null ' + ': $access.toHive($expression as $nonNullType)'; + } + + return '$access.toHive($expression)'; + } + + HiveConverterMatch? _converterFor(DartType type, AdapterField field) { + return findHiveConverter( + targetType: type, + field: AdapterFieldContext.from( + field: field, + cls: cls, + ), + specConverters: specConverters, + globalConverters: globalConverters, + ); + } } /// Suffix to use when accessing a field in [type]. diff --git a/hive_generator/lib/src/generator/adapters_generator.dart b/hive_generator/lib/src/generator/adapters_generator.dart index b21405ba..26776a4c 100644 --- a/hive_generator/lib/src/generator/adapters_generator.dart +++ b/hive_generator/lib/src/generator/adapters_generator.dart @@ -96,6 +96,8 @@ class AdaptersGenerator extends GeneratorForAnnotation { typeId: schemaType.typeId, schema: schemaType, ignoredFields: spec.ignoredFields, + specConverters: spec.converters, + globalConverters: revived.converters, ); content.write(result.content); diff --git a/hive_generator/lib/src/generator/type_adapter_generator.dart b/hive_generator/lib/src/generator/type_adapter_generator.dart index f75f6437..e4f3dba2 100644 --- a/hive_generator/lib/src/generator/type_adapter_generator.dart +++ b/hive_generator/lib/src/generator/type_adapter_generator.dart @@ -1,3 +1,4 @@ +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:build/build.dart'; import 'package:hive_ce/hive_ce.dart'; @@ -38,6 +39,8 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { String? adapterName, HiveSchemaType? schema, Set ignoredFields = const {}, + List specConverters = const [], + List globalConverters = const [], }) { final cls = getClass(element); final getAccessorsResult = getAccessors( @@ -57,7 +60,13 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { adapterName ??= generateAdapterName(cls.displayName); final builder = cls.thisType.isEnum ? EnumAdapterBuilder(cls, getters) - : ClassAdapterBuilder(cls, getters, setters); + : ClassAdapterBuilder( + cls, + getters, + setters: setters, + specConverters: specConverters, + globalConverters: globalConverters, + ); final content = ''' class $adapterName extends TypeAdapter<${cls.displayName}> { diff --git a/hive_generator/lib/src/helper/hive_converter_helper.dart b/hive_generator/lib/src/helper/hive_converter_helper.dart new file mode 100644 index 00000000..2b364992 --- /dev/null +++ b/hive_generator/lib/src/helper/hive_converter_helper.dart @@ -0,0 +1,377 @@ +import 'package:analyzer/dart/constant/value.dart'; +import 'package:analyzer/dart/element/element.dart'; +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; +import 'package:collection/collection.dart'; +import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; +import 'package:meta/meta.dart'; +import 'package:source_gen/source_gen.dart'; + +/// Information about a matched HiveConverter for code generation +@immutable +class HiveConverterMatch { + /// Expression used to access the converter instance + final String accessString; + + /// The Dart field type converted from/to ([HiveConverter]'s [T]) + final DartType fieldType; + + /// The Hive-stored type ([HiveConverter]'s [S]), with type args substituted + final DartType hiveType; + + /// Whether the converter class is generic and was instantiated with type args + final bool isGeneric; + + /// Constructor + const HiveConverterMatch({ + required this.accessString, + required this.fieldType, + required this.hiveType, + required this.isGeneric, + }); +} + +const _hiveConverterChecker = TypeChecker.typeNamedLiterally( + 'HiveConverter', + inPackage: 'hive_ce', +); + +/// Resolve a [HiveConverter] for [targetType] from field/class annotations and +/// configured converter lists +/// +/// Matching priority (same as json_serializable's [JsonConverter]): +/// 1. Annotations on the field getter +/// 2. Annotations on the field +/// 3. Annotations on the class +/// 4. [specConverters] (from [AdapterSpec.converters]) +/// 5. [globalConverters] (from [GenerateAdapters.converters]) +HiveConverterMatch? findHiveConverter({ + required DartType targetType, + required AdapterFieldContext field, + List specConverters = const [], + List globalConverters = const [], +}) { + List<_HiveConverterCandidate> converterMatches( + List items, + ) => + items + .map( + (annotation) => _compatibleMatch( + targetType, + annotation, + annotation.computeConstantValue(), + ), + ) + .whereType<_HiveConverterCandidate>() + .toList(); + + var matchingAnnotations = converterMatches( + field.getterAnnotations, + ); + + if (matchingAnnotations.isEmpty) { + matchingAnnotations = converterMatches(field.fieldAnnotations); + } + + if (matchingAnnotations.isEmpty) { + matchingAnnotations = converterMatches(field.classAnnotations); + } + + if (matchingAnnotations.isEmpty) { + matchingAnnotations = specConverters + .map((e) => _compatibleMatch(targetType, null, e)) + .whereType<_HiveConverterCandidate>() + .toList(); + } + + if (matchingAnnotations.isEmpty) { + matchingAnnotations = globalConverters + .map((e) => _compatibleMatch(targetType, null, e)) + .whereType<_HiveConverterCandidate>() + .toList(); + } + + return _converterFrom(matchingAnnotations, targetType); +} + +/// Field context needed to look up converter annotations +@immutable +class AdapterFieldContext { + /// Annotations on the getter + final List getterAnnotations; + + /// Annotations on the field/variable + final List fieldAnnotations; + + /// Annotations on the enclosing class + final List classAnnotations; + + /// Constructor + const AdapterFieldContext({ + required this.getterAnnotations, + required this.fieldAnnotations, + required this.classAnnotations, + }); + + /// Create from an [AdapterField] and its enclosing class + factory AdapterFieldContext.from({ + required AdapterField field, + required InterfaceElement cls, + }) { + final variable = field.element.variable; + return AdapterFieldContext( + getterAnnotations: variable.getter?.metadata.annotations ?? const [], + fieldAnnotations: variable.metadata.annotations, + classAnnotations: cls.metadata.annotations, + ); + } +} + +HiveConverterMatch? _converterFrom( + List<_HiveConverterCandidate> matchingAnnotations, + DartType targetType, +) { + if (matchingAnnotations.isEmpty) return null; + + if (matchingAnnotations.length > 1) { + throw InvalidGenerationSourceError( + 'Found more than one matching converter for ' + '`${targetType.getDisplayString()}`.', + element: matchingAnnotations[1].elementAnnotation?.element, + ); + } + + final match = matchingAnnotations.single; + final annotationElement = match.elementAnnotation?.element; + if (annotationElement is PropertyAccessorElement) { + final enclosing = annotationElement.enclosingElement; + + final accessorName = annotationElement.name; + if (accessorName == null) { + throw InvalidGenerationSourceError( + 'Could not resolve converter accessor name.', + element: annotationElement, + ); + } + final accessString = enclosing is ClassElement + ? '${enclosing.name}.$accessorName' + : accessorName; + + return HiveConverterMatch( + accessString: accessString, + fieldType: match.fieldType, + hiveType: match.hiveType, + isGeneric: false, + ); + } + + final reviver = ConstantReader(match.annotation).revive(); + if (reviver.namedArguments.isNotEmpty || + reviver.positionalArguments.isNotEmpty) { + throw InvalidGenerationSourceError( + 'Converters with constructor arguments are not supported.', + element: match.elementAnnotation?.element, + ); + } + + final annotationType = match.annotation.type; + final annotationTypeElement = annotationType?.element; + final className = annotationTypeElement?.name; + if (className == null) { + throw InvalidGenerationSourceError( + 'Could not resolve converter class name.', + element: match.elementAnnotation?.element, + ); + } + final accessor = reviver.accessor.isEmpty ? '' : '.${reviver.accessor}'; + + if (match.genericTypeArgs != null) { + return HiveConverterMatch( + accessString: '$className<${match.genericTypeArgs}>$accessor()', + fieldType: match.fieldType, + hiveType: match.hiveType, + isGeneric: true, + ); + } + + return HiveConverterMatch( + accessString: 'const $className$accessor()', + fieldType: match.fieldType, + hiveType: match.hiveType, + isGeneric: false, + ); +} + +@immutable +class _HiveConverterCandidate { + final DartObject annotation; + final DartType fieldType; + final DartType hiveType; + final ElementAnnotation? elementAnnotation; + final String? genericTypeArgs; + + const _HiveConverterCandidate( + this.elementAnnotation, + this.annotation, + this.hiveType, + this.genericTypeArgs, + this.fieldType, + ); +} + +_HiveConverterCandidate? _compatibleMatch( + DartType targetType, + ElementAnnotation? annotation, + DartObject? constantValue, +) { + if (constantValue == null || constantValue.isNull) return null; + + final converterType = constantValue.type; + if (converterType is! InterfaceType) return null; + + final converterClassElement = converterType.element; + if (converterClassElement is! ClassElement) return null; + + final hiveConverterSuper = converterClassElement.allSupertypes + .where((e) => _hiveConverterChecker.isExactly(e.element)) + .singleOrNull; + + if (hiveConverterSuper == null) return null; + + assert(hiveConverterSuper.typeArguments.length == 2); + + final fieldType = hiveConverterSuper.typeArguments[0]; + final hiveType = hiveConverterSuper.typeArguments[1]; + final nonNullableTarget = _promoteNonNullable(targetType); + + // Exact match (allow T for T?) + if (fieldType == targetType || fieldType == nonNullableTarget) { + return _HiveConverterCandidate( + annotation, + constantValue, + hiveType, + null, + fieldType, + ); + } + + // Generic converter where T is a type parameter of the annotated class + if (fieldType is TypeParameterType && targetType is TypeParameterType) { + if (converterClassElement.typeParameters.length > 1) { + throw InvalidGenerationSourceError( + '`HiveConverter` implementations can have no more than one type ' + 'argument. `${converterClassElement.name}` has ' + '${converterClassElement.typeParameters.length}.', + element: converterClassElement, + ); + } + + return _HiveConverterCandidate( + annotation, + constantValue, + hiveType, + '${targetType.element.name}${_nullabilitySuffix(targetType)}', + fieldType, + ); + } + + // Generic converter such as `HiveConverter, List>` matching + // `IList` by unifying type parameters + final bindings = {}; + if (_unify(fieldType, nonNullableTarget, bindings)) { + final typeArgs = converterClassElement.typeParameters.map((param) { + final bound = bindings[param]; + if (bound == null) { + throw InvalidGenerationSourceError( + 'Could not infer type argument `${param.name}` for converter ' + '`${converterClassElement.name}` when matching ' + '`${targetType.getDisplayString()}`.', + element: converterClassElement, + ); + } + return bound.getDisplayString(); + }).join(', '); + + return _HiveConverterCandidate( + annotation, + constantValue, + _substitute(hiveType, bindings), + typeArgs.isEmpty ? null : typeArgs, + _substitute(fieldType, bindings), + ); + } + + return null; +} + +bool _unify( + DartType pattern, + DartType concrete, + Map bindings, +) { + if (pattern is TypeParameterType) { + final element = pattern.element; + final existing = bindings[element]; + if (existing != null) { + return existing == concrete || + existing == _promoteNonNullable(concrete) || + _promoteNonNullable(existing) == _promoteNonNullable(concrete); + } + bindings[element] = concrete; + return true; + } + + if (pattern is InterfaceType && concrete is InterfaceType) { + if (pattern.element != concrete.element) return false; + if (pattern.typeArguments.length != concrete.typeArguments.length) { + return false; + } + for (var i = 0; i < pattern.typeArguments.length; i++) { + if (!_unify( + pattern.typeArguments[i], + concrete.typeArguments[i], + bindings, + )) { + return false; + } + } + return true; + } + + return pattern == concrete || + pattern == _promoteNonNullable(concrete) || + _promoteNonNullable(pattern) == _promoteNonNullable(concrete); +} + +DartType _substitute( + DartType type, + Map bindings, +) { + if (type is TypeParameterType) { + return bindings[type.element] ?? type; + } + if (type is InterfaceType && type.typeArguments.isNotEmpty) { + final args = + type.typeArguments.map((arg) => _substitute(arg, bindings)).toList(); + return type.element.instantiate( + typeArguments: args, + nullabilitySuffix: type.nullabilitySuffix, + ); + } + return type; +} + +DartType _promoteNonNullable(DartType type) { + if (type.nullabilitySuffix == NullabilitySuffix.none) return type; + if (type is InterfaceType) { + return type.element.instantiate( + typeArguments: type.typeArguments, + nullabilitySuffix: NullabilitySuffix.none, + ); + } + return type; +} + +String _nullabilitySuffix(DartType type) { + return type.nullabilitySuffix == NullabilitySuffix.question ? '?' : ''; +} diff --git a/hive_generator/lib/src/model/revived_generate_adapter.dart b/hive_generator/lib/src/model/revived_generate_adapter.dart index 008e74b8..a05d2906 100644 --- a/hive_generator/lib/src/model/revived_generate_adapter.dart +++ b/hive_generator/lib/src/model/revived_generate_adapter.dart @@ -15,6 +15,9 @@ class RevivedGenerateAdapters { /// The reserved type ids final Set reservedTypeIds; + /// Converters shared by all specs + final List converters; + /// Revive a GenerateAdapters annotation RevivedGenerateAdapters(ConstantReader annotation) : specs = annotation @@ -28,7 +31,8 @@ class RevivedGenerateAdapters { .setValue .map((e) => e.toIntValue()) .whereType() - .toSet(); + .toSet(), + converters = _readConverters(annotation); } /// A revived adapter spec @@ -40,8 +44,15 @@ class RevivedAdapterSpec { /// Fields that should be ignored final Set ignoredFields; + /// Converters for this type + final List converters; + /// Constructor - const RevivedAdapterSpec({required this.type, required this.ignoredFields}); + const RevivedAdapterSpec({ + required this.type, + required this.ignoredFields, + this.converters = const [], + }); /// Create a [RevivedAdapterSpec] from a [DartObject] factory RevivedAdapterSpec.fromObject(DartObject object) { @@ -55,6 +66,16 @@ class RevivedAdapterSpec { .whereType() .toSet(); - return RevivedAdapterSpec(type: type, ignoredFields: ignoredFields); + return RevivedAdapterSpec( + type: type, + ignoredFields: ignoredFields, + converters: _readConverters(reader), + ); } } + +List _readConverters(ConstantReader reader) { + final converters = reader.read('converters'); + if (converters.isNull) return const []; + return converters.listValue; +} diff --git a/hive_generator/pubspec.yaml b/hive_generator/pubspec.yaml index 88430a87..3773cc20 100644 --- a/hive_generator/pubspec.yaml +++ b/hive_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: hive_ce_generator description: Extension for Hive. Automatically generates TypeAdapters to store any class. -version: 1.11.3 +version: 1.12.0 homepage: https://github.com/IO-Design-Team/hive_ce/tree/main/hive_generator environment: diff --git a/hive_generator/test/hive_converter_test.dart b/hive_generator/test/hive_converter_test.dart new file mode 100644 index 00000000..f51fe0bc --- /dev/null +++ b/hive_generator/test/hive_converter_test.dart @@ -0,0 +1,239 @@ +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +const directives = ''' +import 'package:hive_ce/hive_ce.dart'; +part 'hive_adapters.g.dart'; +'''; + +const epochConverter = ''' +class EpochDateTimeConverter implements HiveConverter { + const EpochDateTimeConverter(); + + @override + DateTime fromHive(int hive) => + DateTime.fromMillisecondsSinceEpoch(hive); + + @override + int toHive(DateTime object) => object.millisecondsSinceEpoch; +} +'''; + +void main() { + group('HiveConverter', () { + test('applies converter from GenerateAdapters.converters', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [EpochDateTimeConverter()], +) +class Event { + const Event(this.time); + + final DateTime time; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const EpochDateTimeConverter().fromHive(fields[0] as int)', + 'const EpochDateTimeConverter().toHive(obj.time)', + ]), + }, + ); + }); + + test('applies converter from field annotation', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters([AdapterSpec()]) +class Event { + const Event(this.time); + + @EpochDateTimeConverter() + final DateTime time; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const EpochDateTimeConverter().fromHive(fields[0] as int)', + 'const EpochDateTimeConverter().toHive(obj.time)', + ]), + }, + ); + }); + + test('applies converter from class annotation', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters([AdapterSpec()]) +@EpochDateTimeConverter() +class Event { + const Event(this.time); + + final DateTime time; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const EpochDateTimeConverter().fromHive(fields[0] as int)', + 'const EpochDateTimeConverter().toHive(obj.time)', + ]), + }, + ); + }); + + test('applies converter from AdapterSpec.converters', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters([ + AdapterSpec(converters: [EpochDateTimeConverter()]), +]) +class Event { + const Event(this.time); + + final DateTime time; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const EpochDateTimeConverter().fromHive(fields[0] as int)', + 'const EpochDateTimeConverter().toHive(obj.time)', + ]), + }, + ); + }); + + test('applies generic converter with inferred type arguments', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +class WrappedList { + const WrappedList(this.values); + final List values; +} + +class WrappedListConverter + implements HiveConverter, List> { + const WrappedListConverter(); + + @override + WrappedList fromHive(List hive) => WrappedList(hive); + + @override + List toHive(WrappedList object) => object.values; +} + +@GenerateAdapters( + [AdapterSpec()], + converters: [WrappedListConverter()], +) +class Box { + const Box(this.items); + + final WrappedList items; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'WrappedListConverter().fromHive(fields[0] as List)', + 'WrappedListConverter().toHive(obj.items)', + ]), + }, + ); + }); + + test('applies converter inside nested collections', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [EpochDateTimeConverter()], +) +class Timeline { + const Timeline(this.times); + + final List times; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'const EpochDateTimeConverter().fromHive(e as int)', + 'const EpochDateTimeConverter().toHive(e)', + ]), + }, + ); + }); + + test('handles nullable fields with non-nullable converter', () { + expectGeneration( + input: { + ...pubspec(), + 'lib/hive/hive_adapters.dart': ''' +$directives + +$epochConverter + +@GenerateAdapters( + [AdapterSpec()], + converters: [EpochDateTimeConverter()], +) +class Event { + const Event(this.time); + + final DateTime? time; +} +''', + }, + output: { + 'lib/hive/hive_adapters.g.dart': const ContainsAll([ + 'fields[0] == null', + 'const EpochDateTimeConverter().fromHive(fields[0] as int)', + 'obj.time == null', + 'const EpochDateTimeConverter().toHive(obj.time as DateTime)', + ]), + }, + ); + }); + }); +} diff --git a/hive_generator/test/test_utils.dart b/hive_generator/test/test_utils.dart index 9e9c88c2..00ce53a5 100644 --- a/hive_generator/test/test_utils.dart +++ b/hive_generator/test/test_utils.dart @@ -9,11 +9,21 @@ const schemaComment = HiveSchema.comment; const fileExists = true; const fileDoesNotExist = false; +/// Match generated file content that contains all of [snippets] +class ContainsAll { + /// The snippets that must appear in the file + final List snippets; + + /// Constructor + const ContainsAll(this.snippets); +} + /// Expect the given input generates the given output /// /// About [output] -/// - A [String] value will check if the file exists and contains the given +/// - A [String] value will check if the file exists and equals the given /// content +/// - A [ContainsAll] value will check that the file contains all snippets /// - [fileExists] will check if the file exists /// - [fileDoesNotExist] will check if the file does not exist /// @@ -44,12 +54,27 @@ void expectGeneration({ } return; } else { - expect(result.exitCode, 0); + expect( + result.exitCode, + 0, + reason: result.stdout.toString(), + ); } for (final MapEntry(:key, :value) in output.entries) { final file = File(path.join(projectRoot, key)); - expect(file.existsSync(), value == true || value is String); + expect( + file.existsSync(), + value == true || value is String || value is ContainsAll, + ); + + if (value is ContainsAll) { + final content = file.readAsStringSync(); + for (final snippet in value.snippets) { + expect(content, contains(snippet)); + } + continue; + } if (value is! String) continue; expect(file.readAsStringSync(), value); From 2086706afd794a41709674b31bdc9c8f9798498d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 20:48:11 +0000 Subject: [PATCH 2/4] refactor: limit HiveConverter registration to GenerateAdapters Remove AdapterSpec.converters and field/class annotation support. App-local Hive serialization only needs one converter per type, so GenerateAdapters.converters is sufficient. Co-authored-by: Rexios --- hive/CHANGELOG.md | 2 +- .../src/annotations/generate_adapters.dart | 16 +- hive/lib/src/annotations/hive_converter.dart | 39 ++--- hive_generator/CHANGELOG.md | 3 +- .../example/lib/hive/hive_adapters.g.dart | 52 +++---- .../example/lib/hive/hive_adapters.g.yaml | 8 +- .../src/adapter_builder/adapter_builder.dart | 8 +- .../class_adapter_builder.dart | 50 +++---- .../lib/src/generator/adapters_generator.dart | 3 +- .../src/generator/type_adapter_generator.dart | 6 +- .../lib/src/helper/hive_converter_helper.dart | 141 ++---------------- .../src/model/revived_generate_adapter.dart | 10 +- hive_generator/test/hive_converter_test.dart | 82 ---------- 13 files changed, 86 insertions(+), 334 deletions(-) diff --git a/hive/CHANGELOG.md b/hive/CHANGELOG.md index 666a7218..fb5e2b21 100644 --- a/hive/CHANGELOG.md +++ b/hive/CHANGELOG.md @@ -1,7 +1,7 @@ ## 2.20.0 - Adds `HiveConverter` for custom field conversion during TypeAdapter generation (similar to `json_serializable`'s `JsonConverter`) -- Adds `converters` parameter to `GenerateAdapters` and `AdapterSpec` +- Adds `converters` parameter to `GenerateAdapters` ## 2.19.3 diff --git a/hive/lib/src/annotations/generate_adapters.dart b/hive/lib/src/annotations/generate_adapters.dart index 4bbdae1d..c4e5ed1a 100644 --- a/hive/lib/src/annotations/generate_adapters.dart +++ b/hive/lib/src/annotations/generate_adapters.dart @@ -27,8 +27,8 @@ class GenerateAdapters { /// A list of [HiveConverter]s to apply when generating adapters for [specs] /// - /// These converters are used when a matching converter is not found on the - /// field or class. See [HiveConverter] for details. + /// A converter is selected by matching its type parameter [T] against each + /// field type. See [HiveConverter] for details. final List converters; } @@ -37,10 +37,7 @@ class GenerateAdapters { class AdapterSpec { /// Constructor // coverage:ignore-start - const AdapterSpec({ - this.ignoredFields = const {}, - this.converters = const [], - }); + const AdapterSpec({this.ignoredFields = const {}}); // coverage:ignore-end /// Fields that should be ignored @@ -48,11 +45,4 @@ class AdapterSpec { /// This should only be used to simplify migrations from `HiveType` /// annotations. Model classes should only contain fields to be persisted. final Set ignoredFields; - - /// A list of [HiveConverter]s to apply when generating an adapter for [T] - /// - /// These converters take precedence over [GenerateAdapters.converters], but - /// are overridden by converters placed on the class or field. See - /// [HiveConverter] for details. - final List converters; } diff --git a/hive/lib/src/annotations/hive_converter.dart b/hive/lib/src/annotations/hive_converter.dart index eb4ab2ca..a7e639f0 100644 --- a/hive/lib/src/annotations/hive_converter.dart +++ b/hive/lib/src/annotations/hive_converter.dart @@ -8,45 +8,26 @@ import 'package:meta/meta.dart'; /// write natively (such as [String], [int], [List], [Set], or [Map]) or a type /// with a registered [TypeAdapter]. /// -/// [HiveConverter]s can be placed either on the class: +/// Pass converter instances to [GenerateAdapters.converters]: /// /// ```dart -/// class MyHiveConverter extends HiveConverter { -/// const MyHiveConverter(); +/// class EpochDateTimeConverter implements HiveConverter { +/// const EpochDateTimeConverter(); /// /// @override -/// Value fromHive(String hive) => Value(hive); +/// DateTime fromHive(int hive) => +/// DateTime.fromMillisecondsSinceEpoch(hive); /// /// @override -/// String toHive(Value object) => object.toString(); +/// int toHive(DateTime object) => object.millisecondsSinceEpoch; /// } /// -/// @GenerateAdapters([AdapterSpec()]) -/// @MyHiveConverter() -/// class Example { -/// final Value property; -/// } -/// ``` -/// -/// or on a property: -/// -/// ```dart -/// @GenerateAdapters([AdapterSpec()]) -/// class Example { -/// @MyHiveConverter() -/// final Value property; -/// } -/// ``` -/// -/// Or finally, passed to the [GenerateAdapters] or [AdapterSpec] annotation: -/// -/// ```dart /// @GenerateAdapters( -/// [AdapterSpec()], -/// converters: [MyHiveConverter()], +/// [AdapterSpec()], +/// converters: [EpochDateTimeConverter()], /// ) -/// class Example { -/// final Value property; +/// class Event { +/// final DateTime time; /// } /// ``` @immutable diff --git a/hive_generator/CHANGELOG.md b/hive_generator/CHANGELOG.md index 4793e6cf..e24ef438 100644 --- a/hive_generator/CHANGELOG.md +++ b/hive_generator/CHANGELOG.md @@ -1,7 +1,6 @@ ## 1.12.0 -- Adds support for `HiveConverter` in adapter generation (similar to `json_serializable`'s `JsonConverter`) -- Converters can be placed on fields, classes, `AdapterSpec.converters`, or `GenerateAdapters.converters` +- Adds support for `HiveConverter` via `GenerateAdapters.converters` (similar to `json_serializable`'s `JsonConverter`) - Supports generic converters with inferred type arguments (e.g. `WrappedListConverter`) ## 1.11.3 diff --git a/hive_generator/example/lib/hive/hive_adapters.g.dart b/hive_generator/example/lib/hive/hive_adapters.g.dart index 72af63cc..8c0653ac 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.dart +++ b/hive_generator/example/lib/hive/hive_adapters.g.dart @@ -157,32 +157,6 @@ class ClassSpec3Adapter extends TypeAdapter { typeId == other.typeId; } -class ClassSpec4Adapter extends TypeAdapter { - @override - final typeId = 54; - - @override - ClassSpec4 read(BinaryReader reader) { - reader.readByte(); - return ClassSpec4(); - } - - @override - void write(BinaryWriter writer, ClassSpec4 obj) { - writer.writeByte(0); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is ClassSpec4Adapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - class ClassSpec5Adapter extends TypeAdapter { @override final typeId = 55; @@ -227,3 +201,29 @@ class ClassSpec5Adapter extends TypeAdapter { runtimeType == other.runtimeType && typeId == other.typeId; } + +class ClassSpec4Adapter extends TypeAdapter { + @override + final typeId = 54; + + @override + ClassSpec4 read(BinaryReader reader) { + reader.readByte(); + return ClassSpec4(); + } + + @override + void write(BinaryWriter writer, ClassSpec4 obj) { + writer.writeByte(0); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is ClassSpec4Adapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/hive_generator/example/lib/hive/hive_adapters.g.yaml b/hive_generator/example/lib/hive/hive_adapters.g.yaml index 964502d1..ac96b589 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.yaml +++ b/hive_generator/example/lib/hive/hive_adapters.g.yaml @@ -39,10 +39,6 @@ types: fields: value: index: 0 - ClassSpec4: - typeId: 54 - nextIndex: 0 - fields: {} ClassSpec5: typeId: 55 nextIndex: 2 @@ -51,3 +47,7 @@ types: index: 0 optionalTime: index: 1 + ClassSpec4: + typeId: 54 + nextIndex: 0 + fields: {} diff --git a/hive_generator/lib/src/adapter_builder/adapter_builder.dart b/hive_generator/lib/src/adapter_builder/adapter_builder.dart index c67e9004..096597bb 100644 --- a/hive_generator/lib/src/adapter_builder/adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/adapter_builder.dart @@ -49,19 +49,15 @@ abstract class AdapterBuilder { /// TODO: Document this! final List setters; - /// Converters from [AdapterSpec.converters] - final List specConverters; - /// Converters from [GenerateAdapters.converters] - final List globalConverters; + final List converters; /// TODO: Document this! const AdapterBuilder( this.cls, this.getters, { this.setters = const [], - this.specConverters = const [], - this.globalConverters = const [], + this.converters = const [], }); /// TODO: Document this! diff --git a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart index 5322806f..c7c4dc69 100644 --- a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart @@ -22,8 +22,7 @@ class ClassAdapterBuilder extends AdapterBuilder { super.cls, super.getters, { super.setters, - super.specConverters, - super.globalConverters, + super.converters, }); /// [TypeChecker] for [HiveList]. @@ -104,7 +103,7 @@ class ClassAdapterBuilder extends AdapterBuilder { String _value(DartType type, AdapterField field) { final variable = 'fields[${field.index}]'; - final value = _cast(type, variable, field); + final value = _cast(type, variable); final annotationDefaultIsNull = field.annotationDefault?.isNull ?? true; final constructorDefaultIsNull = field.constructorDefault == null; @@ -123,8 +122,8 @@ class ClassAdapterBuilder extends AdapterBuilder { return '$variable == null ? $defaultValue : $value'; } - String _cast(DartType type, String variable, AdapterField field) { - final converter = _converterFor(type, field); + String _cast(DartType type, String variable) { + final converter = _converterFor(type); if (converter != null) { return _fromHive(converter, type, variable); } @@ -133,12 +132,12 @@ class ClassAdapterBuilder extends AdapterBuilder { if (hiveListChecker.isAssignableFromType(type)) { return '($variable as HiveList$suffix)$suffix.castHiveList()'; } else if (setChecker.isAssignableFromType(type)) { - return '($variable as Set$suffix)${_castIterable(type, field)}'; + return '($variable as Set$suffix)${_castIterable(type)}'; } else if (iterableChecker.isAssignableFromType(type) && !isUint8List(type)) { - return '($variable as List$suffix)${_castIterable(type, field)}'; + return '($variable as List$suffix)${_castIterable(type)}'; } else if (mapChecker.isAssignableFromType(type)) { - return '($variable as Map$suffix)${_castMap(type, field)}'; + return '($variable as Map$suffix)${_castMap(type)}'; } else if (type.isDartCoreInt) { return '($variable as num$suffix)$suffix.toInt()'; } else if (type.isDartCoreDouble) { @@ -179,12 +178,12 @@ class ClassAdapterBuilder extends AdapterBuilder { return uint8ListChecker.isExactlyType(type); } - String _castIterable(DartType type, AdapterField field) { + String _castIterable(DartType type) { final paramType = type as ParameterizedType; final arg = paramType.typeArguments.first; final suffix = _accessorSuffixFromType(type); if (isMapOrIterable(arg) && !isUint8List(arg) || - _converterFor(arg, field) != null) { + _converterFor(arg) != null) { var cast = ''; // Using assignable because Set? is not exactly Set if (setChecker.isAssignableFromType(type)) { @@ -194,23 +193,23 @@ class ClassAdapterBuilder extends AdapterBuilder { cast = '.toList()'; } - return '$suffix.map((e) => ${_cast(arg, 'e', field)})$cast'; + return '$suffix.map((e) => ${_cast(arg, 'e')})$cast'; } else { return '$suffix.cast<${arg.getPrefixedDisplayString(cls.library)}>()'; } } - String _castMap(DartType type, AdapterField field) { + String _castMap(DartType type) { final paramType = type as ParameterizedType; final arg1 = paramType.typeArguments[0]; final arg2 = paramType.typeArguments[1]; final suffix = _accessorSuffixFromType(type); if (isMapOrIterable(arg1) || isMapOrIterable(arg2) || - _converterFor(arg1, field) != null || - _converterFor(arg2, field) != null) { + _converterFor(arg1) != null || + _converterFor(arg2) != null) { return '$suffix.map((dynamic k, dynamic v)=>' - 'MapEntry(${_cast(arg1, 'k', field)},${_cast(arg2, 'v', field)}))'; + 'MapEntry(${_cast(arg1, 'k')},${_cast(arg2, 'v')}))'; } else { return '$suffix.cast<${arg1.getPrefixedDisplayString(cls.library)}, ' '${arg2.getPrefixedDisplayString(cls.library)}>()'; @@ -227,15 +226,15 @@ class ClassAdapterBuilder extends AdapterBuilder { for (final field in getters) { code.writeln(''' ..writeByte(${field.index}) - ..write(${_writeValue(field.type, 'obj.${field.name}', field)})'''); + ..write(${_writeValue(field.type, 'obj.${field.name}')})'''); } code.writeln(';'); return code.toString(); } - String _writeValue(DartType type, String expression, AdapterField field) { - final converter = _converterFor(type, field); + String _writeValue(DartType type, String expression) { + final converter = _converterFor(type); if (converter != null) { return _toHive(converter, type, expression); } @@ -244,7 +243,7 @@ class ClassAdapterBuilder extends AdapterBuilder { (iterableChecker.isAssignableFromType(type) && !isUint8List(type))) { final paramType = type as ParameterizedType; final arg = paramType.typeArguments.first; - final inner = _writeValue(arg, 'e', field); + final inner = _writeValue(arg, 'e'); if (inner != 'e') { final suffix = _accessorSuffixFromType(type); if (setChecker.isAssignableFromType(type)) { @@ -256,8 +255,8 @@ class ClassAdapterBuilder extends AdapterBuilder { final paramType = type as ParameterizedType; final arg1 = paramType.typeArguments[0]; final arg2 = paramType.typeArguments[1]; - final key = _writeValue(arg1, 'k', field); - final value = _writeValue(arg2, 'v', field); + final key = _writeValue(arg1, 'k'); + final value = _writeValue(arg2, 'v'); if (key != 'k' || value != 'v') { final suffix = _accessorSuffixFromType(type); return '$expression$suffix.map((dynamic k, dynamic v) => ' @@ -289,15 +288,10 @@ class ClassAdapterBuilder extends AdapterBuilder { return '$access.toHive($expression)'; } - HiveConverterMatch? _converterFor(DartType type, AdapterField field) { + HiveConverterMatch? _converterFor(DartType type) { return findHiveConverter( targetType: type, - field: AdapterFieldContext.from( - field: field, - cls: cls, - ), - specConverters: specConverters, - globalConverters: globalConverters, + converters: converters, ); } } diff --git a/hive_generator/lib/src/generator/adapters_generator.dart b/hive_generator/lib/src/generator/adapters_generator.dart index 26776a4c..c2f70e92 100644 --- a/hive_generator/lib/src/generator/adapters_generator.dart +++ b/hive_generator/lib/src/generator/adapters_generator.dart @@ -96,8 +96,7 @@ class AdaptersGenerator extends GeneratorForAnnotation { typeId: schemaType.typeId, schema: schemaType, ignoredFields: spec.ignoredFields, - specConverters: spec.converters, - globalConverters: revived.converters, + converters: revived.converters, ); content.write(result.content); diff --git a/hive_generator/lib/src/generator/type_adapter_generator.dart b/hive_generator/lib/src/generator/type_adapter_generator.dart index e4f3dba2..4a7a61fc 100644 --- a/hive_generator/lib/src/generator/type_adapter_generator.dart +++ b/hive_generator/lib/src/generator/type_adapter_generator.dart @@ -39,8 +39,7 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { String? adapterName, HiveSchemaType? schema, Set ignoredFields = const {}, - List specConverters = const [], - List globalConverters = const [], + List converters = const [], }) { final cls = getClass(element); final getAccessorsResult = getAccessors( @@ -64,8 +63,7 @@ class TypeAdapterGenerator extends GeneratorForAnnotation { cls, getters, setters: setters, - specConverters: specConverters, - globalConverters: globalConverters, + converters: converters, ); final content = ''' diff --git a/hive_generator/lib/src/helper/hive_converter_helper.dart b/hive_generator/lib/src/helper/hive_converter_helper.dart index 2b364992..f1fbc32a 100644 --- a/hive_generator/lib/src/helper/hive_converter_helper.dart +++ b/hive_generator/lib/src/helper/hive_converter_helper.dart @@ -3,7 +3,6 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; -import 'package:hive_ce_generator/src/adapter_builder/adapter_builder.dart'; import 'package:meta/meta.dart'; import 'package:source_gen/source_gen.dart'; @@ -13,10 +12,10 @@ class HiveConverterMatch { /// Expression used to access the converter instance final String accessString; - /// The Dart field type converted from/to ([HiveConverter]'s [T]) + /// The Dart field type converted from/to (HiveConverter's `T`) final DartType fieldType; - /// The Hive-stored type ([HiveConverter]'s [S]), with type args substituted + /// The Hive-stored type (HiveConverter's `S`), with type args substituted final DartType hiveType; /// Whether the converter class is generic and was instantiated with type args @@ -36,141 +35,34 @@ const _hiveConverterChecker = TypeChecker.typeNamedLiterally( inPackage: 'hive_ce', ); -/// Resolve a [HiveConverter] for [targetType] from field/class annotations and -/// configured converter lists -/// -/// Matching priority (same as json_serializable's [JsonConverter]): -/// 1. Annotations on the field getter -/// 2. Annotations on the field -/// 3. Annotations on the class -/// 4. [specConverters] (from [AdapterSpec.converters]) -/// 5. [globalConverters] (from [GenerateAdapters.converters]) +/// Resolve a HiveConverter for [targetType] from [GenerateAdapters.converters] HiveConverterMatch? findHiveConverter({ required DartType targetType, - required AdapterFieldContext field, - List specConverters = const [], - List globalConverters = const [], + required List converters, }) { - List<_HiveConverterCandidate> converterMatches( - List items, - ) => - items - .map( - (annotation) => _compatibleMatch( - targetType, - annotation, - annotation.computeConstantValue(), - ), - ) - .whereType<_HiveConverterCandidate>() - .toList(); - - var matchingAnnotations = converterMatches( - field.getterAnnotations, - ); - - if (matchingAnnotations.isEmpty) { - matchingAnnotations = converterMatches(field.fieldAnnotations); - } - - if (matchingAnnotations.isEmpty) { - matchingAnnotations = converterMatches(field.classAnnotations); - } - - if (matchingAnnotations.isEmpty) { - matchingAnnotations = specConverters - .map((e) => _compatibleMatch(targetType, null, e)) - .whereType<_HiveConverterCandidate>() - .toList(); - } - - if (matchingAnnotations.isEmpty) { - matchingAnnotations = globalConverters - .map((e) => _compatibleMatch(targetType, null, e)) - .whereType<_HiveConverterCandidate>() - .toList(); - } + final matches = converters + .map((e) => _compatibleMatch(targetType, e)) + .whereType<_HiveConverterCandidate>() + .toList(); - return _converterFrom(matchingAnnotations, targetType); -} - -/// Field context needed to look up converter annotations -@immutable -class AdapterFieldContext { - /// Annotations on the getter - final List getterAnnotations; - - /// Annotations on the field/variable - final List fieldAnnotations; - - /// Annotations on the enclosing class - final List classAnnotations; + if (matches.isEmpty) return null; - /// Constructor - const AdapterFieldContext({ - required this.getterAnnotations, - required this.fieldAnnotations, - required this.classAnnotations, - }); - - /// Create from an [AdapterField] and its enclosing class - factory AdapterFieldContext.from({ - required AdapterField field, - required InterfaceElement cls, - }) { - final variable = field.element.variable; - return AdapterFieldContext( - getterAnnotations: variable.getter?.metadata.annotations ?? const [], - fieldAnnotations: variable.metadata.annotations, - classAnnotations: cls.metadata.annotations, - ); - } -} - -HiveConverterMatch? _converterFrom( - List<_HiveConverterCandidate> matchingAnnotations, - DartType targetType, -) { - if (matchingAnnotations.isEmpty) return null; - - if (matchingAnnotations.length > 1) { + if (matches.length > 1) { throw InvalidGenerationSourceError( 'Found more than one matching converter for ' '`${targetType.getDisplayString()}`.', - element: matchingAnnotations[1].elementAnnotation?.element, ); } - final match = matchingAnnotations.single; - final annotationElement = match.elementAnnotation?.element; - if (annotationElement is PropertyAccessorElement) { - final enclosing = annotationElement.enclosingElement; - - final accessorName = annotationElement.name; - if (accessorName == null) { - throw InvalidGenerationSourceError( - 'Could not resolve converter accessor name.', - element: annotationElement, - ); - } - final accessString = enclosing is ClassElement - ? '${enclosing.name}.$accessorName' - : accessorName; - - return HiveConverterMatch( - accessString: accessString, - fieldType: match.fieldType, - hiveType: match.hiveType, - isGeneric: false, - ); - } + return _converterFrom(matches.single); +} +HiveConverterMatch _converterFrom(_HiveConverterCandidate match) { final reviver = ConstantReader(match.annotation).revive(); if (reviver.namedArguments.isNotEmpty || reviver.positionalArguments.isNotEmpty) { throw InvalidGenerationSourceError( 'Converters with constructor arguments are not supported.', - element: match.elementAnnotation?.element, ); } @@ -180,7 +72,6 @@ HiveConverterMatch? _converterFrom( if (className == null) { throw InvalidGenerationSourceError( 'Could not resolve converter class name.', - element: match.elementAnnotation?.element, ); } final accessor = reviver.accessor.isEmpty ? '' : '.${reviver.accessor}'; @@ -207,11 +98,9 @@ class _HiveConverterCandidate { final DartObject annotation; final DartType fieldType; final DartType hiveType; - final ElementAnnotation? elementAnnotation; final String? genericTypeArgs; const _HiveConverterCandidate( - this.elementAnnotation, this.annotation, this.hiveType, this.genericTypeArgs, @@ -221,7 +110,6 @@ class _HiveConverterCandidate { _HiveConverterCandidate? _compatibleMatch( DartType targetType, - ElementAnnotation? annotation, DartObject? constantValue, ) { if (constantValue == null || constantValue.isNull) return null; @@ -247,7 +135,6 @@ _HiveConverterCandidate? _compatibleMatch( // Exact match (allow T for T?) if (fieldType == targetType || fieldType == nonNullableTarget) { return _HiveConverterCandidate( - annotation, constantValue, hiveType, null, @@ -267,7 +154,6 @@ _HiveConverterCandidate? _compatibleMatch( } return _HiveConverterCandidate( - annotation, constantValue, hiveType, '${targetType.element.name}${_nullabilitySuffix(targetType)}', @@ -293,7 +179,6 @@ _HiveConverterCandidate? _compatibleMatch( }).join(', '); return _HiveConverterCandidate( - annotation, constantValue, _substitute(hiveType, bindings), typeArgs.isEmpty ? null : typeArgs, diff --git a/hive_generator/lib/src/model/revived_generate_adapter.dart b/hive_generator/lib/src/model/revived_generate_adapter.dart index a05d2906..b0742438 100644 --- a/hive_generator/lib/src/model/revived_generate_adapter.dart +++ b/hive_generator/lib/src/model/revived_generate_adapter.dart @@ -44,14 +44,10 @@ class RevivedAdapterSpec { /// Fields that should be ignored final Set ignoredFields; - /// Converters for this type - final List converters; - /// Constructor const RevivedAdapterSpec({ required this.type, required this.ignoredFields, - this.converters = const [], }); /// Create a [RevivedAdapterSpec] from a [DartObject] @@ -66,11 +62,7 @@ class RevivedAdapterSpec { .whereType() .toSet(); - return RevivedAdapterSpec( - type: type, - ignoredFields: ignoredFields, - converters: _readConverters(reader), - ); + return RevivedAdapterSpec(type: type, ignoredFields: ignoredFields); } } diff --git a/hive_generator/test/hive_converter_test.dart b/hive_generator/test/hive_converter_test.dart index f51fe0bc..6b5c6220 100644 --- a/hive_generator/test/hive_converter_test.dart +++ b/hive_generator/test/hive_converter_test.dart @@ -51,88 +51,6 @@ class Event { ); }); - test('applies converter from field annotation', () { - expectGeneration( - input: { - ...pubspec(), - 'lib/hive/hive_adapters.dart': ''' -$directives - -$epochConverter - -@GenerateAdapters([AdapterSpec()]) -class Event { - const Event(this.time); - - @EpochDateTimeConverter() - final DateTime time; -} -''', - }, - output: { - 'lib/hive/hive_adapters.g.dart': const ContainsAll([ - 'const EpochDateTimeConverter().fromHive(fields[0] as int)', - 'const EpochDateTimeConverter().toHive(obj.time)', - ]), - }, - ); - }); - - test('applies converter from class annotation', () { - expectGeneration( - input: { - ...pubspec(), - 'lib/hive/hive_adapters.dart': ''' -$directives - -$epochConverter - -@GenerateAdapters([AdapterSpec()]) -@EpochDateTimeConverter() -class Event { - const Event(this.time); - - final DateTime time; -} -''', - }, - output: { - 'lib/hive/hive_adapters.g.dart': const ContainsAll([ - 'const EpochDateTimeConverter().fromHive(fields[0] as int)', - 'const EpochDateTimeConverter().toHive(obj.time)', - ]), - }, - ); - }); - - test('applies converter from AdapterSpec.converters', () { - expectGeneration( - input: { - ...pubspec(), - 'lib/hive/hive_adapters.dart': ''' -$directives - -$epochConverter - -@GenerateAdapters([ - AdapterSpec(converters: [EpochDateTimeConverter()]), -]) -class Event { - const Event(this.time); - - final DateTime time; -} -''', - }, - output: { - 'lib/hive/hive_adapters.g.dart': const ContainsAll([ - 'const EpochDateTimeConverter().fromHive(fields[0] as int)', - 'const EpochDateTimeConverter().toHive(obj.time)', - ]), - }, - ); - }); - test('applies generic converter with inferred type arguments', () { expectGeneration( input: { From 0e99db900cbcd23632e56400f5998f448c35597d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 20:50:47 +0000 Subject: [PATCH 3/4] docs: use Uri instead of DateTime in HiveConverter examples DateTime is already supported by Hive natively, so Uri is a clearer demonstration of when a converter is needed. Co-authored-by: Rexios --- hive/lib/src/annotations/hive_converter.dart | 17 +++--- .../example/lib/hive/hive_adapters.dart | 16 ++--- .../example/lib/hive/hive_adapters.g.dart | 12 ++-- .../example/lib/hive/hive_adapters.g.yaml | 4 +- hive_generator/test/hive_converter_test.dart | 61 +++++++++---------- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/hive/lib/src/annotations/hive_converter.dart b/hive/lib/src/annotations/hive_converter.dart index a7e639f0..bb7ffeca 100644 --- a/hive/lib/src/annotations/hive_converter.dart +++ b/hive/lib/src/annotations/hive_converter.dart @@ -11,23 +11,22 @@ import 'package:meta/meta.dart'; /// Pass converter instances to [GenerateAdapters.converters]: /// /// ```dart -/// class EpochDateTimeConverter implements HiveConverter { -/// const EpochDateTimeConverter(); +/// class UriConverter implements HiveConverter { +/// const UriConverter(); /// /// @override -/// DateTime fromHive(int hive) => -/// DateTime.fromMillisecondsSinceEpoch(hive); +/// Uri fromHive(String hive) => Uri.parse(hive); /// /// @override -/// int toHive(DateTime object) => object.millisecondsSinceEpoch; +/// String toHive(Uri object) => object.toString(); /// } /// /// @GenerateAdapters( -/// [AdapterSpec()], -/// converters: [EpochDateTimeConverter()], +/// [AdapterSpec()], +/// converters: [UriConverter()], /// ) -/// class Event { -/// final DateTime time; +/// class Website { +/// final Uri url; /// } /// ``` @immutable diff --git a/hive_generator/example/lib/hive/hive_adapters.dart b/hive_generator/example/lib/hive/hive_adapters.dart index fd6cb8e6..f4396d96 100644 --- a/hive_generator/example/lib/hive/hive_adapters.dart +++ b/hive_generator/example/lib/hive/hive_adapters.dart @@ -11,19 +11,19 @@ import 'package:meta/meta.dart'; AdapterSpec(), ], firstTypeId: 50, - converters: [EpochDateTimeConverter()], + converters: [UriConverter()], ) part 'hive_adapters.g.dart'; /// Example converter matching json_serializable's JsonConverter pattern -class EpochDateTimeConverter implements HiveConverter { - const EpochDateTimeConverter(); +class UriConverter implements HiveConverter { + const UriConverter(); @override - DateTime fromHive(int hive) => DateTime.fromMillisecondsSinceEpoch(hive); + Uri fromHive(String hive) => Uri.parse(hive); @override - int toHive(DateTime object) => object.millisecondsSinceEpoch; + String toHive(Uri object) => object.toString(); } @immutable @@ -60,8 +60,8 @@ enum EnumSpec { @immutable class ClassSpec5 { - final DateTime time; - final DateTime? optionalTime; + final Uri url; + final Uri? optionalUrl; - const ClassSpec5(this.time, this.optionalTime); + const ClassSpec5(this.url, this.optionalUrl); } diff --git a/hive_generator/example/lib/hive/hive_adapters.g.dart b/hive_generator/example/lib/hive/hive_adapters.g.dart index 8c0653ac..fb6fbb21 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.dart +++ b/hive_generator/example/lib/hive/hive_adapters.g.dart @@ -168,10 +168,10 @@ class ClassSpec5Adapter extends TypeAdapter { for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return ClassSpec5( - const EpochDateTimeConverter().fromHive(fields[0] as int), + const UriConverter().fromHive(fields[0] as String), fields[1] == null ? null - : const EpochDateTimeConverter().fromHive(fields[1] as int), + : const UriConverter().fromHive(fields[1] as String), ); } @@ -180,14 +180,12 @@ class ClassSpec5Adapter extends TypeAdapter { writer ..writeByte(2) ..writeByte(0) - ..write(const EpochDateTimeConverter().toHive(obj.time)) + ..write(const UriConverter().toHive(obj.url)) ..writeByte(1) ..write( - obj.optionalTime == null + obj.optionalUrl == null ? null - : const EpochDateTimeConverter().toHive( - obj.optionalTime as DateTime, - ), + : const UriConverter().toHive(obj.optionalUrl as Uri), ); } diff --git a/hive_generator/example/lib/hive/hive_adapters.g.yaml b/hive_generator/example/lib/hive/hive_adapters.g.yaml index ac96b589..e0c391d8 100644 --- a/hive_generator/example/lib/hive/hive_adapters.g.yaml +++ b/hive_generator/example/lib/hive/hive_adapters.g.yaml @@ -43,9 +43,9 @@ types: typeId: 55 nextIndex: 2 fields: - time: + url: index: 0 - optionalTime: + optionalUrl: index: 1 ClassSpec4: typeId: 54 diff --git a/hive_generator/test/hive_converter_test.dart b/hive_generator/test/hive_converter_test.dart index 6b5c6220..0b141973 100644 --- a/hive_generator/test/hive_converter_test.dart +++ b/hive_generator/test/hive_converter_test.dart @@ -7,16 +7,15 @@ import 'package:hive_ce/hive_ce.dart'; part 'hive_adapters.g.dart'; '''; -const epochConverter = ''' -class EpochDateTimeConverter implements HiveConverter { - const EpochDateTimeConverter(); +const uriConverter = ''' +class UriConverter implements HiveConverter { + const UriConverter(); @override - DateTime fromHive(int hive) => - DateTime.fromMillisecondsSinceEpoch(hive); + Uri fromHive(String hive) => Uri.parse(hive); @override - int toHive(DateTime object) => object.millisecondsSinceEpoch; + String toHive(Uri object) => object.toString(); } '''; @@ -29,23 +28,23 @@ void main() { 'lib/hive/hive_adapters.dart': ''' $directives -$epochConverter +$uriConverter @GenerateAdapters( - [AdapterSpec()], - converters: [EpochDateTimeConverter()], + [AdapterSpec()], + converters: [UriConverter()], ) -class Event { - const Event(this.time); +class Website { + const Website(this.url); - final DateTime time; + final Uri url; } ''', }, output: { 'lib/hive/hive_adapters.g.dart': const ContainsAll([ - 'const EpochDateTimeConverter().fromHive(fields[0] as int)', - 'const EpochDateTimeConverter().toHive(obj.time)', + 'const UriConverter().fromHive(fields[0] as String)', + 'const UriConverter().toHive(obj.url)', ]), }, ); @@ -101,23 +100,23 @@ class Box { 'lib/hive/hive_adapters.dart': ''' $directives -$epochConverter +$uriConverter @GenerateAdapters( - [AdapterSpec()], - converters: [EpochDateTimeConverter()], + [AdapterSpec()], + converters: [UriConverter()], ) -class Timeline { - const Timeline(this.times); +class LinkList { + const LinkList(this.urls); - final List times; + final List urls; } ''', }, output: { 'lib/hive/hive_adapters.g.dart': const ContainsAll([ - 'const EpochDateTimeConverter().fromHive(e as int)', - 'const EpochDateTimeConverter().toHive(e)', + 'const UriConverter().fromHive(e as String)', + 'const UriConverter().toHive(e)', ]), }, ); @@ -130,25 +129,25 @@ class Timeline { 'lib/hive/hive_adapters.dart': ''' $directives -$epochConverter +$uriConverter @GenerateAdapters( - [AdapterSpec()], - converters: [EpochDateTimeConverter()], + [AdapterSpec()], + converters: [UriConverter()], ) -class Event { - const Event(this.time); +class Website { + const Website(this.url); - final DateTime? time; + final Uri? url; } ''', }, output: { 'lib/hive/hive_adapters.g.dart': const ContainsAll([ 'fields[0] == null', - 'const EpochDateTimeConverter().fromHive(fields[0] as int)', - 'obj.time == null', - 'const EpochDateTimeConverter().toHive(obj.time as DateTime)', + 'const UriConverter().fromHive(fields[0] as String)', + 'obj.url == null', + 'const UriConverter().toHive(obj.url as Uri)', ]), }, ); From f0df1c684c98bce84e74e7664eaaf3f3fb17a2cc Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 20:58:20 +0000 Subject: [PATCH 4/4] refactor: simplify HiveConverter matching and codegen Keep the json_serializable-like user API, but drop annotation/revive edge cases and replace recursive type unification with a smaller exact + identity-generic matcher. Co-authored-by: Rexios --- .../class_adapter_builder.dart | 100 ++---- .../lib/src/helper/hive_converter_helper.dart | 291 +++++------------- 2 files changed, 114 insertions(+), 277 deletions(-) diff --git a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart index c7c4dc69..c6faf898 100644 --- a/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart +++ b/hive_generator/lib/src/adapter_builder/class_adapter_builder.dart @@ -125,7 +125,9 @@ class ClassAdapterBuilder extends AdapterBuilder { String _cast(DartType type, String variable) { final converter = _converterFor(type); if (converter != null) { - return _fromHive(converter, type, variable); + final call = + '${converter.access}.fromHive($variable as ${converter.hiveType})'; + return _nullSafe(type, variable, call); } final suffix = _suffixFromType(type); @@ -147,26 +149,6 @@ class ClassAdapterBuilder extends AdapterBuilder { } } - String _fromHive( - HiveConverterMatch converter, - DartType targetType, - String variable, - ) { - final hiveType = converter.hiveType.getDisplayString(); - final access = converter.accessString; - - final targetIsNullable = - targetType.nullabilitySuffix == NullabilitySuffix.question; - final hiveIsNullable = - converter.hiveType.nullabilitySuffix == NullabilitySuffix.question; - - if (targetIsNullable && !hiveIsNullable) { - return '$variable == null ? null : $access.fromHive($variable as $hiveType)'; - } - - return '$access.fromHive($variable as $hiveType)'; - } - /// TODO: Document this! bool isMapOrIterable(DartType type) { return iterableChecker.isAssignableFromType(type) || @@ -236,64 +218,48 @@ class ClassAdapterBuilder extends AdapterBuilder { String _writeValue(DartType type, String expression) { final converter = _converterFor(type); if (converter != null) { - return _toHive(converter, type, expression); + final nullable = type.nullabilitySuffix == NullabilitySuffix.question; + final value = + nullable ? '$expression as ${converter.fieldType}' : expression; + return _nullSafe( + type, + expression, + '${converter.access}.toHive($value)', + ); } if (setChecker.isAssignableFromType(type) || (iterableChecker.isAssignableFromType(type) && !isUint8List(type))) { - final paramType = type as ParameterizedType; - final arg = paramType.typeArguments.first; + final arg = (type as ParameterizedType).typeArguments.first; final inner = _writeValue(arg, 'e'); - if (inner != 'e') { - final suffix = _accessorSuffixFromType(type); - if (setChecker.isAssignableFromType(type)) { - return '$expression$suffix.map((e) => $inner).toSet()'; - } - return '$expression$suffix.map((e) => $inner).toList()'; - } - } else if (mapChecker.isAssignableFromType(type)) { - final paramType = type as ParameterizedType; - final arg1 = paramType.typeArguments[0]; - final arg2 = paramType.typeArguments[1]; - final key = _writeValue(arg1, 'k'); - final value = _writeValue(arg2, 'v'); - if (key != 'k' || value != 'v') { - final suffix = _accessorSuffixFromType(type); - return '$expression$suffix.map((dynamic k, dynamic v) => ' - 'MapEntry($key, $value))'; - } + if (inner == 'e') return expression; + final suffix = _accessorSuffixFromType(type); + final mapped = '$expression$suffix.map((e) => $inner)'; + return setChecker.isAssignableFromType(type) + ? '$mapped.toSet()' + : '$mapped.toList()'; } - return expression; - } - - String _toHive( - HiveConverterMatch converter, - DartType targetType, - String expression, - ) { - final access = converter.accessString; - final targetIsNullable = - targetType.nullabilitySuffix == NullabilitySuffix.question; - final fieldIsNullable = - converter.fieldType.nullabilitySuffix == NullabilitySuffix.question; - - if (targetIsNullable && !fieldIsNullable) { - final nonNullType = converter.fieldType.getDisplayString(); - return '$expression == null ' - '? null ' - ': $access.toHive($expression as $nonNullType)'; + if (mapChecker.isAssignableFromType(type)) { + final args = (type as ParameterizedType).typeArguments; + final key = _writeValue(args[0], 'k'); + final value = _writeValue(args[1], 'v'); + if (key == 'k' && value == 'v') return expression; + final suffix = _accessorSuffixFromType(type); + return '$expression$suffix.map((dynamic k, dynamic v) => ' + 'MapEntry($key, $value))'; } - return '$access.toHive($expression)'; + return expression; } - HiveConverterMatch? _converterFor(DartType type) { - return findHiveConverter( - targetType: type, - converters: converters, - ); + String _nullSafe(DartType type, String expression, String call) { + if (type.nullabilitySuffix != NullabilitySuffix.question) return call; + return '$expression == null ? null : $call'; } + + HiveConverterMatch? _converterFor(DartType type) => + findHiveConverter(type, converters); } /// Suffix to use when accessing a field in [type]. diff --git a/hive_generator/lib/src/helper/hive_converter_helper.dart b/hive_generator/lib/src/helper/hive_converter_helper.dart index f1fbc32a..cfc02e38 100644 --- a/hive_generator/lib/src/helper/hive_converter_helper.dart +++ b/hive_generator/lib/src/helper/hive_converter_helper.dart @@ -3,260 +3,131 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:collection/collection.dart'; -import 'package:meta/meta.dart'; import 'package:source_gen/source_gen.dart'; -/// Information about a matched HiveConverter for code generation -@immutable +/// A [HiveConverter] matched to a field type for code generation class HiveConverterMatch { - /// Expression used to access the converter instance - final String accessString; + /// Converter instance expression, e.g. `const UriConverter()` + final String access; - /// The Dart field type converted from/to (HiveConverter's `T`) - final DartType fieldType; + /// Stored Hive type display string, e.g. `String` + final String hiveType; - /// The Hive-stored type (HiveConverter's `S`), with type args substituted - final DartType hiveType; - - /// Whether the converter class is generic and was instantiated with type args - final bool isGeneric; + /// Field type display string, e.g. `Uri` + final String fieldType; /// Constructor const HiveConverterMatch({ - required this.accessString, - required this.fieldType, + required this.access, required this.hiveType, - required this.isGeneric, + required this.fieldType, }); } -const _hiveConverterChecker = TypeChecker.typeNamedLiterally( +const _checker = TypeChecker.typeNamedLiterally( 'HiveConverter', inPackage: 'hive_ce', ); -/// Resolve a HiveConverter for [targetType] from [GenerateAdapters.converters] -HiveConverterMatch? findHiveConverter({ - required DartType targetType, - required List converters, -}) { - final matches = converters - .map((e) => _compatibleMatch(targetType, e)) - .whereType<_HiveConverterCandidate>() - .toList(); - - if (matches.isEmpty) return null; - - if (matches.length > 1) { - throw InvalidGenerationSourceError( - 'Found more than one matching converter for ' - '`${targetType.getDisplayString()}`.', - ); - } - - return _converterFrom(matches.single); -} - -HiveConverterMatch _converterFrom(_HiveConverterCandidate match) { - final reviver = ConstantReader(match.annotation).revive(); - if (reviver.namedArguments.isNotEmpty || - reviver.positionalArguments.isNotEmpty) { - throw InvalidGenerationSourceError( - 'Converters with constructor arguments are not supported.', - ); - } - - final annotationType = match.annotation.type; - final annotationTypeElement = annotationType?.element; - final className = annotationTypeElement?.name; - if (className == null) { - throw InvalidGenerationSourceError( - 'Could not resolve converter class name.', - ); - } - final accessor = reviver.accessor.isEmpty ? '' : '.${reviver.accessor}'; - - if (match.genericTypeArgs != null) { - return HiveConverterMatch( - accessString: '$className<${match.genericTypeArgs}>$accessor()', - fieldType: match.fieldType, - hiveType: match.hiveType, - isGeneric: true, - ); +/// Find a converter for [type] in [converters] from [GenerateAdapters] +HiveConverterMatch? findHiveConverter( + DartType type, + List converters, +) { + HiveConverterMatch? result; + for (final converter in converters) { + final match = _match(type, converter); + if (match == null) continue; + if (result != null) { + throw InvalidGenerationSourceError( + 'Found more than one matching converter for ' + '`${type.getDisplayString()}`.', + ); + } + result = match; } - - return HiveConverterMatch( - accessString: 'const $className$accessor()', - fieldType: match.fieldType, - hiveType: match.hiveType, - isGeneric: false, - ); -} - -@immutable -class _HiveConverterCandidate { - final DartObject annotation; - final DartType fieldType; - final DartType hiveType; - final String? genericTypeArgs; - - const _HiveConverterCandidate( - this.annotation, - this.hiveType, - this.genericTypeArgs, - this.fieldType, - ); + return result; } -_HiveConverterCandidate? _compatibleMatch( - DartType targetType, - DartObject? constantValue, -) { - if (constantValue == null || constantValue.isNull) return null; +HiveConverterMatch? _match(DartType target, DartObject object) { + final objectType = object.type; + if (objectType is! InterfaceType) return null; - final converterType = constantValue.type; - if (converterType is! InterfaceType) return null; + final element = objectType.element; + if (element is! ClassElement) return null; - final converterClassElement = converterType.element; - if (converterClassElement is! ClassElement) return null; - - final hiveConverterSuper = converterClassElement.allSupertypes - .where((e) => _hiveConverterChecker.isExactly(e.element)) + final hiveConverter = element.allSupertypes + .where((t) => _checker.isExactly(t.element)) .singleOrNull; + if (hiveConverter == null) return null; - if (hiveConverterSuper == null) return null; - - assert(hiveConverterSuper.typeArguments.length == 2); + final converted = hiveConverter.typeArguments[0]; + final stored = hiveConverter.typeArguments[1]; + final nonNullTarget = _nonNull(target); + final name = element.name; + if (name == null) return null; - final fieldType = hiveConverterSuper.typeArguments[0]; - final hiveType = hiveConverterSuper.typeArguments[1]; - final nonNullableTarget = _promoteNonNullable(targetType); - - // Exact match (allow T for T?) - if (fieldType == targetType || fieldType == nonNullableTarget) { - return _HiveConverterCandidate( - constantValue, - hiveType, - null, - fieldType, + if (converted == nonNullTarget) { + return HiveConverterMatch( + access: 'const $name()', + hiveType: stored.getDisplayString(), + fieldType: converted.getDisplayString(), ); } - // Generic converter where T is a type parameter of the annotated class - if (fieldType is TypeParameterType && targetType is TypeParameterType) { - if (converterClassElement.typeParameters.length > 1) { - throw InvalidGenerationSourceError( - '`HiveConverter` implementations can have no more than one type ' - 'argument. `${converterClassElement.name}` has ' - '${converterClassElement.typeParameters.length}.', - element: converterClassElement, - ); - } - - return _HiveConverterCandidate( - constantValue, - hiveType, - '${targetType.element.name}${_nullabilitySuffix(targetType)}', - fieldType, - ); + // e.g. `IListConverter` for `IList` + if (converted is! InterfaceType || + nonNullTarget is! InterfaceType || + converted.element != nonNullTarget.element || + element.typeParameters.isEmpty || + converted.typeArguments.length != element.typeParameters.length) { + return null; } - // Generic converter such as `HiveConverter, List>` matching - // `IList` by unifying type parameters - final bindings = {}; - if (_unify(fieldType, nonNullableTarget, bindings)) { - final typeArgs = converterClassElement.typeParameters.map((param) { - final bound = bindings[param]; - if (bound == null) { - throw InvalidGenerationSourceError( - 'Could not infer type argument `${param.name}` for converter ' - '`${converterClassElement.name}` when matching ' - '`${targetType.getDisplayString()}`.', - element: converterClassElement, - ); - } - return bound.getDisplayString(); - }).join(', '); - - return _HiveConverterCandidate( - constantValue, - _substitute(hiveType, bindings), - typeArgs.isEmpty ? null : typeArgs, - _substitute(fieldType, bindings), - ); + for (var i = 0; i < element.typeParameters.length; i++) { + final arg = converted.typeArguments[i]; + if (arg is! TypeParameterType || arg.element != element.typeParameters[i]) { + return null; + } } - return null; -} + final args = + nonNullTarget.typeArguments.map((t) => t.getDisplayString()).join(', '); + final bindings = { + for (var i = 0; i < element.typeParameters.length; i++) + element.typeParameters[i]: nonNullTarget.typeArguments[i], + }; -bool _unify( - DartType pattern, - DartType concrete, - Map bindings, -) { - if (pattern is TypeParameterType) { - final element = pattern.element; - final existing = bindings[element]; - if (existing != null) { - return existing == concrete || - existing == _promoteNonNullable(concrete) || - _promoteNonNullable(existing) == _promoteNonNullable(concrete); - } - bindings[element] = concrete; - return true; - } + return HiveConverterMatch( + access: '$name<$args>()', + hiveType: _substitute(stored, bindings).getDisplayString(), + fieldType: nonNullTarget.getDisplayString(), + ); +} - if (pattern is InterfaceType && concrete is InterfaceType) { - if (pattern.element != concrete.element) return false; - if (pattern.typeArguments.length != concrete.typeArguments.length) { - return false; - } - for (var i = 0; i < pattern.typeArguments.length; i++) { - if (!_unify( - pattern.typeArguments[i], - concrete.typeArguments[i], - bindings, - )) { - return false; - } - } - return true; +DartType _nonNull(DartType type) { + if (type.nullabilitySuffix == NullabilitySuffix.none) return type; + if (type is InterfaceType) { + return type.element.instantiate( + typeArguments: type.typeArguments, + nullabilitySuffix: NullabilitySuffix.none, + ); } - - return pattern == concrete || - pattern == _promoteNonNullable(concrete) || - _promoteNonNullable(pattern) == _promoteNonNullable(concrete); + return type; } DartType _substitute( DartType type, Map bindings, ) { - if (type is TypeParameterType) { - return bindings[type.element] ?? type; - } + if (type is TypeParameterType) return bindings[type.element] ?? type; if (type is InterfaceType && type.typeArguments.isNotEmpty) { - final args = - type.typeArguments.map((arg) => _substitute(arg, bindings)).toList(); return type.element.instantiate( - typeArguments: args, + typeArguments: [ + for (final arg in type.typeArguments) _substitute(arg, bindings), + ], nullabilitySuffix: type.nullabilitySuffix, ); } return type; } - -DartType _promoteNonNullable(DartType type) { - if (type.nullabilitySuffix == NullabilitySuffix.none) return type; - if (type is InterfaceType) { - return type.element.instantiate( - typeArguments: type.typeArguments, - nullabilitySuffix: NullabilitySuffix.none, - ); - } - return type; -} - -String _nullabilitySuffix(DartType type) { - return type.nullabilitySuffix == NullabilitySuffix.question ? '?' : ''; -}