Skip to content

Commit 0454c83

Browse files
committed
Fix overridden member metadata in source generation
Honor inherited serialization metadata on overridden members so source-generated contexts match reflection for property names, ignore settings, ordering, and related member attributes. Add regression tests for overridden properties in both reflection and source-generated paths. Follow-up to #120
1 parent 35338fe commit 0454c83

3 files changed

Lines changed: 143 additions & 60 deletions

File tree

src/Tomlyn.SourceGeneration/TomlSerializerContextGenerator.cs

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4309,18 +4309,46 @@ private static bool TryGetDictionaryValueType(ITypeSymbol type, out ITypeSymbol
43094309
}
43104310

43114311
private static bool HasAttribute(ISymbol symbol, string attributeMetadataName)
4312+
=> TryGetAttribute(symbol, attributeMetadataName, out _);
4313+
4314+
private static bool TryGetAttribute(ISymbol symbol, string attributeMetadataName, out AttributeData attribute)
43124315
{
4313-
foreach (var attr in symbol.GetAttributes())
4316+
foreach (var current in EnumerateSelfAndBaseSymbols(symbol))
43144317
{
4315-
if (attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::" + attributeMetadataName)
4318+
foreach (var attr in current.GetAttributes())
43164319
{
4317-
return true;
4320+
if (attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) == "global::" + attributeMetadataName)
4321+
{
4322+
attribute = attr;
4323+
return true;
4324+
}
43184325
}
43194326
}
43204327

4328+
attribute = null!;
43214329
return false;
43224330
}
43234331

