diff --git a/CHANGELOG.md b/CHANGELOG.md index dc47eef5..fc2165bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ # 8.10.0 +- Allow `null` values in generic types when the type argument is nullable. For + example, if a class `Value` has a field of type `T`, a `Value` can + now hold a `null` value. - Stop generating unnecessary `new` keywords. - Stop generating explicit null checks in constructors: these are not needed with sound null safety. diff --git a/built_value_generator/lib/src/serializer_source_class.dart b/built_value_generator/lib/src/serializer_source_class.dart index ab82331e..6898b470 100644 --- a/built_value_generator/lib/src/serializer_source_class.dart +++ b/built_value_generator/lib/src/serializer_source_class.dart @@ -475,7 +475,7 @@ class $serializerImplName implements PrimitiveSerializer<$genericName> { String _generateRequiredFieldSerializers() { return fields - .where((field) => !field.isNullable) + .where((field) => !field.isNullable && !field.hasGenericType) .map((field) => "'${escapeString(field.wireName)}', " 'serializers.serialize(object.${field.name}, ' 'specifiedType: ' @@ -484,7 +484,7 @@ class $serializerImplName implements PrimitiveSerializer<$genericName> { } String _generateNullableFieldSerializers() { - var nullableFields = fields.where((field) => field.isNullable).toList(); + var nullableFields = fields.where((field) => field.isNullable || field.hasGenericType).toList(); if (nullableFields.isEmpty) return ''; return 'Object? value;' + diff --git a/built_value_generator/lib/src/serializer_source_field.dart b/built_value_generator/lib/src/serializer_source_field.dart index 90da3c16..738eae3a 100644 --- a/built_value_generator/lib/src/serializer_source_field.dart +++ b/built_value_generator/lib/src/serializer_source_field.dart @@ -86,6 +86,10 @@ abstract class SerializerSourceField element.getter?.returnType.nullabilitySuffix == NullabilitySuffix.question; + @memoized + bool get hasGenericType => + element.getter?.returnType is TypeParameterType; + @memoized bool get isNullable => hasNullableAnnotation || hasNullableType; diff --git a/built_value_generator/lib/src/value_source_class.dart b/built_value_generator/lib/src/value_source_class.dart index 16b18dd4..16e96fbe 100644 --- a/built_value_generator/lib/src/value_source_class.dart +++ b/built_value_generator/lib/src/value_source_class.dart @@ -1048,7 +1048,7 @@ abstract class ValueSourceClass if (!field.isNullable) { needsNullCheck.add(name); } - if (field.hasNullableGenericType) { + if (field.hasGenericType) { genericFields[name] = field.element.getter!.returnType.element!.displayName; } diff --git a/built_value_generator/lib/src/value_source_field.dart b/built_value_generator/lib/src/value_source_field.dart index 6763fde0..bf49dc09 100644 --- a/built_value_generator/lib/src/value_source_field.dart +++ b/built_value_generator/lib/src/value_source_field.dart @@ -109,12 +109,8 @@ abstract class ValueSourceField NullabilitySuffix.question; @memoized - bool get hasNullableGenericType => - element.getter?.returnType is TypeParameterType && - (element.getter!.returnType as TypeParameterType) - .bound - .nullabilitySuffix == - NullabilitySuffix.question; + bool get hasGenericType => + element.getter?.returnType is TypeParameterType; @memoized bool get isNullable => hasNullableAnnotation || hasNullableType; diff --git a/built_value_generator/lib/src/value_source_field.g.dart b/built_value_generator/lib/src/value_source_field.g.dart index 334de950..37878d20 100644 --- a/built_value_generator/lib/src/value_source_field.g.dart +++ b/built_value_generator/lib/src/value_source_field.g.dart @@ -24,7 +24,6 @@ class _$ValueSourceField extends ValueSourceField { bool? __isGetter; bool? __hasNullableAnnotation; bool? __hasNullableType; - bool? __hasNullableGenericType; bool? __isNullable; BuiltValueField? __builtValueField; bool? __builderFieldIsNormalField; @@ -69,10 +68,6 @@ class _$ValueSourceField extends ValueSourceField { @override bool get hasNullableType => __hasNullableType ??= super.hasNullableType; - @override - bool get hasNullableGenericType => - __hasNullableGenericType ??= super.hasNullableGenericType; - @override bool get isNullable => __isNullable ??= super.isNullable; diff --git a/end_to_end_test/lib/generics.g.dart b/end_to_end_test/lib/generics.g.dart index 6e44666a..93a4eb19 100644 --- a/end_to_end_test/lib/generics.g.dart +++ b/end_to_end_test/lib/generics.g.dart @@ -49,11 +49,14 @@ class _$GenericValueSerializer final parameterT = isUnderspecified ? FullType.object : specifiedType.parameters[0]; - final result = [ - 'value', - serializers.serialize(object.value, specifiedType: parameterT), - ]; - + final result = []; + Object? value; + value = object.value; + if (value != null) { + result + ..add('value') + ..add(serializers.serialize(value, specifiedType: parameterT)); + } return result; } @@ -105,11 +108,14 @@ class _$BoundGenericValueSerializer final parameterT = isUnderspecified ? FullType.object : specifiedType.parameters[0]; - final result = [ - 'value', - serializers.serialize(object.value, specifiedType: parameterT), - ]; + final result = []; Object? value; + value = object.value; + if (value != null) { + result + ..add('value') + ..add(serializers.serialize(value, specifiedType: parameterT)); + } value = object.nullableValue; if (value != null) { result @@ -175,11 +181,14 @@ class _$BoundNullableGenericValueSerializer final parameterT = isUnderspecified ? FullType.object : specifiedType.parameters[0]; - final result = [ - 'value', - serializers.serialize(object.value, specifiedType: parameterT), - ]; + final result = []; Object? value; + value = object.value; + if (value != null) { + result + ..add('value') + ..add(serializers.serialize(value, specifiedType: parameterT)); + } value = object.nullableValue; if (value != null) { result @@ -713,8 +722,10 @@ class GenericValueBuilder _$GenericValue _build() { final _$result = _$v ?? _$GenericValue._( - value: BuiltValueNullFieldError.checkNotNull( - value, r'GenericValue', 'value'), + value: null is T + ? value as T + : BuiltValueNullFieldError.checkNotNull( + value, r'GenericValue', 'value'), ); replace(_$result); return _$result; @@ -799,8 +810,10 @@ class InitializeGenericValueBuilder _$InitializeGenericValue _build() { final _$result = _$v ?? _$InitializeGenericValue._( - value: BuiltValueNullFieldError.checkNotNull( - value, r'InitializeGenericValue', 'value'), + value: null is T + ? value as T + : BuiltValueNullFieldError.checkNotNull( + value, r'InitializeGenericValue', 'value'), ); replace(_$result); return _$result; @@ -893,8 +906,10 @@ class BoundGenericValueBuilder _$BoundGenericValue _build() { final _$result = _$v ?? _$BoundGenericValue._( - value: BuiltValueNullFieldError.checkNotNull( - value, r'BoundGenericValue', 'value'), + value: null is T + ? value as T + : BuiltValueNullFieldError.checkNotNull( + value, r'BoundGenericValue', 'value'), nullableValue: nullableValue, ); replace(_$result); @@ -1614,8 +1629,10 @@ class _$CustomBuilderGenericValueBuilder _$CustomBuilderGenericValue _build() { final _$result = _$v ?? _$CustomBuilderGenericValue._( - value: BuiltValueNullFieldError.checkNotNull( - value, r'CustomBuilderGenericValue', 'value'), + value: null is T + ? value as T + : BuiltValueNullFieldError.checkNotNull( + value, r'CustomBuilderGenericValue', 'value'), ); replace(_$result); return _$result; diff --git a/end_to_end_test/test/generics_serializer_test.dart b/end_to_end_test/test/generics_serializer_test.dart index a1e50af8..d9f3d94a 100644 --- a/end_to_end_test/test/generics_serializer_test.dart +++ b/end_to_end_test/test/generics_serializer_test.dart @@ -67,6 +67,37 @@ void main() { }); }); + group('GenericValue with known specifiedType, correct builder and null', () { + var data = GenericValue(); + var specifiedType = const FullType(GenericValue, [FullType.nullable(int)]); + var serializersWithBuilder = (serializers.toBuilder() + ..addBuilderFactory(specifiedType, () => GenericValueBuilder())) + .build(); + var serialized = json.decode(json.encode([])) as Object; + + test('can be serialized', () { + expect( + serializersWithBuilder.serialize(data, specifiedType: specifiedType), + serialized); + }); + + test('can be deserialized', () { + expect( + serializersWithBuilder.deserialize(serialized, + specifiedType: specifiedType), + data); + }); + + test('keeps generic type on deserialization', () { + expect( + serializersWithBuilder + .deserialize(serialized, specifiedType: specifiedType) + .runtimeType + .toString(), + r'_$GenericValue'); + }); + }); + group('GenericValue with unknown specifiedType', () { var data = GenericValue((b) => b..value = 1); var serialized = json.decode(json.encode([ @@ -89,6 +120,26 @@ void main() { }); }); + group('GenericValue with unknown specifiedType null value', () { + var data = GenericValue(); + var serialized = json.decode(json.encode([ + 'GenericValue', + ])) as Object; + + test('can be serialized', () { + expect(serializers.serialize(data), serialized); + }); + + test('can be deserialized', () { + expect(serializers.deserialize(serialized), data); + }); + + test('loses generic type on deserialization', () { + expect(serializers.deserialize(serialized).runtimeType.toString(), + r'_$GenericValue'); + }); + }); + group('BoundGenericValue with known specifiedType but missing builder', () { var data = BoundGenericValue((b) => b..value = 1); var specifiedType = const FullType(BoundGenericValue, [FullType(int)]); diff --git a/end_to_end_test/test/generics_test.dart b/end_to_end_test/test/generics_test.dart index 71b7acf9..68ce20e2 100644 --- a/end_to_end_test/test/generics_test.dart +++ b/end_to_end_test/test/generics_test.dart @@ -18,6 +18,10 @@ void main() { throwsA(const TypeMatcher())); }); + test('does not throw on null for nullable fields on build', () { + GenericValue(); + }); + test('fields can be set via build constructor', () { final value = GenericValue((b) => b..value = 1); expect(value.value, 1);