Skip to content

Commit def0ca1

Browse files
authored
Support C# 'required' keyword in source generation and reflection (#139)
The source generator now detects the C# 'required' modifier on properties and fields (via IPropertySymbol.IsRequired / IFieldSymbol.IsRequired) in addition to [YamlRequired] and [JsonRequired] attributes. Source generator changes: - MemberModel gains IsRequiredKeyword and NeedsObjectInitializer properties - CreateMemberModel() detects C# required modifier via Roslyn symbol APIs - Types with required-keyword members are routed through the constructor/ object-initializer code path (avoiding bare 'new T()' which causes CS9035) - Required-keyword members use 'default!' as fallback in the object initializer (dead code since required-member validation throws first) - __defaults construction includes required members in its initializer when needed for pure init-only member defaults Reflection changes: - YamlObjectConverter.IsRequired() now checks for RequiredMemberAttribute (emitted by the C# compiler for required members) on net8.0+ Tests: 35 new tests covering required set/init properties, mixed required modes, records, value types, inheritance, naming policies, and custom options in both reflection and source-generated modes.
1 parent add4de0 commit def0ca1

3 files changed

Lines changed: 508 additions & 46 deletions

File tree

src/SharpYaml.SourceGenerator/YamlSerializerContextGenerator.cs

Lines changed: 87 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,7 @@ typeSymbol is INamedTypeSymbol lifecycleType &&
14671467

14681468
if (selectedConstructor is not null &&
14691469
(selectedConstructor.Parameters.Length != 0 ||
1470-
members.Any(static member => member.IsInitOnly) ||
1470+
members.Any(static member => member.NeedsObjectInitializer) ||
14711471
extensionData is { IsInitOnly: true }))
14721472
{
14731473
EmitReadObjectCoreWithConstructor(builder, index, ctorType, typeName, selectedConstructor, members, extensionData, indexByType, emitLifecycleCallbacks, propertyNamingPolicy);
@@ -1849,9 +1849,12 @@ private static void EmitReadObjectCoreWithConstructor(
18491849
var bufferedMembers = members
18501850
.Where(m => IsWritableMember(m.Symbol) && !ctorBoundMembers.Contains(m.Symbol))
18511851
.ToArray();
1852-
var initOnlyMembers = bufferedMembers.Where(static m => m.IsInitOnly).ToArray();
1853-
var postCreateBufferedMembers = bufferedMembers.Where(static m => !m.IsInitOnly).ToArray();
1854-
var useObjectInitializer = initOnlyMembers.Length != 0 || extensionData is { IsInitOnly: true };
1852+
var initializerMembers = bufferedMembers.Where(static m => m.NeedsObjectInitializer).ToArray();
1853+
var postCreateBufferedMembers = bufferedMembers.Where(static m => !m.NeedsObjectInitializer).ToArray();
1854+
var useObjectInitializer = initializerMembers.Length != 0 || extensionData is { IsInitOnly: true };
1855+
var pureInitOnlyMembers = initializerMembers.Where(static m => m.IsInitOnly && !m.IsRequiredKeyword).ToArray();
1856+
var requiredKeywordInitMembers = initializerMembers.Where(static m => m.IsRequiredKeyword).ToArray();
1857+
var needsDefaults = pureInitOnlyMembers.Length != 0 || extensionData is { IsInitOnly: true };
18551858
var initOnlyExtensionDataValueVarName = extensionData is { IsInitOnly: true } ? $"__extensionData{index}" : null;
18561859

18571860
var bufferedMemberValueVarNames = new Dictionary<ISymbol, string>(bufferedMembers.Length, SymbolEqualityComparer.Default);
@@ -2044,7 +2047,8 @@ private static void EmitReadObjectCoreWithConstructor(
20442047
member.AttributeConverterTypeName,
20452048
member.ObjectCreationHandling,
20462049
member.IsRequired,
2047-
member.IsInitOnly);
2050+
member.IsInitOnly,
2051+
member.IsRequiredKeyword);
20482052

20492053
builder.Append(" if (!matched && global::System.String.Equals(mergeKey, ").Append(member.SerializedNameExpressionForRead)
20502054
.Append(", options.PropertyNameCaseInsensitive ? global::System.StringComparison.OrdinalIgnoreCase : global::System.StringComparison.Ordinal))");
@@ -2181,7 +2185,8 @@ private static void EmitReadObjectCoreWithConstructor(
21812185
member.AttributeConverterTypeName,
21822186
member.ObjectCreationHandling,
21832187
member.IsRequired,
2184-
member.IsInitOnly);
2188+
member.IsInitOnly,
2189+
member.IsRequiredKeyword);
21852190

