Generate StatementInformation classes with code generator

This commit is contained in:
Stone_Red
2026-06-11 14:47:59 +02:00
parent 5ad0053ef7
commit 1760b85245
8 changed files with 229 additions and 235 deletions
@@ -27,8 +27,14 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
CollectMethods(compilation.Assembly.GlobalNamespace, statementMethods, staticStatementMethods);
string source = GenerateRegistrySource(statementMethods, staticStatementMethods);
context.AddSource("GeneratedStatementRegistry.g.cs", source);
INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName);
INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName);
string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol);
context.AddSource("GeneratedStatementInformation.g.cs", infoSource);
string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods);
context.AddSource("GeneratedStatementRegistry.g.cs", registrySource);
}
private static void CollectMethods(
@@ -77,6 +83,187 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
}
}
// -------------------------------------------------------------------------
// Information class generation
// -------------------------------------------------------------------------
private static string GenerateInformationClassesSource(
INamedTypeSymbol? statementAttrSymbol,
INamedTypeSymbol? staticStatementAttrSymbol)
{
StringBuilder sb = new StringBuilder();
_ = sb.AppendLine("// <auto-generated />");
_ = sb.AppendLine("#nullable enable");
_ = sb.AppendLine();
_ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;");
_ = sb.AppendLine();
if (statementAttrSymbol is not null)
{
EmitInformationClass(sb, statementAttrSymbol, "StatementInformation");
}
if (staticStatementAttrSymbol is not null)
{
EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation");
}
return sb.ToString();
}
private static void EmitInformationClass(
StringBuilder sb,
INamedTypeSymbol attributeSymbol,
string className)
{
List<IMethodSymbol> ctors = attributeSymbol.Constructors
.Where(c => !c.IsImplicitlyDeclared)
.OrderByDescending(c => c.Parameters.Length)
.ToList();
// All parameters across all ctors that are covered by a ctor (get-only properties).
HashSet<string> ctorParamNames = new HashSet<string>(
ctors.SelectMany(c => c.Parameters).Select(p => p.Name),
StringComparer.OrdinalIgnoreCase);
// Settable properties NOT covered by any ctor parameter.
List<IPropertySymbol> settableProps = attributeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsStatic
&& p.DeclaredAccessibility == Accessibility.Public
&& p.SetMethod is not null
&& !ctorParamNames.Contains(p.Name))
.ToList();
// All properties ever assigned by any ctor, deduplicated by name.
// We need a get-only property for each of them.
List<(string PropName, ITypeSymbol PropType)> ctorProps = ctors
.SelectMany(c => c.Parameters)
.Select(p => (
PropName: ResolvePropertyName(attributeSymbol, p),
PropType: p.Type))
.GroupBy(x => x.PropName, StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
// ---- class declaration ----
_ = sb.AppendLine($"public sealed class {className}");
_ = sb.AppendLine("{");
// ---- one constructor per attribute constructor ----
foreach (IMethodSymbol ctor in ctors)
{
if (ctor.Parameters.Length == 0)
{
_ = sb.AppendLine($" public {className}() {{ }}");
_ = sb.AppendLine();
continue;
}
string ctorParams = string.Join(", ",
ctor.Parameters.Select(p => $"{GlobalType(p.Type)} {p.Name}"));
_ = sb.AppendLine($" public {className}({ctorParams})");
_ = sb.AppendLine(" {");
foreach (IParameterSymbol p in ctor.Parameters)
{
string propName = ResolvePropertyName(attributeSymbol, p);
_ = sb.AppendLine($" {propName} = {p.Name};");
}
_ = sb.AppendLine(" }");
_ = sb.AppendLine();
}
// ---- get-only properties sourced from ctor parameters ----
foreach ((string propName, ITypeSymbol propType) in ctorProps)
{
_ = sb.AppendLine($" public {GlobalType(propType)} {propName} {{ get; }}");
}
// ---- settable properties (named-argument style) ----
foreach (IPropertySymbol prop in settableProps)
{
string defaultClause = GetDefaultClause(prop);
_ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}");
}
_ = sb.AppendLine("}");
_ = sb.AppendLine();
}
// Finds the attribute property that corresponds to a constructor parameter.
private static string ResolvePropertyName(INamedTypeSymbol attributeSymbol, IParameterSymbol parameter)
{
IPropertySymbol? match = attributeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.FirstOrDefault(p => string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase));
return match?.Name ?? ToPascalCase(parameter.Name);
}
private static string GlobalType(ITypeSymbol type)
{
// Primitive aliases don't have a global:: form — emit the C# keyword instead.
string keyword = type.SpecialType switch
{
SpecialType.System_String => "string",
SpecialType.System_Boolean => "bool",
SpecialType.System_Byte => "byte",
SpecialType.System_SByte => "sbyte",
SpecialType.System_Int16 => "short",
SpecialType.System_UInt16 => "ushort",
SpecialType.System_Int32 => "int",
SpecialType.System_UInt32 => "uint",
SpecialType.System_Int64 => "long",
SpecialType.System_UInt64 => "ulong",
SpecialType.System_Single => "float",
SpecialType.System_Double => "double",
SpecialType.System_Decimal => "decimal",
SpecialType.System_Char => "char",
SpecialType.System_Object => "object",
_ => ""
};
if (keyword != "")
{
// Preserve nullability annotation (e.g. string?)
return type.NullableAnnotation == NullableAnnotation.Annotated
? keyword + "?"
: keyword;
}
return $"global::{type.ToDisplayString()}";
}
private static string ToPascalCase(string name)
{
return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1);
}
private static string GetDefaultClause(IPropertySymbol prop)
{
// Only emit a default for a handful of well-known "safe" defaults so the
// generated code compiles even when the caller omits the named argument.
return prop.Type.IsReferenceType || prop.Type.NullableAnnotation == NullableAnnotation.Annotated
? " = null!;"
: prop.Type.SpecialType switch
{
SpecialType.System_Boolean => " = false;",
SpecialType.System_Int32 => " = 0;",
SpecialType.System_String => " = \"\";",
_ => ""
};
}
// -------------------------------------------------------------------------
// Registry source generation (unchanged from original)
// -------------------------------------------------------------------------
private static string GenerateRegistrySource(
List<MethodRegistration> statementMethods,
List<MethodRegistration> staticStatementMethods)
@@ -94,8 +281,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
_ = sb.AppendLine("{");
_ = sb.AppendLine(" internal static void Register(");
_ = sb.AppendLine(" RuntimeInformation runtimeInfo,");
_ = sb.AppendLine(" out Dictionary<global::YesNt.Interpreter.Attributes.StatementAttributeContainer, Action<string>> statements,");
_ = sb.AppendLine(" out List<KeyValuePair<global::YesNt.Interpreter.Attributes.StaticStatementAttributeContainer, Action>> staticStatements)");
_ = sb.AppendLine(" out Dictionary<global::YesNt.Interpreter.Runtime.StatementInformation, Action<string>> statements,");
_ = sb.AppendLine(" out List<KeyValuePair<global::YesNt.Interpreter.Runtime.StaticStatementInformation, Action>> staticStatements)");
_ = sb.AppendLine(" {");
List<INamedTypeSymbol> allTypes = statementMethods
@@ -116,25 +303,25 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
_ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;");
}
_ = sb.AppendLine(" var statementEntries = new List<KeyValuePair<global::YesNt.Interpreter.Attributes.StatementAttributeContainer, Action<string>>>();");
_ = sb.AppendLine(" var statementEntries = new List<KeyValuePair<global::YesNt.Interpreter.Runtime.StatementInformation, Action<string>>>();");
foreach (MethodRegistration method in statementMethods
.OrderBy(x => x.ContainingType.ToDisplayString())
.ThenBy(x => x.Method.Name))
{
string instanceName = instanceNames[method.ContainingType];
string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StatementAttributeContainer", method.Attribute);
string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StatementInformation", method.Attribute);
_ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));");
}
_ = sb.AppendLine(" var staticEntries = new List<KeyValuePair<global::YesNt.Interpreter.Attributes.StaticStatementAttributeContainer, Action>>();");
_ = sb.AppendLine(" var staticEntries = new List<KeyValuePair<global::YesNt.Interpreter.Runtime.StaticStatementInformation, Action>>();");
foreach (MethodRegistration method in staticStatementMethods
.OrderBy(x => x.ContainingType.ToDisplayString())
.ThenBy(x => x.Method.Name))
{
string instanceName = instanceNames[method.ContainingType];
string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StaticStatementAttributeContainer", method.Attribute);
string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StaticStatementInformation", method.Attribute);
_ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));");
}
@@ -143,7 +330,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
_ = sb.AppendLine(" int cmp = a.Key.Priority.CompareTo(b.Key.Priority);");
_ = sb.AppendLine(" return cmp != 0 ? cmp : b.Key.Name.Length.CompareTo(a.Key.Name.Length);");
_ = sb.AppendLine(" });");
_ = sb.AppendLine(" statements = new Dictionary<global::YesNt.Interpreter.Attributes.StatementAttributeContainer, Action<string>>();");
_ = sb.AppendLine(" statements = new Dictionary<global::YesNt.Interpreter.Runtime.StatementInformation, Action<string>>();");
_ = sb.AppendLine(" foreach (var entry in statementEntries)");
_ = sb.AppendLine(" statements.Add(entry.Key, entry.Value);");
_ = sb.AppendLine();
@@ -236,4 +423,4 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator
public AttributeData Attribute { get; } = attribute;
}
}
}