Add property initializer default value support in generated classes

This commit is contained in:
Stone_Red
2026-06-11 15:24:04 +02:00
parent 73cb4ca669
commit 13d85dbd05
@@ -1,4 +1,5 @@
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -30,7 +31,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName); INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName);
INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName); INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName);
string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol); string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol, compilation);
context.AddSource("GeneratedStatementInformation.g.cs", infoSource); context.AddSource("GeneratedStatementInformation.g.cs", infoSource);
string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods); string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods);
@@ -89,7 +90,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
private static string GenerateInformationClassesSource( private static string GenerateInformationClassesSource(
INamedTypeSymbol? statementAttrSymbol, INamedTypeSymbol? statementAttrSymbol,
INamedTypeSymbol? staticStatementAttrSymbol) INamedTypeSymbol? staticStatementAttrSymbol,
Compilation compilation)
{ {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
@@ -101,12 +103,12 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
if (statementAttrSymbol is not null) if (statementAttrSymbol is not null)
{ {
EmitInformationClass(sb, statementAttrSymbol, "StatementInformation"); EmitInformationClass(sb, statementAttrSymbol, "StatementInformation", compilation);
} }
if (staticStatementAttrSymbol is not null) if (staticStatementAttrSymbol is not null)
{ {
EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation"); EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation", compilation);
} }
return sb.ToString(); return sb.ToString();
@@ -115,7 +117,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
private static void EmitInformationClass( private static void EmitInformationClass(
StringBuilder sb, StringBuilder sb,
INamedTypeSymbol attributeSymbol, INamedTypeSymbol attributeSymbol,
string className) string className,
Compilation compilation)
{ {
List<IMethodSymbol> ctors = attributeSymbol.Constructors List<IMethodSymbol> ctors = attributeSymbol.Constructors
.Where(c => !c.IsImplicitlyDeclared) .Where(c => !c.IsImplicitlyDeclared)
@@ -186,7 +189,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
// ---- settable properties (named-argument style) ---- // ---- settable properties (named-argument style) ----
foreach (IPropertySymbol prop in settableProps) foreach (IPropertySymbol prop in settableProps)
{ {
string defaultClause = GetDefaultClause(prop); string defaultClause = GetDefaultClause(prop, compilation);
_ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}"); _ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}");
} }
@@ -244,11 +247,47 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1); return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1);
} }
private static string GetDefaultClause(IPropertySymbol prop) private static string GetDefaultClause(
IPropertySymbol prop,
Compilation compilation)
{ {
// Only emit a default for a handful of well-known "safe" defaults so the foreach (SyntaxReference syntaxRef in prop.DeclaringSyntaxReferences)
// generated code compiles even when the caller omits the named argument. {
return prop.Type.IsReferenceType || prop.Type.NullableAnnotation == NullableAnnotation.Annotated if (syntaxRef.GetSyntax() is not Microsoft.CodeAnalysis.CSharp.Syntax.PropertyDeclarationSyntax syntax)
{
continue;
}
if (syntax.Initializer?.Value is null)
{
continue;
}
SemanticModel model = compilation.GetSemanticModel(syntax.SyntaxTree);
ExpressionSyntax expr = syntax.Initializer.Value;
// 1. Try constant evaluation first (SAFE)
Optional<object?> constant = model.GetConstantValue(expr);
if (constant.HasValue)
{
return $" = {ToLiteral(constant.Value!, prop.Type)};";
}
// 2. Try symbol resolution (enum fields etc.)
ISymbol? symbolInfo = model.GetSymbolInfo(expr).Symbol;
if (symbolInfo is IFieldSymbol field)
{
string typeName = GlobalType(field.ContainingType);
return $" = {typeName}.{field.Name};";
}
// 3. fallback: raw expression (last resort)
return $" = {expr};";
}
// fallback defaults
return prop.Type.IsReferenceType || prop.NullableAnnotation == NullableAnnotation.Annotated
? " = null!;" ? " = null!;"
: prop.Type.SpecialType switch : prop.Type.SpecialType switch
{ {
@@ -391,6 +430,38 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
}; };
} }
private static string ToLiteral(object value, ITypeSymbol type)
{
if (value is null)
{
return "null!";
}
if (type.TypeKind == TypeKind.Enum)
{
string enumType = GlobalType(type);
// value is already boxed enum OR underlying integral type
long underlying = Convert.ToInt64(value);
// fallback: cast
return $"({enumType}){underlying}";
}
return type.SpecialType switch
{
SpecialType.System_String => $"\"{EscapeString((string)value)}\"",
SpecialType.System_Char => $"'{EscapeChar((char)value)}'",
SpecialType.System_Boolean => (bool)value ? "true" : "false",
SpecialType.System_Int32 => ((int)value).ToString(System.Globalization.CultureInfo.InvariantCulture),
SpecialType.System_Int64 => ((long)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L",
SpecialType.System_Single => ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f",
SpecialType.System_Double => ((double)value).ToString(System.Globalization.CultureInfo.InvariantCulture),
SpecialType.System_Decimal => ((decimal)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "m",
_ => value.ToString() ?? "null!"
};
}
private static string EscapeString(string value) private static string EscapeString(string value)
{ {
return value return value