Skip to content

Commit 316ad1d

Browse files
committed
Fix deserialization codegen with manually implemented builder.
The generated code now checks at runtime if the builder method auto-instantiated the nested builder. Add `FieldMixin` to reuse code between value generator and serializer generator.
1 parent ec705bf commit 316ad1d

14 files changed

+541
-58
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
# 8.4.3 (unreleased)
44

5+
- Fix generated deserialization code when there is a manually written builder
6+
with nullable fields.
57
- Drop dev dependency on `quiver`.
68
- Disable all linting of generated code.
79
- Change generated hash code implementation so it formats better when there are
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import 'package:analyzer/dart/analysis/results.dart';
2+
import 'package:analyzer/dart/ast/ast.dart';
3+
import 'package:analyzer/dart/element/element.dart';
4+
import 'package:built_value/built_value.dart';
5+
import 'package:built_value_generator/src/dart_types.dart';
6+
7+
/// Common logic between `ValueSourceField` and `SerializerSourceField`.
8+
mixin FieldMixin {
9+
FieldElement? get builderElement;
10+
ParsedLibraryResult get parsedLibrary;
11+
12+
bool get builderFieldExists => builderElement != null;
13+
14+
/// Gets the type of the field in a manually written builder.
15+
///
16+
/// Returns `dynamic` if there is no such field.
17+
///
18+
/// Goes via the AST if needed. This happens if the type has not been
19+
/// generated yet, which will be the case for a nested builder field if the
20+
/// builder is in this same library.
21+
@memoized
22+
String get fullBuildElementType {
23+
if (!builderFieldExists) return 'dynamic';
24+
25+
// Try to get a resolved type first, it's faster.
26+
var result = DartTypes.tryGetName(builderElement!.getter?.returnType,
27+
withNullabilitySuffix: true);
28+
29+
if (result != null && result != 'dynamic') return result;
30+
// Go via AST to allow use of unresolvable types not yet generated;
31+
// this includes generated Builder types.
32+
result = parsedLibrary
33+
.getElementDeclaration(builderElement!)
34+
?.node
35+
.parent
36+
?.childEntities
37+
.first
38+
.toString();
39+
40+
if (result != null) return result;
41+
42+
result = builderElement!.getter != null
43+
? (parsedLibrary.getElementDeclaration(builderElement!.getter!)?.node
44+
as MethodDeclaration?)
45+
?.returnType
46+
.toString()
47+
: null;
48+
49+
return result ?? 'dynamic';
50+
}
51+
}

built_value_generator/lib/src/serializer_source_class.dart

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,17 @@ abstract class SerializerSourceClass
309309

