Skip to content

Commit 4419271

Browse files
authored
Merge pull request #63 from prozolic/breakingChange
Add nullable reference type support and improve struct/class formatter handling
2 parents a5aeb87 + f742dac commit 4419271

14 files changed

Lines changed: 204 additions & 31 deletions

src/CsToml.Generator/FormatterTypeMetaData.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,13 @@ public static TomlSerializationKind GetTomlSerializationKind(ITypeSymbol type)
326326
return TomlSerializationKind.TomlSerializedObject;
327327
}
328328

329+
// For a Nullable<T> Collection/Dictionary like ImmutableArray<T>?
330+
if (TryGetNullableParameterType(type, out var nullableType) &&
331+
(ContainsCollectionType(nullableType!) || ContainsDictionary(nullableType!)))
332+
{
333+
type = nullableType!;
334+
}
335+
329336
var genericFormatterType = TryGetGenericFormatterType(type, out var _);
330337
if (genericFormatterType != GenericFormatterType.None)
331338
{

src/CsToml.Generator/Generator.cs

Lines changed: 93 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,22 +79,24 @@ private string Generate(TypeMeta typeMeta, ConstructorMeta constructorMeta)
7979
#pragma warning disable CS8602 // Dereference of a possibly null reference.
8080
#pragma warning disable CS8603 // Possible null reference return.
8181
#pragma warning disable CS8604 // Possible null reference argument for parameter.
82-
#pragma warning disable CS8619 // Possible null reference assignment fix
82+
#pragma warning disable CS8619 // Possible null reference assignment fix.
83+
#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types.
8384
8485
using CsToml;
86+
using CsToml.Error;
8587
using CsToml.Formatter;
8688
using CsToml.Formatter.Resolver;
8789
8890
{{namespaceTag}}
8991
90-
partial {{typeMeta.TypeKeyword}} {{typeMeta.TypeName}} : ITomlSerializedObject<{{typeMeta.TypeName}}>
92+
partial {{typeMeta.TypeKeyword}} {{typeMeta.TypeName}} : ITomlSerializedObject<{{typeMeta.GenericTypeParameterName}}>
9193
{
9294
93-
static {{typeMeta.TypeName}} ITomlSerializedObject<{{typeMeta.TypeName}}>.Deserialize(ref TomlDocumentNode rootNode, CsTomlSerializerOptions options)
95+
static {{typeMeta.GenericTypeParameterName}} ITomlSerializedObject<{{typeMeta.GenericTypeParameterName}}>.Deserialize(ref TomlDocumentNode rootNode, CsTomlSerializerOptions options)
9496
{
9597
{{GenerateDeserializePart(typeMeta, constructorMeta)}} }
9698
97-
static void ITomlSerializedObject<{{typeMeta.TypeName}}>.Serialize<TBufferWriter>(ref Utf8TomlDocumentWriter<TBufferWriter> writer, {{typeMeta.TypeName}} target, CsTomlSerializerOptions options)
99+
static void ITomlSerializedObject<{{typeMeta.GenericTypeParameterName}}>.Serialize<TBufferWriter>(ref Utf8TomlDocumentWriter<TBufferWriter> writer, {{typeMeta.GenericTypeParameterName}} target, CsTomlSerializerOptions options)
98100
{
99101
{{GenerateSerializePart(typeMeta)}} }
100102
@@ -112,6 +114,14 @@ private string GenerateDeserializePart(TypeMeta typeMeta, ConstructorMeta constr
112114
{
113115
var builder = new StringBuilder();
114116

117+
if (typeMeta.IsReferenceType)
118+
{
119+
builder.AppendLine($$"""
120+
if (!(rootNode.HasValue || rootNode.IsTableHeader)) return default;
121+
122+
""");
123+
}
124+
115125
foreach (var member in typeMeta.Members)
116126
{
117127
var propertyName = member.DefinedName;
@@ -178,6 +188,14 @@ private string GenerateSerializePart(TypeMeta typeMeta)
178188
{
179189
var builder = new StringBuilder();
180190

191+
if (typeMeta.IsReferenceType)
192+
{
193+
builder.AppendLine($$"""
194+
if (target == null) ThrowIfNull(nameof(target));
195+
196+
""");
197+
}
198+
181199
var members = typeMeta.Members;
182200
var onlyTomlSerializedObject = members.Length == 1 && members[0].SerializationKind == TomlSerializationKind.TomlSerializedObject;
183201
if (!onlyTomlSerializedObject)
@@ -355,6 +373,17 @@ private string GenerateSerializePart(TypeMeta typeMeta)
355373
{
356374
builder.AppendLine(" writer.EndScope();");
357375
}
376+
377+
if (typeMeta.IsReferenceType)
378+
{
379+
builder.AppendLine($$"""
380+
381+
static void ThrowIfNull(string args)
382+
{
383+
throw new CsTomlException($@"Serialization failed because the argument '{args}' is null.");
384+
}
385+
""");
386+
}
358387
return builder.ToString();
359388
}
360389

@@ -397,19 +426,52 @@ private string GenerateRegisterPart(TypeMeta typeMeta)
397426
""");
398427
break;
399428
case TomlSerializationKind.CollectionOfITomlSerializedObject:
400-
if (FormatterTypeMetaData.TryGetGenericFormatterType(type, out var formatter) != GenericFormatterType.None)
429+
if (type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
401430
{
402-
var collectionNamedType = (INamedTypeSymbol)type;
403-
var typeParameters = string.Join(",", collectionNamedType.TypeArguments.Select(x => x.ToFullFormatString()));
404-
formatter = formatter!.Replace("TYPEPARAMETER", typeParameters);
431+
// Nullable<T> is a special case.
432+
var typeSymbol = namedTypeSymbol.ConstructUnboundGenericType();
433+
if (typeSymbol.ToDisplayString() == "T?")
434+
{
435+
builder.AppendLine($$"""
436+
if (!TomlValueFormatterResolver.IsRegistered<{{fullTypeName}}>())
437+
{
438+
TomlValueFormatterResolver.Register(new NullableFormatter<{{namedTypeSymbol.TypeArguments[0].ToFullFormatString()}}>());
439+
}
440+
""");
441+
break;
442+
}
405443

406-
builder.AppendLine($$"""
444+
if (FormatterTypeMetaData.TryGetGenericFormatterType(typeSymbol.ToFullFormatString(), out var typeFormatter) != GenericFormatterType.None)
445+
{
446+
var typeParameters = string.Join(",", namedTypeSymbol.TypeArguments.Select(x => x.ToFullFormatString()));
447+
typeFormatter = typeFormatter.Replace("TYPEPARAMETER", typeParameters);
448+
449+
builder.AppendLine($$"""
450+
if (!TomlValueFormatterResolver.IsRegistered<{{fullTypeName}}>())
451+
{
452+
TomlValueFormatterResolver.Register(new {{typeFormatter}}());
453+
}
454+
""");
455+
break;
456+
}
457+
}
458+
else
459+
{
460+
if (FormatterTypeMetaData.TryGetGenericFormatterType(type, out var formatter) != GenericFormatterType.None)
461+
{
462+
var collectionNamedType = (INamedTypeSymbol)type;
463+
var typeParameters = string.Join(",", collectionNamedType.TypeArguments.Select(x => x.ToFullFormatString()));
464+
formatter = formatter!.Replace("TYPEPARAMETER", typeParameters);
465+
466+
builder.AppendLine($$"""
407467
if (!TomlValueFormatterResolver.IsRegistered<{{fullTypeName}}>())
408468
{
409469
TomlValueFormatterResolver.Register(new {{formatter}}());
410470
}
411471
""");
472+
}
412473
}
474+
413475
break;
414476
case TomlSerializationKind.Dictionary:
415477
if (FormatterTypeMetaData.TryGetGenericFormatterType(type, out var dictFormatter) != GenericFormatterType.None)
@@ -442,24 +504,24 @@ private string GenerateRegisterPart(TypeMeta typeMeta)
442504
if (FormatterTypeMetaData.ContainsBuiltInFormatterType(type))
443505
break;
444506

445-
if (type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsGenericType)
507+
if (type is INamedTypeSymbol namedTypeSymbol2 && namedTypeSymbol2.IsGenericType)
446508
{
447509
// Nullable<T> is a special case.
448-
var typeSymbol = namedTypeSymbol.ConstructUnboundGenericType();
510+
var typeSymbol = namedTypeSymbol2.ConstructUnboundGenericType();
449511
if (typeSymbol.ToDisplayString() == "T?")
450512
{
451513
builder.AppendLine($$"""
452514
if (!TomlValueFormatterResolver.IsRegistered<{{fullTypeName}}>())
453515
{
454-
TomlValueFormatterResolver.Register(new NullableFormatter<{{namedTypeSymbol.TypeArguments[0].ToFullFormatString()}}>());
516+
TomlValueFormatterResolver.Register(new NullableFormatter<{{namedTypeSymbol2.TypeArguments[0].ToFullFormatString()}}>());
455517
}
456518
""");
457519
break;
458520
}
459521

460522
if (FormatterTypeMetaData.TryGetGenericFormatterType(typeSymbol.ToFullFormatString(), out var typeFormatter) != GenericFormatterType.None)
461523
{
462-
var typeParameters = string.Join(",", namedTypeSymbol.TypeArguments.Select(x => x.ToFullFormatString()));
524+
var typeParameters = string.Join(",", namedTypeSymbol2.TypeArguments.Select(x => x.ToFullFormatString()));
463525
typeFormatter = typeFormatter.Replace("TYPEPARAMETER", typeParameters);
464526

465527
builder.AppendLine($$"""
@@ -475,7 +537,9 @@ private string GenerateRegisterPart(TypeMeta typeMeta)
475537
}
476538
}
477539

478-
var code = $$"""
540+
if (typeMeta.IsReferenceType)
541+
{
542+
var code = $$"""
479543
if (!TomlValueFormatterResolver.IsRegistered<{{typeMeta.TypeName}}>())
480544
{
481545
TomlValueFormatterResolver.Register(new TomlSerializedObjectFormatter<{{typeMeta.TypeName}}>());
@@ -484,7 +548,21 @@ private string GenerateRegisterPart(TypeMeta typeMeta)
484548
// Register Formatter in advance.
485549
{{builder}}
486550
""";
487-
return code;
551+
return code;
552+
}
553+
else
554+
{
555+
var code = $$"""
556+
if (!TomlValueFormatterResolver.IsRegistered<{{typeMeta.TypeName}}>())
557+
{
558+
TomlValueFormatterResolver.Register(new StructTomlSerializedObjectFormatter<{{typeMeta.TypeName}}>());
559+
}
560+
561+
// Register Formatter in advance.
562+
{{builder}}
563+
""";
564+
return code;
565+
}
488566
}
489567
}
490568

src/CsToml.Generator/TypeMeta.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ internal sealed class TypeMeta
1818
public string TypeName { get; }
1919
public string FullTypeName { get; }
2020
public string TypeKeyword { get; }
21+
public string GenericTypeParameterName { get; }
22+
public bool IsReferenceType => !symbol.IsValueType;
2123

2224
public TypeMeta(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
2325
{
@@ -26,8 +28,7 @@ public TypeMeta(INamedTypeSymbol symbol, TypeDeclarationSyntax syntax)
2628

2729
TypeName = symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat);
2830
FullTypeName = symbol.ToFullFormatString();
29-
30-
Console.WriteLine(FullTypeName);
31+
GenericTypeParameterName = symbol.IsValueType ? TypeName : $"{TypeName}?";
3132

3233
if (symbol.IsRecord)
3334
TypeKeyword = symbol.IsValueType ? "record struct" : "record";

src/CsToml/Formatter/Resolver/TomlSerializedObjectFormatterResolver.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,16 @@ public bool IsRegistered<T>()
4444
=> CacheCheck<T>.Registered;
4545

4646
public void Register<T>(TomlSerializedObjectFormatter<T> fomatter)
47-
where T : ITomlSerializedObject<T>
47+
where T : class, ITomlSerializedObject<T?>
48+
{
49+
if (CacheCheck<T>.Registered) return;
50+
51+
CacheCheck<T>.Registered = true;
52+
Cache<T>.Formatter = fomatter!;
53+
}
54+
55+
public void Register<T>(StructTomlSerializedObjectFormatter<T> fomatter)
56+
where T : struct, ITomlSerializedObject<T>
4857
{
4958
if (CacheCheck<T>.Registered) return;
5059

src/CsToml/Formatter/Resolver/TomlValueFormatterResolver.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,13 @@ public static void Register<T>(ITomlValueFormatter<T> formatter)
9595
}
9696

9797
public static void Register<T>(TomlSerializedObjectFormatter<T> formatter)
98-
where T : ITomlSerializedObject<T>
98+
where T : class, ITomlSerializedObject<T?>
99+
{
100+
TomlSerializedObjectFormatterResolver.Instance.Register(formatter);
101+
}
102+
103+
public static void Register<T>(StructTomlSerializedObjectFormatter<T> formatter)
104+
where T : struct, ITomlSerializedObject<T>
99105
{
100106
TomlSerializedObjectFormatterResolver.Instance.Register(formatter);
101107
}

src/CsToml/Formatter/TomlSerializedObjectFormatter.cs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,31 @@
22

33
namespace CsToml.Formatter;
44

5-
public sealed class TomlSerializedObjectFormatter<T> : ITomlValueFormatter<T>
6-
where T : ITomlSerializedObject<T>
5+
public sealed class TomlSerializedObjectFormatter<T> : ITomlValueFormatter<T?>
6+
where T : class, ITomlSerializedObject<T?>
77
{
8-
public T Deserialize(ref TomlDocumentNode rootNode, CsTomlSerializerOptions options)
8+
public T? Deserialize(ref TomlDocumentNode rootNode, CsTomlSerializerOptions options)
99
{
1010
return T.Deserialize(ref rootNode, options);
1111
}
1212

13-
public void Serialize<TBufferWriter>(ref Utf8TomlDocumentWriter<TBufferWriter> writer, T target, CsTomlSerializerOptions options)
13+
public void Serialize<TBufferWriter>(ref Utf8TomlDocumentWriter<TBufferWriter> writer, T? target, CsTomlSerializerOptions options)
1414
where TBufferWriter : IBufferWriter<byte>
1515
{
1616
T.Serialize(ref writer, target, options);
1717
}
18+
}
19+
20+
public sealed class StructTomlSerializedObjectFormatter<T> : ITomlValueFormatter<T>
21+
where T : struct, ITomlSerializedObject<T>
22+
{
23+
public T Deserialize(ref TomlDocumentNode rootNode, CsTomlSerializerOptions options)
24+
{
25+
return T.Deserialize(ref rootNode, options);
26+
}
27+
28+
public void Serialize<TBufferWriter>(ref Utf8TomlDocumentWriter<TBufferWriter> writer, T target, CsTomlSerializerOptions options) where TBufferWriter : IBufferWriter<byte>
29+
{
30+
T.Serialize(ref writer, target, options);
31+
}
1832
}

src/CsToml/TomlDocument.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public partial class TomlDocument : ITomlValueFormatter<TomlDocument>
1414
private readonly TomlTable table;
1515

1616
public TomlDocumentNode RootNode
17-
=> new(table.RootNode);
17+
=> new(table.RootNode, true);
1818

1919
public long LineNumber { get; internal set; }
2020

src/CsToml/TomlDocumentNode.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,25 @@ public struct TomlDocumentNode
2525
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
2626
private readonly TomlValue value;
2727
private readonly TomlTableNode node;
28+
private readonly bool isRoot;
2829

2930
internal readonly int NodeCount => node?.NodeCount ?? 0;
3031

3132
internal readonly TomlValue Value => value;
3233

3334
internal readonly TomlTableNode Node => node;
3435

35-
public readonly bool HasValue => Value.HasValue || NodeCount > 0;
36+
public readonly bool HasValue => Value.HasValue || NodeCount > 0 || isRoot;
3637

3738
[EditorBrowsable(EditorBrowsableState.Never)]
3839
public readonly bool HasNodeOnly => !Value.HasValue && NodeCount > 0;
3940

4041
[EditorBrowsable(EditorBrowsableState.Never)]
4142
public readonly bool HasValueOnly => Value.HasValue && NodeCount == 0;
4243

44+
[EditorBrowsable(EditorBrowsableState.Never)]
45+
public readonly bool IsTableHeader => node?.IsTableHeader ?? false;
46+
4347
public readonly TomlValueType ValueType => Value.Type;
4448

4549
public TomlDocumentNode this[ReadOnlySpan<char> key]
@@ -110,10 +114,11 @@ public TomlDocumentNode this[int index]
110114
}
111115
}
112116

113-
internal TomlDocumentNode(TomlTableNode node)
117+
internal TomlDocumentNode(TomlTableNode node, bool isRoot = false)
114118
{
115119
this.node = node;
116120
this.value = node.Value!;
121+
this.isRoot = isRoot;
117122
}
118123

119124
public TomlDocumentNode(TomlValue value)

tests/CsToml.Generator.Tests/CsToml.Generator.Tests.csproj

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
<ItemGroup>
1616
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
1717
<PackageReference Include="Shouldly" Version="4.2.1" />
18-
<PackageReference Include="System.Net.Http" Version="4.3.4" />
19-
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
2018
<PackageReference Include="Utf8StringInterpolation" Version="1.3.2" />
2119
<PackageReference Include="xunit.v3" Version="1.1.0" />
2220
</ItemGroup>

0 commit comments

Comments
 (0)