Files
cloakhq--cloakbrowser/dotnet/src/CloakBrowser.Generators/InterfaceDelegationGenerator.cs
T
wehub-resource-sync 0af812490d
CI / python (push) Failing after 0s
CI / javascript (push) Failing after 1s
CI / dotnet (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:12:36 +08:00

425 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace CloakBrowser.Generators;
/// <summary>
/// Roslyn source generator that auto-implements the boilerplate "delegate everything
/// to the wrapped object" members for a decorator class.
///
/// Usage: annotate a <c>partial class</c> with
/// <c>[GenerateInterfaceDelegation(typeof(IPage))]</c> and provide a field/property
/// named <c>_inner</c> (or the name passed to the attribute) of the interface type.
/// The generator emits delegating implementations for every interface member the
/// class does NOT already declare itself. Members you declare manually (the
/// "intercepted" / humanized ones) are left untouched.
///
/// This lets us hand-write only the handful of methods we want to humanize while the
/// remaining ~hundreds of interface members are forwarded verbatim with full static
/// typing, correct async signatures, optional parameters, and no reflection.
/// </summary>
[Generator(Microsoft.CodeAnalysis.LanguageNames.CSharp)]
public sealed class InterfaceDelegationGenerator : IIncrementalGenerator
{
private const string AttributeNamespace = "CloakBrowser.Wrappers";
private const string AttributeName = "GenerateInterfaceDelegationAttribute";
private const string AttributeFullName = AttributeNamespace + "." + AttributeName;
private const string AttributeSource = @"// <auto-generated/>
#nullable enable
namespace CloakBrowser.Wrappers
{
/// <summary>
/// Marks a partial decorator class for automatic generation of delegating members
/// for the given interface. The class must expose a backing member (default name
/// <c>_inner</c>) of the interface type that calls are forwarded to.
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
internal sealed class GenerateInterfaceDelegationAttribute : System.Attribute
{
public GenerateInterfaceDelegationAttribute(System.Type interfaceType, string innerMemberName = ""_inner"")
{
InterfaceType = interfaceType;
InnerMemberName = innerMemberName;
}
public System.Type InterfaceType { get; }
public string InnerMemberName { get; }
}
}
";
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Emit the marker attribute into the consuming compilation.
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("GenerateInterfaceDelegationAttribute.g.cs",
SourceText.From(AttributeSource, Encoding.UTF8)));
var candidates = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) =>
node is ClassDeclarationSyntax c &&
c.AttributeLists.Count > 0 &&
c.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)),
transform: static (ctx, _) => (INamedTypeSymbol?)ctx.SemanticModel.GetDeclaredSymbol(ctx.Node))
.Where(static s => s is not null)
.Select(static (s, _) => s!);
var compilationAndClasses = context.CompilationProvider.Combine(candidates.Collect());
context.RegisterSourceOutput(compilationAndClasses, static (spc, source) =>
Execute(spc, source.Left, source.Right));
}
private static void Execute(SourceProductionContext spc, Compilation compilation,
System.Collections.Immutable.ImmutableArray<INamedTypeSymbol> classes)
{
var attrSymbol = compilation.GetTypeByMetadataName(AttributeFullName);
if (attrSymbol is null)
return;
var seen = new HashSet<string>(StringComparer.Ordinal);
foreach (var cls in classes.Distinct(SymbolEqualityComparer.Default).Cast<INamedTypeSymbol>())
{
var attrs = cls.GetAttributes()
.Where(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrSymbol))
.ToList();
if (attrs.Count == 0)
continue;
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
sb.AppendLine("#pragma warning disable CS0108 // member hides inherited member");
sb.AppendLine("#pragma warning disable CS0612 // member is obsolete (delegation is intentional)");
sb.AppendLine("#pragma warning disable CS0618 // member is obsolete (delegation is intentional)");
sb.AppendLine();
string? ns = cls.ContainingNamespace.IsGlobalNamespace
? null
: cls.ContainingNamespace.ToDisplayString();
if (ns is not null)
{
sb.Append("namespace ").Append(ns).AppendLine();
sb.AppendLine("{");
}
sb.Append(" partial class ").Append(cls.Name).AppendLine();
sb.AppendLine(" {");
// Collect members the class already declares (by signature) so we don't
// re-emit (and conflict with) hand-written intercepted methods.
var declared = BuildDeclaredSignatureSet(cls);
foreach (var attr in attrs)
{
if (attr.ConstructorArguments.Length < 1)
continue;
if (attr.ConstructorArguments[0].Value is not INamedTypeSymbol iface)
continue;
string innerName = attr.ConstructorArguments.Length >= 2 &&
attr.ConstructorArguments[1].Value is string s
? s
: "_inner";
EmitForInterface(sb, iface, innerName, declared);
}
sb.AppendLine(" }");
if (ns is not null)
sb.AppendLine("}");
string hint = (ns is null ? "" : ns + ".") + cls.Name + ".Delegation.g.cs";
// De-dup hint names across partials.
if (!seen.Add(hint))
hint = hint + "." + Guid.NewGuid().ToString("N").Substring(0, 6);
spc.AddSource(hint, SourceText.From(sb.ToString(), Encoding.UTF8));
}
}
private static HashSet<string> BuildDeclaredSignatureSet(INamedTypeSymbol cls)
{
var set = new HashSet<string>(StringComparer.Ordinal);
foreach (var m in cls.GetMembers())
{
switch (m)
{
case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
set.Add(MethodSignature(method));
break;
case IPropertySymbol prop when prop.IsIndexer:
set.Add(IndexerSignature(prop));
break;
case IPropertySymbol prop:
set.Add("prop:" + prop.Name);
break;
case IEventSymbol evt:
set.Add("event:" + evt.Name);
break;
}
}
return set;
}
private static void EmitForInterface(StringBuilder sb, INamedTypeSymbol iface,
string innerName, HashSet<string> declared)
{
// Walk the interface and every interface it inherits.
var allIfaces = new List<INamedTypeSymbol> { iface };
allIfaces.AddRange(iface.AllInterfaces);
var emitted = new HashSet<string>(StringComparer.Ordinal);
foreach (var i in allIfaces)
{
foreach (var member in i.GetMembers())
{
switch (member)
{
case IMethodSymbol method when method.MethodKind == MethodKind.Ordinary:
{
string sig = MethodSignature(method);
if (declared.Contains(sig) || !emitted.Add(sig))
continue;
EmitMethod(sb, method, innerName);
break;
}
case IPropertySymbol prop when prop.IsIndexer:
{
string sig = IndexerSignature(prop);
if (declared.Contains(sig) || !emitted.Add(sig))
continue;
EmitIndexer(sb, prop, innerName);
break;
}
case IPropertySymbol prop:
{
string key = "prop:" + prop.Name;
if (declared.Contains(key) || !emitted.Add(key))
continue;
EmitProperty(sb, prop, innerName);
break;
}
case IEventSymbol evt:
{
string key = "event:" + evt.Name;
if (declared.Contains(key) || !emitted.Add(key))
continue;
EmitEvent(sb, evt, innerName);
break;
}
}
}
}
}
// -----------------------------------------------------------------------
// Emitters
// -----------------------------------------------------------------------
// Marker on every generated (delegating) member, so consumers/tests can tell a
// generator-delegated member apart from a hand-written (intercepted) one.
private const string GenAttr =
" [global::System.CodeDom.Compiler.GeneratedCode(\"CloakBrowser.Generators\", \"1.0\")]";
private static void EmitMethod(StringBuilder sb, IMethodSymbol method, string innerName)
{
string ret = method.ReturnType.ToDisplayString(FullyQualified);
string retKeyword = method.ReturnsVoid ? "void" : ret;
var pars = new List<string>();
var args = new List<string>();
foreach (var p in method.Parameters)
BuildParam(p, pars, args);
string typeParams = method.IsGenericMethod
? "<" + string.Join(", ", method.TypeParameters.Select(t => t.Name)) + ">"
: "";
string constraints = BuildConstraints(method);
sb.AppendLine(GenAttr);
sb.Append(" public ")
.Append(retKeyword).Append(' ')
.Append(method.Name).Append(typeParams).Append('(')
.Append(string.Join(", ", pars)).Append(')')
.Append(constraints)
.Append(" => ")
.Append(innerName).Append('.').Append(method.Name).Append(typeParams)
.Append('(').Append(string.Join(", ", args)).Append(");")
.AppendLine();
}
private static void EmitProperty(StringBuilder sb, IPropertySymbol prop, string innerName)
{
string type = prop.Type.ToDisplayString(FullyQualified);
sb.AppendLine(GenAttr);
sb.Append(" public ").Append(type).Append(' ').Append(prop.Name).Append(" { ");
if (prop.GetMethod is not null)
sb.Append("get => ").Append(innerName).Append('.').Append(prop.Name).Append("; ");
if (prop.SetMethod is not null)
sb.Append("set => ").Append(innerName).Append('.').Append(prop.Name).Append(" = value; ");
sb.AppendLine("}");
}
private static void EmitIndexer(StringBuilder sb, IPropertySymbol prop, string innerName)
{
string type = prop.Type.ToDisplayString(FullyQualified);
var pars = new List<string>();
var args = new List<string>();
foreach (var p in prop.Parameters)
BuildParam(p, pars, args);
sb.AppendLine(GenAttr);
sb.Append(" public ").Append(type).Append(" this[")
.Append(string.Join(", ", pars)).Append("] { ");
if (prop.GetMethod is not null)
sb.Append("get => ").Append(innerName).Append('[').Append(string.Join(", ", args)).Append("]; ");
if (prop.SetMethod is not null)
sb.Append("set => ").Append(innerName).Append('[').Append(string.Join(", ", args)).Append("] = value; ");
sb.AppendLine("}");
}
private static void EmitEvent(StringBuilder sb, IEventSymbol evt, string innerName)
{
string type = evt.Type.ToDisplayString(FullyQualified);
sb.AppendLine(GenAttr);
sb.Append(" public event ").Append(type).Append(' ').Append(evt.Name).AppendLine();
sb.AppendLine(" {");
sb.Append(" add => ").Append(innerName).Append('.').Append(evt.Name).AppendLine(" += value;");
sb.Append(" remove => ").Append(innerName).Append('.').Append(evt.Name).AppendLine(" -= value;");
sb.AppendLine(" }");
}
private static void BuildParam(IParameterSymbol p, List<string> pars, List<string> args)
{
string refKind = p.RefKind switch
{
RefKind.Ref => "ref ",
RefKind.Out => "out ",
RefKind.In => "in ",
_ => "",
};
string ptype = p.Type.ToDisplayString(FullyQualified);
// Special types (object, string, ...) sometimes drop their nullable annotation
// in the display string; re-attach it so `object? arg = null` overrides match
// and we don't trip CS8625.
if (p.NullableAnnotation == NullableAnnotation.Annotated &&
!p.Type.IsValueType && !ptype.EndsWith("?"))
{
ptype += "?";
}
string pname = SafeName(p.Name);
string paramsKw = p.IsParams ? "params " : "";
string decl = paramsKw + refKind + ptype + " " + pname;
if (p.HasExplicitDefaultValue)
decl += " = " + FormatDefault(p);
pars.Add(decl);
args.Add(refKind + pname);
}
private static string FormatDefault(IParameterSymbol p)
{
var v = p.ExplicitDefaultValue;
if (v is null)
{
// For a null default, value types use the literal `default`; reference
// types use `default!` so we never trip CS8625 regardless of whether the
// interface was compiled in a nullable-aware context.
return p.Type.IsValueType ? "default" : "default!";
}
return v switch
{
bool b => b ? "true" : "false",
string s => "\"" + s.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"",
char c => "'" + c + "'",
float f => f.ToString(System.Globalization.CultureInfo.InvariantCulture) + "f",
double d => d.ToString(System.Globalization.CultureInfo.InvariantCulture),
_ when p.Type.TypeKind == TypeKind.Enum =>
"(" + p.Type.ToDisplayString(FullyQualified) + ")" + v,
_ => System.Convert.ToString(v, System.Globalization.CultureInfo.InvariantCulture) ?? "default",
};
}
private static string BuildConstraints(IMethodSymbol method)
{
if (!method.IsGenericMethod)
return "";
var sb = new StringBuilder();
foreach (var tp in method.TypeParameters)
{
var parts = new List<string>();
if (tp.HasReferenceTypeConstraint) parts.Add("class");
if (tp.HasValueTypeConstraint) parts.Add("struct");
if (tp.HasNotNullConstraint) parts.Add("notnull");
if (tp.HasUnmanagedTypeConstraint) parts.Add("unmanaged");
foreach (var ct in tp.ConstraintTypes)
parts.Add(ct.ToDisplayString(FullyQualified));
if (tp.HasConstructorConstraint) parts.Add("new()");
if (parts.Count > 0)
sb.Append(" where ").Append(tp.Name).Append(" : ").Append(string.Join(", ", parts));
}
return sb.ToString();
}
// -----------------------------------------------------------------------
// Signature helpers (must match how we detect already-declared members)
// -----------------------------------------------------------------------
private static string MethodSignature(IMethodSymbol m)
{
var sb = new StringBuilder();
sb.Append(m.Name);
sb.Append('`').Append(m.TypeParameters.Length);
sb.Append('(');
sb.Append(string.Join(",", m.Parameters.Select(p =>
(p.RefKind == RefKind.None ? "" : p.RefKind.ToString().ToLowerInvariant() + " ")
+ p.Type.ToDisplayString(SignatureFormat))));
sb.Append(')');
return sb.ToString();
}
private static string IndexerSignature(IPropertySymbol p)
{
return "this[" + string.Join(",", p.Parameters.Select(x =>
x.Type.ToDisplayString(SignatureFormat))) + "]";
}
private static string SafeName(string name)
{
return SyntaxFactsKeywords.Contains(name) ? "@" + name : name;
}
private static readonly HashSet<string> SyntaxFactsKeywords = new(StringComparer.Ordinal)
{
"abstract","as","base","bool","break","byte","case","catch","char","checked","class","const",
"continue","decimal","default","delegate","do","double","else","enum","event","explicit","extern",
"false","finally","fixed","float","for","foreach","goto","if","implicit","in","int","interface",
"internal","is","lock","long","namespace","new","null","object","operator","out","override","params",
"private","protected","public","readonly","ref","return","sbyte","sealed","short","sizeof","stackalloc",
"static","string","struct","switch","this","throw","true","try","typeof","uint","ulong","unchecked",
"unsafe","ushort","using","virtual","void","volatile","while",
};
private static readonly SymbolDisplayFormat FullyQualified =
SymbolDisplayFormat.FullyQualifiedFormat
.WithMiscellaneousOptions(
SymbolDisplayMiscellaneousOptions.UseSpecialTypes |
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier |
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers);
// For signature comparison we want stable type names without nullable annotations,
// so the manual override matches the interface member regardless of NRT modifiers.
private static readonly SymbolDisplayFormat SignatureFormat =
SymbolDisplayFormat.FullyQualifiedFormat
.WithMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.UseSpecialTypes);
}