310310
String generateSerializer() {
311311
if (isBuiltValue) {
312+
// Add local $cast function if it will be used in code generated by
313+
// _generateFieldDeserializers.
314+
var needsCastFn = hasBuilder &&
315+
fields.any((f) =>
316+
f.builderFieldUsesNestedBuilder && f.builderFieldIsNullable);
317+
var maybeCastFn = needsCastFn
318+
? r'''
319+
T $cast<T>(dynamic any) => any as T;
320+
'''
321+
: '';
322+
312323
return '''
313324
class $serializerImplName implements StructuredSerializer<$genericName> {
314325
@override
@@ -331,6 +342,7 @@ class $serializerImplName implements StructuredSerializer<$genericName> {
331342
$genericName deserialize(Serializers serializers, Iterable<Object$orNull> serialized,
332343
{FullType specifiedType = FullType.unspecified}) {
333344
${_generateGenericsSerializerPreamble()}
345+
$maybeCastFn
334346
${fields.isEmpty ? 'return ${_generateNewBuilder()}.build();' : '''
335347
final result = ${_generateNewBuilder()};
336348
@@ -516,11 +528,31 @@ class $serializerImplName implements PrimitiveSerializer<$genericName> {
516528
// If cast exists and is not nullable.
517529
var maybeNotNull = !field.isNullable && cast.isNotEmpty ? notNull : '';
518530
if (field.builderFieldUsesNestedBuilder) {
519-
if (field.builderFieldAutoCreatesNestedBuilder || hasBuilder) {
531+
if (hasBuilder && field.builderFieldIsNullable) {
532+
// The manually implemented builder might or might not return a
533+
// builder. If it does, use `replace` as usual. If not, use `$cast`
534+
// to avoid computing a new type of static cast.
535+
return '''
536+
case '${escapeString(field.wireName)}':
537+
var maybeBuilder = result.${field.name};
538+
var fieldValue = serializers.deserialize(
539+
value, specifiedType: $fullType)$notNull $cast;
540+
if (maybeBuilder == null) {
541+
result.${field.name} = \$cast(fieldValue.toBuilder());
542+
} else {
543+
maybeBuilder.replace(fieldValue);
544+
}
545+
break;
546+
''';
547+
} else if (field.builderFieldAutoCreatesNestedBuilder) {
548+
// Use a looser cast where possible for calls to `replace`.
549+
final looseCast = field.generateCast(
550+
compilationUnit, _genericBoundsAsMap,
551+
forReplace: true);
520552
return '''
521553
case '${escapeString(field.wireName)}':
522554
result.${field.name}.replace(serializers.deserialize(
523-
value, specifiedType: $fullType)$notNull $cast);
555+
value, specifiedType: $fullType)$notNull $looseCast);
524556
break;
525557
''';
526558
} else {

built_value_generator/lib/src/serializer_source_field.dart

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import 'package:analyzer/dart/element/type.dart';
1212
import 'package:built_collection/built_collection.dart';
1313
import 'package:built_value/built_value.dart';
1414
import 'package:built_value_generator/src/dart_types.dart';
15+
import 'package:built_value_generator/src/field_mixin.dart';
1516
import 'package:built_value_generator/src/metadata.dart'
1617
show metadataToStringValue;
1718
import 'package:built_value_generator/src/strings.dart';
1819

1920
part 'serializer_source_field.g.dart';
2021

2122
abstract class SerializerSourceField
23+
with FieldMixin
2224
implements Built<SerializerSourceField, SerializerSourceFieldBuilder> {
2325
static final BuiltMap<String, String> typesWithBuilder =
2426
BuiltMap<String, String>({
@@ -29,8 +31,10 @@ abstract class SerializerSourceField
2931
'BuiltSetMultimap': 'SetMultimapBuilder',
3032
});
3133
BuiltValue get settings;
34+
@override
3235
ParsedLibraryResult get parsedLibrary;
3336
FieldElement get element;
37+
@override
3438
FieldElement? get builderElement;
3539

3640
factory SerializerSourceField(
@@ -142,11 +146,12 @@ abstract class SerializerSourceField
142146
}
143147

144148
@memoized
145-
bool get builderFieldUsesNestedBuilder {
146-
var builderFieldElementIsValid =
147-
(builderElement?.getter?.isAbstract == false) &&
148-
!builderElement!.isStatic;
149+
bool get builderFieldElementIsValid =>
150+
(builderElement?.getter?.isAbstract == false) &&
151+
!builderElement!.isStatic;
149152

153+
@memoized
154+
bool get builderFieldUsesNestedBuilder {
150155
// If the builder is present, check it to determine whether a nested
151156
// builder is needed. Otherwise, use the same logic as built_value when
152157
// it decides whether to use a nested builder.
@@ -157,6 +162,19 @@ abstract class SerializerSourceField
157162
DartTypes.needsNestedBuilder(element.getter!.returnType);
158163
}
159164

165+
@memoized
166+
@override
167+
String get fullBuildElementType => super.fullBuildElementType;
168+
169+
@memoized
170+
bool get builderFieldIsNullable {
171+
if (!isNonNullByDefault) return true;
172+
if (!builderFieldElementIsValid) return true;
173+
174+
return fullBuildElementType == 'dynamic' ||
175+
fullBuildElementType.endsWith('?');
176+
}
177+
160178
@memoized
161179
bool get builderFieldAutoCreatesNestedBuilder =>
162180
builtValueField.autoCreateNestedBuilder ??
@@ -192,10 +210,15 @@ abstract class SerializerSourceField
192210
///
193211
/// Generics are cast to the bound of the generic. If there is no bound,
194212
/// no cast is needed, and an empty string is returned.
213+
///
214+
/// Set [forReplace] to loosen generics of cast where possible for calling
215+
/// a `replace` method.
195216
String generateCast(CompilationUnitElement compilationUnit,
196-
BuiltMap<String, String> classGenericBounds) {
217+
BuiltMap<String, String> classGenericBounds,
218+
{bool forReplace = false}) {
197219
var result = _generateCast(
198-
typeInCompilationUnit(compilationUnit), classGenericBounds);
220+
typeInCompilationUnit(compilationUnit), classGenericBounds,
221+
forReplace: forReplace);
199222
return result == 'Object' ? '' : 'as $result';
200223
}
201224

@@ -236,16 +259,19 @@ abstract class SerializerSourceField
236259
}
237260

238261
String _generateCast(String type, BuiltMap<String, String> classGenericBounds,
239-
{bool topLevel = true}) {
262+
{bool topLevel = true, bool forReplace = false}) {
240263
var resultNullabilitySuffix = (!topLevel && type.endsWith('?')) ? '?' : '';
241264
var bareType = _getBareType(type);
242265

243266
// `built_collection` `replace` methods don't care about the full generic
244267
// type, so we can be less precise about the cast. This doesn't add any
245268
// particular value for vanilla `built_value` but it allows plugging in
246269
// serializers that don't get the generic types correct.
270+
//
271+
// Only if `forReplace` was passed.
247272
String generics;
248-
if (topLevel &&
273+
if (forReplace &&
274+
topLevel &&
249275
DartTypes.isBuiltCollectionTypeName(bareType) &&
250276
builderFieldUsesNestedBuilder) {
251277
if (bareType == 'BuiltList' || bareType == 'BuiltSet') {

built_value_generator/lib/src/serializer_source_field.g.dart

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

built_value_generator/lib/src/value_source_field.dart

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import 'package:analyzer/dart/element/type.dart';
1212
import 'package:built_collection/built_collection.dart';
1313
import 'package:built_value/built_value.dart';
1414
import 'package:built_value_generator/src/dart_types.dart';
15+
import 'package:built_value_generator/src/field_mixin.dart';
1516
import 'package:built_value_generator/src/fixes.dart';
1617
import 'package:built_value_generator/src/fields.dart' show collectFields;
1718
import 'package:built_value_generator/src/metadata.dart'
@@ -28,10 +29,13 @@ const _suggestedTypes = <String, String>{
2829
};
2930

3031
abstract class ValueSourceField
32+
with FieldMixin
3133
implements Built<ValueSourceField, ValueSourceFieldBuilder> {
3234
BuiltValue get settings;
35+
@override
3336
ParsedLibraryResult get parsedLibrary;
3437
FieldElement get element;
38+
@override
3539
FieldElement? get builderElement;
3640

3741
factory ValueSourceField(
@@ -134,9 +138,6 @@ abstract class ValueSourceField
134138
annotation.getField('autoCreateNestedBuilder')?.toBoolValue());
135139
}
136140

137-
@memoized
138-
bool get builderFieldExists => builderElement != null;
139-
140141
@memoized
141142
bool get builderFieldIsNormalField =>
142143
builderFieldExists &&
@@ -156,39 +157,12 @@ abstract class ValueSourceField
156157
builderElement!.getter!.isAbstract;
157158

158159
@memoized
159-
String get _fullBuildElementType {
160-
if (!builderFieldExists) return 'dynamic';
161-
162-
// Try to get a resolved type first, it's faster.
163-
var result = DartTypes.tryGetName(builderElement!.getter?.returnType,
164-
withNullabilitySuffix: true);
165-
166-
if (result != null && result != 'dynamic') return result;
167-
// Go via AST to allow use of unresolvable types not yet generated;
168-
// this includes generated Builder types.
169-
result = parsedLibrary
170-
.getElementDeclaration(builderElement!)
171-
?.node
172-
.parent
173-
?.childEntities
174-
.first
175-
.toString();
176-
177-
if (result != null) return result;
178-
179-
result = builderElement!.getter != null
180-
? (parsedLibrary.getElementDeclaration(builderElement!.getter!)?.node
181-
as MethodDeclaration?)
182-
?.returnType
183-
.toString()
184-
: null;
185-
186-
return result ?? 'dynamic';
187-
}
160+
@override
161+
String get fullBuildElementType => super.fullBuildElementType;
188162

189163
@memoized
190164
String get buildElementType {
191-
var result = _fullBuildElementType;
165+
var result = fullBuildElementType;
192166
// If we went via the AST there may be an import prefix, but we don't
193167
// want one here. Strip it off.
194168
if (result.contains('.')) {
@@ -197,7 +171,7 @@ abstract class ValueSourceField
197171
return _removeNullabilitySuffix(result);
198172
}
199173

200-
bool get builderElementTypeIsNullable => _fullBuildElementType.endsWith('?');
174+
bool get builderElementTypeIsNullable => fullBuildElementType.endsWith('?');
201175

202176
bool get builderElementSetterIsNullable {
203177
if (!builderFieldExists) return false;

built_value_generator/lib/src/value_source_field.g.dart

Lines changed: 3 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)