4332+
private static IEnumerable<ISymbol> EnumerateSelfAndBaseSymbols(ISymbol symbol)
4333+
{
4334+
for (var current = symbol; current is not null; current = GetBaseSymbol(current))
4335+
{
4336+
yield return current;
4337+
}
4338+
}
4339+
4340+
private static ISymbol? GetBaseSymbol(ISymbol symbol)
4341+
{
4342+
return symbol switch
4343+
{
4344+
IPropertySymbol property => property.OverriddenProperty,
4345+
IMethodSymbol method => method.OverriddenMethod,
4346+
IEventSymbol @event => @event.OverriddenEvent,
4347+
INamedTypeSymbol named => named.BaseType,
4348+
_ => null,
4349+
};
4350+
}
4351+
43244352
private static bool ImplementsInterface(ITypeSymbol type, string interfaceMetadataName)
43254353
{
43264354
if (type is not INamedTypeSymbol named)
@@ -4341,22 +4369,18 @@ private static bool ImplementsInterface(ITypeSymbol type, string interfaceMetada
43414369

43424370
private static string GetSerializedName(ISymbol member, string memberName, string? namingPolicyExpression)
43434371
{
4344-
foreach (var attr in member.GetAttributes())
4372+
if (TryGetAttribute(member, "Tomlyn.Serialization.TomlPropertyNameAttribute", out var tomlAttr) &&
4373+
tomlAttr.ConstructorArguments.Length == 1 &&
4374+
tomlAttr.ConstructorArguments[0].Value is string tomlName)
43454375
{
4346-
var attrName = attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4347-
if (attrName == "global::Tomlyn.Serialization.TomlPropertyNameAttribute" &&
4348-
attr.ConstructorArguments.Length == 1 &&
4349-
attr.ConstructorArguments[0].Value is string tomlName)
4350-
{
4351-
return tomlName;
4352-
}
4376+
return tomlName;
4377+
}
43534378

4354-
if (attrName == "global::System.Text.Json.Serialization.JsonPropertyNameAttribute" &&
4355-
attr.ConstructorArguments.Length == 1 &&
4356-
attr.ConstructorArguments[0].Value is string jsonName)
4357-
{
4358-
return jsonName;
4359-
}
4379+
if (TryGetAttribute(member, "System.Text.Json.Serialization.JsonPropertyNameAttribute", out var jsonAttr) &&
4380+
jsonAttr.ConstructorArguments.Length == 1 &&
4381+
jsonAttr.ConstructorArguments[0].Value is string jsonName)
4382+
{
4383+
return jsonName;
43604384
}
43614385

43624386
if (namingPolicyExpression is not null && TryConvertKnownName(memberName, namingPolicyExpression, out var converted))
@@ -4369,13 +4393,8 @@ private static string GetSerializedName(ISymbol member, string memberName, strin
43694393

43704394
private static ObjectCreationHandlingKind GetObjectCreationHandling(ISymbol symbol)
43714395
{
4372-
foreach (var attr in symbol.GetAttributes())
4396+
if (TryGetAttribute(symbol, JsonObjectCreationHandlingAttributeMetadataName, out var attr))
43734397
{
4374-
if (attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::" + JsonObjectCreationHandlingAttributeMetadataName)
4375-
{
4376-
continue;
4377-
}
4378-
43794398
if (attr.ConstructorArguments.Length == 1 && attr.ConstructorArguments[0].Value is int constructorValue)
43804399
{
43814400
return constructorValue switch
@@ -4405,27 +4424,21 @@ private static ObjectCreationHandlingKind GetObjectCreationHandling(ISymbol symb
44054424

44064425
private static int GetOrder(ISymbol member)
44074426
{
4408-
int? tomlOrder = null;
4409-
int? jsonOrder = null;
4427+
if (TryGetAttribute(member, "Tomlyn.Serialization.TomlPropertyOrderAttribute", out var tomlAttr) &&
4428+
tomlAttr.ConstructorArguments.Length == 1 &&
4429+
tomlAttr.ConstructorArguments[0].Value is int tomlOrder)
4430+
{
4431+
return tomlOrder;
4432+
}
44104433

4411-
foreach (var attr in member.GetAttributes())
4434+
if (TryGetAttribute(member, "System.Text.Json.Serialization.JsonPropertyOrderAttribute", out var jsonAttr) &&
4435+
jsonAttr.ConstructorArguments.Length == 1 &&
4436+
jsonAttr.ConstructorArguments[0].Value is int jsonOrder)
44124437
{
4413-
var attrName = attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4414-
if (attrName == "global::Tomlyn.Serialization.TomlPropertyOrderAttribute" &&
4415-
attr.ConstructorArguments.Length == 1 &&
4416-
attr.ConstructorArguments[0].Value is int toml)
4417-
{
4418-
tomlOrder = toml;
4419-
}
4420-
else if (attrName == "global::System.Text.Json.Serialization.JsonPropertyOrderAttribute" &&
4421-
attr.ConstructorArguments.Length == 1 &&
4422-
attr.ConstructorArguments[0].Value is int json)
4423-
{
4424-
jsonOrder = json;
4425-
}
4438+
return jsonOrder;
44264439
}
44274440

4428-
return tomlOrder ?? jsonOrder ?? 0;
4441+
return 0;
44294442
}
44304443

44314444
private static bool TryConvertKnownName(string name, string namingPolicyExpression, out string converted)
@@ -4452,36 +4465,21 @@ private static bool TryConvertKnownName(string name, string namingPolicyExpressi
44524465

44534466
private static IgnoreBehavior GetIgnoreBehavior(ISymbol symbol)
44544467
{
4455-
TomlIgnoreAttributeModel? toml = null;
4456-
JsonIgnoreAttributeModel? json = null;
4457-
4458-
foreach (var attr in symbol.GetAttributes())
4459-
{
4460-
var attrName = attr.AttributeClass?.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
4461-
if (attrName == "global::Tomlyn.Serialization.TomlIgnoreAttribute")
4462-
{
4463-
toml = TomlIgnoreAttributeModel.From(attr);
4464-
}
4465-
else if (attrName == "global::System.Text.Json.Serialization.JsonIgnoreAttribute")
4466-
{
4467-
json = JsonIgnoreAttributeModel.From(attr);
4468-
}
4469-
}
4470-
4471-
if (toml is not null)
4468+
if (TryGetAttribute(symbol, "Tomlyn.Serialization.TomlIgnoreAttribute", out var tomlAttr))
44724469
{
4470+
var toml = TomlIgnoreAttributeModel.From(tomlAttr);
44734471
// Interpret [TomlIgnore] with default Condition as "ignore always".
4474-
return toml.Value.Condition switch
4472+
return toml.Condition switch
44754473
{
44764474
1 => new IgnoreBehavior(ignoreAlways: false, writeIgnore: WriteIgnoreKind.WhenWritingNull),
44774475
2 => new IgnoreBehavior(ignoreAlways: false, writeIgnore: WriteIgnoreKind.WhenWritingDefault),
44784476
_ => new IgnoreBehavior(ignoreAlways: true, writeIgnore: WriteIgnoreKind.None),
44794477
};
44804478
}
44814479

4482-
if (json is not null)
4480+
if (TryGetAttribute(symbol, "System.Text.Json.Serialization.JsonIgnoreAttribute", out var jsonAttr))
44834481
{
4484-
var condition = json.Value.Condition ?? JsonIgnoreCondition.Always;
4482+
var condition = JsonIgnoreAttributeModel.From(jsonAttr).Condition ?? JsonIgnoreCondition.Always;
44854483
return condition switch
44864484
{
44874485
JsonIgnoreCondition.Never => new IgnoreBehavior(ignoreAlways: false, writeIgnore: WriteIgnoreKind.None),

src/Tomlyn.Tests/NewApiReflectionPocoTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,20 @@ private sealed class JsonNamedDerivedOptions : JsonNamedBaseOptions
7575
public string? Derived { get; init; }
7676
}
7777

78+
private abstract class JsonNamedOverriddenBaseOptions
79+
{
80+
[JsonPropertyName("baseValue")]
81+
public virtual string? Base { get; init; }
82+
}
83+
84+
private sealed class JsonNamedOverriddenDerivedOptions : JsonNamedOverriddenBaseOptions
85+
{
86+
[JsonPropertyName("derivedValue")]
87+
public string? Derived { get; init; }
88+
89+
public override string? Base { get; init; }
90+
}
91+
7892
private sealed class PrivateSetterModel
7993
{
8094
[JsonInclude]
@@ -216,6 +230,25 @@ public void SerializeDeserialize_InheritedProperties_RespectJsonPropertyName()
216230
Assert.That(roundtrip.Derived, Is.EqualTo("leaf"));
217231
}
218232

233+
[Test]
234+
public void SerializeDeserialize_OverriddenProperties_RespectInheritedJsonPropertyName()
235+
{
236+
var original = new JsonNamedOverriddenDerivedOptions
237+
{
238+
Base = "shared",
239+
Derived = "leaf",
240+
};
241+
242+
var toml = TomlSerializer.Serialize(original);
243+
var roundtrip = TomlSerializer.Deserialize<JsonNamedOverriddenDerivedOptions>(toml);
244+
245+
Assert.That(toml, Does.Contain("baseValue = \"shared\""));
246+
Assert.That(toml, Does.Contain("derivedValue = \"leaf\""));
247+
Assert.That(roundtrip, Is.Not.Null);
248+
Assert.That(roundtrip!.Base, Is.EqualTo("shared"));
249+
Assert.That(roundtrip.Derived, Is.EqualTo("leaf"));
250+
}
251+
219252
[Test]
220253
public void Deserialize_PrivateSetter_WithJsonInclude_Works()
221254
{

src/Tomlyn.Tests/NewApiSourceGenerationTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ public sealed class GeneratedDerivedOptions : GeneratedBaseOptions
3636
public string? Derived { get; init; }
3737
}
3838

39+
public abstract class GeneratedOverriddenBaseOptions
40+
{
41+
[JsonPropertyName("baseValue")]
42+
public virtual required string? Base { get; init; }
43+
}
44+
45+
public sealed class GeneratedOverriddenDerivedOptions : GeneratedOverriddenBaseOptions
46+
{
47+
[JsonPropertyName("derivedValue")]
48+
public string? Derived { get; init; }
49+
50+
public override required string? Base { get; init; }
51+
}
52+
3953
public sealed class GeneratedOptionsPerson
4054
{
4155
public string Name { get; set; } = "";
@@ -264,6 +278,12 @@ internal partial class TestTomlSerializerContextInheritedMembers : TomlSerialize
264278
{
265279
}
266280

281+
[TomlSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
282+
[TomlSerializable(typeof(GeneratedOverriddenDerivedOptions))]
283+
internal partial class TestTomlSerializerContextOverriddenMembers : TomlSerializerContext
284+
{
285+
}
286+
267287
[TomlSourceGenerationOptions(
268288
WriteIndented = false,
269289
IndentSize = 4,
@@ -813,6 +833,38 @@ public void GeneratedContext_CanSerializeAndDeserializeInheritedProperties()
813833
Assert.That(roundtrip.Derived, Is.EqualTo("leaf"));
814834
}
815835

836+
[Test]
837+
public void GeneratedContext_CanDeserializeOverriddenProperties_WithInheritedJsonPropertyName()
838+
{
839+
var context = TestTomlSerializerContextOverriddenMembers.Default;
840+
var toml = """
841+
baseValue = "shared"
842+
derivedValue = "leaf"
843+
""";
844+
845+
var roundtrip = TomlSerializer.Deserialize<GeneratedOverriddenDerivedOptions>(toml, context);
846+
847+
Assert.That(roundtrip, Is.Not.Null);
848+
Assert.That(roundtrip!.Base, Is.EqualTo("shared"));
849+
Assert.That(roundtrip.Derived, Is.EqualTo("leaf"));
850+
}
851+
852+
[Test]
853+
public void GeneratedContext_CanSerializeOverriddenProperties_WithInheritedJsonPropertyName()
854+
{
855+
var context = TestTomlSerializerContextOverriddenMembers.Default;
856+
var original = new GeneratedOverriddenDerivedOptions
857+
{
858+
Base = "shared",
859+
Derived = "leaf",
860+
};
861+
862+
var toml = TomlSerializer.Serialize(original, context);
863+
864+
Assert.That(toml, Does.Contain("baseValue = \"shared\""));
865+
Assert.That(toml, Does.Contain("derivedValue = \"leaf\""));
866+
}
867+
816868
[Test]
817869
public void GeneratedContext_AppliesTomlSourceGenerationOptions()
818870
{

0 commit comments

Comments
 (0)