21862191
builder.Append(" if (!matched && global::System.String.Equals(key, ").Append(member.SerializedNameExpressionForRead)
21872192
.Append(", options.PropertyNameCaseInsensitive ? global::System.StringComparison.OrdinalIgnoreCase : global::System.StringComparison.Ordinal))");
@@ -2267,55 +2272,37 @@ private static void EmitReadObjectCoreWithConstructor(
22672272
builder.AppendLine();
22682273
if (useObjectInitializer)
22692274
{
2270-
builder.Append(" var __defaults").Append(index).Append(" = default(").Append(typeName);
2271-
if (typeSymbol.IsReferenceType)
2272-
{
2273-
builder.Append('?');
2274-
}
2275-
2276-
builder.AppendLine(");");
2277-
if (extensionData is { IsInitOnly: true })
2275+
if (needsDefaults)
22782276
{
2279-
builder.Append(" __defaults").Append(index).Append(" = new ").Append(typeName).Append('(');
2280-
for (var i = 0; i < parameters.Length; i++)
2277+
builder.Append(" var __defaults").Append(index).Append(" = default(").Append(typeName);
2278+
if (typeSymbol.IsReferenceType)
22812279
{
2282-
if (i != 0)
2283-
{
2284-
builder.Append(", ");
2285-
}
2286-
2287-
builder.Append(parameterValueVarNames[i]);
2280+
builder.Append('?');
22882281
}
22892282

22902283
builder.AppendLine(");");
22912284
}
2292-
else
2285+
2286+
if (extensionData is { IsInitOnly: true })
2287+
{
2288+
EmitDefaultsConstruction(builder, index, typeName, parameters, parameterValueVarNames, requiredKeywordInitMembers, " ");
2289+
}
2290+
else if (pureInitOnlyMembers.Length != 0)
22932291
{
22942292
builder.Append(" if (");
2295-
for (var i = 0; i < initOnlyMembers.Length; i++)
2293+
for (var i = 0; i < pureInitOnlyMembers.Length; i++)
22962294
{
22972295
if (i != 0)
22982296
{
22992297
builder.Append(" || ");
23002298
}
23012299

2302-
builder.Append('!').Append(bufferedMemberSeenVarNames[initOnlyMembers[i].Symbol]);
2300+
builder.Append('!').Append(bufferedMemberSeenVarNames[pureInitOnlyMembers[i].Symbol]);
23032301
}
23042302

23052303
builder.AppendLine(")");
23062304
builder.AppendLine(" {");
2307-
builder.Append(" __defaults").Append(index).Append(" = new ").Append(typeName).Append('(');
2308-
for (var i = 0; i < parameters.Length; i++)
2309-
{
2310-
if (i != 0)
2311-
{
2312-
builder.Append(", ");
2313-
}
2314-
2315-
builder.Append(parameterValueVarNames[i]);
2316-
}
2317-
2318-
builder.AppendLine(");");
2305+
EmitDefaultsConstruction(builder, index, typeName, parameters, parameterValueVarNames, requiredKeywordInitMembers, " ");
23192306
builder.AppendLine(" }");
23202307
}
23212308

@@ -2404,19 +2391,31 @@ private static void EmitReadObjectCoreWithConstructor(
24042391
{
24052392
builder.AppendLine(")");
24062393
builder.AppendLine(" {");
2407-
for (var i = 0; i < initOnlyMembers.Length; i++)
2394+
for (var i = 0; i < initializerMembers.Length; i++)
24082395
{
2409-
var member = initOnlyMembers[i];
2396+
var member = initializerMembers[i];
24102397
var seenVar = bufferedMemberSeenVarNames[member.Symbol];
24112398
var valueVar = bufferedMemberValueVarNames[member.Symbol];
24122399

2413-
builder.Append(" ").Append(member.Symbol.Name).Append(" = ").Append(seenVar).Append(" ? ").Append(valueVar).Append(" : __defaults").Append(index);
2414-
if (typeSymbol.IsReferenceType)
2400+
builder.Append(" ").Append(member.Symbol.Name).Append(" = ").Append(seenVar).Append(" ? ").Append(valueVar);
2401+
if (member.IsRequiredKeyword)
24152402
{
2416-
builder.Append('!');
2403+
// Required-keyword members: if not seen, the required-member validation
2404+
// will throw before this value is observed. Use default! as a safe placeholder.
2405+
builder.Append(" : default!");
24172406
}
2407+
else
2408+
{
2409+
builder.Append(" : __defaults").Append(index);
2410+
if (typeSymbol.IsReferenceType)
2411+
{
2412+
builder.Append('!');
2413+
}
24182414

2419-
builder.Append('.').Append(member.Symbol.Name).AppendLine(",");
2415+
builder.Append('.').Append(member.Symbol.Name);
2416+
}
2417+
2418+
builder.AppendLine(",");
24202419
}
24212420

24222421
if (extensionData is { IsInitOnly: true } initOnlyExtensionData)
@@ -2581,6 +2580,43 @@ private static void EmitReadObjectCoreWithConstructor(
25812580
builder.AppendLine(" return instance;");
25822581
}
25832582

2583+
private static void EmitDefaultsConstruction(
2584+
StringBuilder builder,
2585+
int index,
2586+
string typeName,
2587+
ImmutableArray<IParameterSymbol> parameters,
2588+
string[] parameterValueVarNames,
2589+
MemberModel[] requiredKeywordInitMembers,
2590+
string indent)
2591+
{
2592+
builder.Append(indent).Append("__defaults").Append(index).Append(" = new ").Append(typeName).Append('(');
2593+
for (var i = 0; i < parameters.Length; i++)
2594+
{
2595+
if (i != 0)
2596+
{
2597+
builder.Append(", ");
2598+
}
2599+
2600+
builder.Append(parameterValueVarNames[i]);
2601+
}
2602+
2603+
if (requiredKeywordInitMembers.Length != 0)
2604+
{
2605+
builder.AppendLine(")");
2606+
builder.Append(indent).AppendLine("{");
2607+
for (var i = 0; i < requiredKeywordInitMembers.Length; i++)
2608+
{
2609+
builder.Append(indent).Append(" ").Append(requiredKeywordInitMembers[i].Symbol.Name).AppendLine(" = default!,");
2610+
}
2611+
2612+
builder.Append(indent).AppendLine("};");
2613+
}
2614+
else
2615+
{
2616+
builder.AppendLine(");");
2617+
}
2618+
}
2619+
25842620
private static void EmitReadValue(
25852621
StringBuilder builder,
25862622
int index,
@@ -5662,7 +5698,8 @@ public MemberModel(
56625698
string? attributeConverterTypeName,
56635699
string? objectCreationHandling,
56645700
bool isRequired,
5665-
bool isInitOnly)
5701+
bool isInitOnly,
5702+
bool isRequiredKeyword)
56665703
{
56675704
Symbol = symbol;
56685705
Type = type;
@@ -5675,6 +5712,7 @@ public MemberModel(
56755712
ObjectCreationHandling = objectCreationHandling;
56765713
IsRequired = isRequired;
56775714
IsInitOnly = isInitOnly;
5715+
IsRequiredKeyword = isRequiredKeyword;
56785716
}
56795717

56805718
public ISymbol Symbol { get; }
@@ -5688,6 +5726,8 @@ public MemberModel(
56885726
public string? ObjectCreationHandling { get; }
56895727
public bool IsRequired { get; }
56905728
public bool IsInitOnly { get; }
5729+
public bool IsRequiredKeyword { get; }
5730+
public bool NeedsObjectInitializer => IsInitOnly || IsRequiredKeyword;
56915731
}
56925732

56935733
private enum ExtensionDataKind
@@ -5739,9 +5779,10 @@ private static MemberModel CreateMemberModel(ISymbol member, JsonNamingPolicy? p
57395779
var ignoreConditionExpression = GetIgnoreConditionExpression(member);
57405780
var converterTypeName = GetYamlConverterAttributeTypeName(member);
57415781
var objectCreationHandling = GetObjectCreationHandling(member);
5742-
var isRequired = HasAttribute(member, "SharpYaml.Serialization.YamlRequiredAttribute") || HasAttribute(member, "System.Text.Json.Serialization.JsonRequiredAttribute");
5782+
var isRequiredKeyword = member is IPropertySymbol { IsRequired: true } || member is IFieldSymbol { IsRequired: true };
5783+
var isRequired = isRequiredKeyword || HasAttribute(member, "SharpYaml.Serialization.YamlRequiredAttribute") || HasAttribute(member, "System.Text.Json.Serialization.JsonRequiredAttribute");
57435784
var isInitOnly = member is IPropertySymbol property && IsInitOnlyProperty(property);
5744-
return new MemberModel(member, type, nameForRead, nameForWrite, accessExpression, assign, ignoreConditionExpression, converterTypeName, objectCreationHandling, isRequired, isInitOnly);
5785+
return new MemberModel(member, type, nameForRead, nameForWrite, accessExpression, assign, ignoreConditionExpression, converterTypeName, objectCreationHandling, isRequired, isInitOnly, isRequiredKeyword);
57455786
}
57465787

57475788
private static (string ForRead, string ForWrite) GetSerializedMemberNameExpressions(ISymbol member, JsonNamingPolicy? propertyNamingPolicy)

0 commit comments

Comments
 (0)