Files
YesNt-Interpreter/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs
T

574 lines
23 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace YesNt.Interpreter.Generator;
[Generator]
public sealed class StatementRegistryGenerator : IIncrementalGenerator
{
private const string StatementAttributeName = "YesNt.Interpreter.Attributes.StatementAttribute";
private const string StaticStatementAttributeName = "YesNt.Interpreter.Attributes.StaticStatementAttribute";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterSourceOutput(
context.CompilationProvider,
Execute);
}
private static void Execute(SourceProductionContext context, Compilation compilation)
{
List<MethodRegistration> statementMethods = [];
List<MethodRegistration> staticStatementMethods = [];
CollectMethods(compilation.Assembly.GlobalNamespace, statementMethods, staticStatementMethods);
INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName);
INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName);
string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol, compilation);
context.AddSource("GeneratedStatementInformation.g.cs", infoSource);
string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods);
context.AddSource("GeneratedStatementRegistry.g.cs", registrySource);
}
private static void CollectMethods(
INamespaceSymbol namespaceSymbol,
List<MethodRegistration> statementMethods,
List<MethodRegistration> staticStatementMethods)
{
foreach (INamespaceSymbol childNamespace in namespaceSymbol.GetNamespaceMembers())
{
CollectMethods(childNamespace, statementMethods, staticStatementMethods);
}
foreach (INamedTypeSymbol type in namespaceSymbol.GetTypeMembers())
{
CollectMethods(type, statementMethods, staticStatementMethods);
}
}
private static void CollectMethods(
INamedTypeSymbol typeSymbol,
List<MethodRegistration> statementMethods,
List<MethodRegistration> staticStatementMethods)
{
foreach (ISymbol member in typeSymbol.GetMembers())
{
if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary)
{
foreach (AttributeData attribute in method.GetAttributes())
{
string? attributeName = attribute.AttributeClass?.ToDisplayString();
if (attributeName == StatementAttributeName)
{
statementMethods.Add(new MethodRegistration(typeSymbol, method, attribute));
}
else if (attributeName == StaticStatementAttributeName)
{
staticStatementMethods.Add(new MethodRegistration(typeSymbol, method, attribute));
}
}
}
}
foreach (INamedTypeSymbol nestedType in typeSymbol.GetTypeMembers())
{
CollectMethods(nestedType, statementMethods, staticStatementMethods);
}
}
// -------------------------------------------------------------------------
// Information class generation
// -------------------------------------------------------------------------
private static string GenerateInformationClassesSource(
INamedTypeSymbol? statementAttrSymbol,
INamedTypeSymbol? staticStatementAttrSymbol,
Compilation compilation)
{
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", compilation);
}
if (staticStatementAttrSymbol is not null)
{
EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation", compilation);
}
return sb.ToString();
}
private static void EmitInformationClass(
StringBuilder sb,
INamedTypeSymbol attributeSymbol,
string className,
Compilation compilation)
{
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
&& !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.
var ctorProps = ctors
.SelectMany(c => c.Parameters)
.Select(p => {
string propName = ResolvePropertyName(attributeSymbol, p);
IPropertySymbol? propSymbol = attributeSymbol
.GetMembers()
.OfType<IPropertySymbol>()
.FirstOrDefault(sym => string.Equals(sym.Name, propName, StringComparison.OrdinalIgnoreCase));
return (PropName: propName, PropType: p.Type, PropSymbol: propSymbol);
})
.GroupBy(x => x.PropName, StringComparer.OrdinalIgnoreCase)
.Select(g => g.First())
.ToList();
// ---- class declaration ----
_ = sb.Append(GetFormattedComment(attributeSymbol, attributeSymbol, className, $"Information about the {className} class.", ""));
_ = sb.AppendLine($"public sealed class {className}");
_ = sb.AppendLine("{");
// ---- one constructor per attribute constructor ----
foreach (IMethodSymbol ctor in ctors)
{
if (ctor.Parameters.Length == 0)
{
_ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the <see cref=\"{className}\"/> class.", " "));
_ = sb.AppendLine($" public {className}() {{ }}");
_ = sb.AppendLine();
continue;
}
string ctorParams = string.Join(", ",
ctor.Parameters.Select(p => $"{GlobalType(p.Type)} {p.Name}"));
_ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the <see cref=\"{className}\"/> class.", " "));
_ = 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 (var ctorProp in ctorProps)
{
if (ctorProp.PropSymbol is not null)
{
_ = sb.Append(GetFormattedComment(ctorProp.PropSymbol, attributeSymbol, className, $"Gets the {ctorProp.PropName} property.", " "));
}
else
{
_ = sb.AppendLine(" /// <summary>");
_ = sb.AppendLine($" /// Gets the {ctorProp.PropName} property.");
_ = sb.AppendLine(" /// </summary>");
}
_ = sb.AppendLine($" public {GlobalType(ctorProp.PropType)} {ctorProp.PropName} {{ get; }}");
}
// ---- settable properties (named-argument style) ----
foreach (IPropertySymbol prop in settableProps)
{
string defaultClause = GetDefaultClause(prop, compilation);
_ = sb.Append(GetFormattedComment(prop, attributeSymbol, className, $"Gets or sets the {prop.Name} property.", " "));
_ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}");
}
_ = sb.AppendLine("}");
_ = sb.AppendLine();
}
private static string GetFormattedComment(
ISymbol symbol,
INamedTypeSymbol attributeSymbol,
string className,
string fallbackSummary,
string indent = " ")
{
string? xml = symbol.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xml))
{
xml = xml!.Replace(attributeSymbol.Name, className);
}
string formatted = FormatXmlComment(xml, indent);
if (!string.IsNullOrWhiteSpace(formatted))
{
return formatted;
}
// Fallback
var sb = new StringBuilder();
_ = sb.AppendLine($"{indent}/// <summary>");
_ = sb.AppendLine($"{indent}/// {fallbackSummary}");
_ = sb.AppendLine($"{indent}/// </summary>");
return sb.ToString();
}
private static string FormatXmlComment(string? xml, string indent)
{
if (string.IsNullOrWhiteSpace(xml))
{
return string.Empty;
}
try
{
var element = System.Xml.Linq.XElement.Parse(xml!);
var sb = new StringBuilder();
foreach (var child in element.Elements())
{
string nodeXml = child.ToString();
string[] lines = nodeXml.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None);
foreach (string line in lines)
{
string trimmedLine = line.Trim();
if (trimmedLine.Length > 0)
{
_ = sb.AppendLine($"{indent}/// {trimmedLine}");
}
}
}
return sb.ToString();
}
catch
{
return string.Empty;
}
}
// 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,
Compilation compilation)
{
foreach (SyntaxReference syntaxRef in prop.DeclaringSyntaxReferences)
{
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!;"
: 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)
{
StringBuilder sb = new StringBuilder();
_ = sb.AppendLine("// <auto-generated />");
_ = sb.AppendLine("#nullable enable");
_ = sb.AppendLine("using System;");
_ = sb.AppendLine("using System.Collections.Generic;");
_ = sb.AppendLine();
_ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;");
_ = sb.AppendLine();
_ = sb.AppendLine("internal static class GeneratedStatementRegistry");
_ = sb.AppendLine("{");
_ = sb.AppendLine(" internal static void Register(");
_ = sb.AppendLine(" RuntimeInformation runtimeInfo,");
_ = 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
.Concat(staticStatementMethods)
.Select(x => x.ContainingType)
.GroupBy(x => x, SymbolEqualityComparer.Default)
.Select(g => g.First())
.OrderBy(x => x.ToDisplayString())
.ToList();
Dictionary<INamedTypeSymbol, string> instanceNames = new Dictionary<INamedTypeSymbol, string>(SymbolEqualityComparer.Default);
int index = 0;
foreach (INamedTypeSymbol type in allTypes)
{
string instanceName = $"instance{index++}";
instanceNames[type] = instanceName;
_ = sb.AppendLine($" var {instanceName} = new global::{type.ToDisplayString()}();");
_ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;");
}
_ = 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.Runtime.StatementInformation", method.Attribute);
_ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));");
}
_ = 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.Runtime.StaticStatementInformation", method.Attribute);
_ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));");
}
_ = sb.AppendLine(" statementEntries.Sort((a, b) =>");
_ = sb.AppendLine(" {");
_ = 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.Runtime.StatementInformation, Action<string>>();");
_ = sb.AppendLine(" foreach (var entry in statementEntries)");
_ = sb.AppendLine(" statements.Add(entry.Key, entry.Value);");
_ = sb.AppendLine();
_ = sb.AppendLine(" staticEntries.Sort((a, b) => a.Key.Priority.CompareTo(b.Key.Priority));");
_ = sb.AppendLine(" staticStatements = staticEntries;");
_ = sb.AppendLine(" }");
_ = sb.AppendLine("}");
return sb.ToString();
}
private static string BuildAttributeCreation(string attributeTypeName, AttributeData attribute)
{
string ctorArgs = string.Join(", ",
attribute.ConstructorArguments.Select(ToLiteral));
string creation = $"new {attributeTypeName}({ctorArgs})";
if (attribute.NamedArguments.Length == 0)
{
return creation;
}
string namedArgs = string.Join(", ",
attribute.NamedArguments.Select(arg => $"{arg.Key} = {ToLiteral(arg.Value)}"));
return $"{creation} {{ {namedArgs} }}";
}
private static string ToLiteral(TypedConstant constant)
{
if (constant.IsNull)
{
return "null!";
}
if (constant.Type is null)
{
return "null!";
}
if (constant.Kind == TypedConstantKind.Enum)
{
string enumType = $"global::{constant.Type.ToDisplayString()}";
object value = constant.Value!;
return $"({enumType}){Convert.ToInt64(value)}";
}
return constant.Type.SpecialType switch
{
SpecialType.System_String => "\"" + EscapeString((string)constant.Value!) + "\"",
SpecialType.System_Char => "'" + EscapeChar((char)constant.Value!) + "'",
SpecialType.System_Boolean => (bool)constant.Value! ? "true" : "false",
SpecialType.System_Int32 => ((int)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture),
SpecialType.System_Int64 => ((long)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L",
SpecialType.System_Single => ((float)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f",
SpecialType.System_Double => ((double)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture),
_ => constant.Value!.ToString() ?? "null!"
};
}
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)
{
return value
.Replace("\\", "\\\\")
.Replace("\"", "\\\"")
.Replace("\r", "\\r")
.Replace("\n", "\\n")
.Replace("\t", "\\t");
}
private static string EscapeChar(char value)
{
return value switch
{
'\\' => "\\\\",
'\'' => "\\'",
'\r' => "\\r",
'\n' => "\\n",
'\t' => "\\t",
_ => value.ToString()
};
}
private sealed class MethodRegistration(INamedTypeSymbol containingType, IMethodSymbol method, AttributeData attribute)
{
public INamedTypeSymbol ContainingType { get; } = containingType;
public IMethodSymbol Method { get; } = method;
public AttributeData Attribute { get; } = attribute;
}
}