From 34a1788dfbec5460ee928a0816351581d6ececd7 Mon Sep 17 00:00:00 2001 From: Beakona Date: Fri, 1 Dec 2023 01:40:10 +0100 Subject: [PATCH] GenerateAutoAs preview --- AutoInterfaceSample/Program.cs | 53 ++- .../GenerateAutoAsAttribute.cs | 12 + .../AutoInterfaceSourceGenerator.cs | 1 - .../BeaKona.AutoInterfaceGenerator.csproj | 2 +- .../GenerateAutoAsSourceGenerator.cs | 323 ++++++++++++++++++ .../INamespaceSymbolExtensions.cs | 16 + 6 files changed, 401 insertions(+), 6 deletions(-) create mode 100644 BeaKona.AutoInterfaceAttributes/GenerateAutoAsAttribute.cs create mode 100644 BeaKona.AutoInterfaceGenerator/GenerateAutoAsSourceGenerator.cs create mode 100644 BeaKona.AutoInterfaceGenerator/INamespaceSymbolExtensions.cs diff --git a/AutoInterfaceSample/Program.cs b/AutoInterfaceSample/Program.cs index b672239..1735b63 100644 --- a/AutoInterfaceSample/Program.cs +++ b/AutoInterfaceSample/Program.cs @@ -1,17 +1,23 @@ -using System; +using BeaKona; +using System; +using System.Collections; +using System.Collections.Generic; -namespace AutoInterfaceSample +namespace AutoInterfaceSample.Test { public class Program { public static void Main() { - //System.Diagnostics.Debug.WriteLine(BeaKona.Output.Debug_Person.Info); + //System.Diagnostics.Debug.WriteLine(BeaKona.Output.Debug_TestClass_1.Info); //IArbitrary p = new Person(); //int f; //int g = 1; //p.Method(1, out f, ref g, "t", 1, 2, 3); + TestClass t = new TestClass(); + var x = t.AsMy1(); + IPrintableComplex p = new Person2(); p.Print(); p.PrintComplex(); @@ -24,13 +30,52 @@ interface IPrintableComplex void PrintComplex(); } + public interface IMy1Base + { + } + + public interface IMy1 : IMy1Base + { + } + + internal interface IMy2 + { + } + + internal interface IMy2 + { + } + + internal interface IMy3 + { + } + + internal interface @internal + { + } + + public abstract class TestClassBase : IMy3 + { + } + + [GenerateAutoAs(EntireInterfaceHierarchy = true, SkipSystemInterfaces = false)] + public partial class TestClass : TestClassBase, IMy1, IMy2, IMy2, IMy2, IEnumerable, @internal + { + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + public IEnumerator GetEnumerator() + { + throw new NotImplementedException(); + } + } + public class SimplePrinter //: IPrintableComplex { public void Print() { Console.WriteLine("OK"); } public void PrintComplex() { Console.WriteLine("OKC"); } } - public partial class Person2 //: IPrintableComplex + public partial class Person2 /*: IPrintableComplex*/ { //[BeaKona.AutoInterface(typeof(IPrintableComplex), AllowMissingMembers = true, MemberMatch = BeaKona.MemberMatchTypes.Public)] //private readonly SimplePrinter aspect1 = new SimplePrinter(); diff --git a/BeaKona.AutoInterfaceAttributes/GenerateAutoAsAttribute.cs b/BeaKona.AutoInterfaceAttributes/GenerateAutoAsAttribute.cs new file mode 100644 index 0000000..377aa9e --- /dev/null +++ b/BeaKona.AutoInterfaceAttributes/GenerateAutoAsAttribute.cs @@ -0,0 +1,12 @@ +using System; +using System.Diagnostics; + +namespace BeaKona; + +[Conditional("CodeGeneration")] +[AttributeUsage(AttributeTargets.Struct | AttributeTargets.Class, Inherited = false, AllowMultiple = false)] +public sealed class GenerateAutoAsAttribute : Attribute +{ + public bool EntireInterfaceHierarchy { get; set; } = false; + public bool SkipSystemInterfaces { get; set; } = true; +} diff --git a/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs b/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs index 0f9e74c..de6248d 100644 --- a/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs +++ b/BeaKona.AutoInterfaceGenerator/AutoInterfaceSourceGenerator.cs @@ -800,7 +800,6 @@ EventModel CreateEvent(IEventSymbol @event) return error == false && anyReasonToEmitSourceFile ? builder.ToString() : null; } - /// /// Created on demand before each generation pass /// diff --git a/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj b/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj index 874fe08..b1a1a06 100644 --- a/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj +++ b/BeaKona.AutoInterfaceGenerator/BeaKona.AutoInterfaceGenerator.csproj @@ -13,7 +13,7 @@ https://github.com/beakona/AutoInterface git $(TargetsForTfmSpecificContentInPackage);_AddAnalyzersToOutput - 1.0.31 + 1.0.32 true true diff --git a/BeaKona.AutoInterfaceGenerator/GenerateAutoAsSourceGenerator.cs b/BeaKona.AutoInterfaceGenerator/GenerateAutoAsSourceGenerator.cs new file mode 100644 index 0000000..2d77639 --- /dev/null +++ b/BeaKona.AutoInterfaceGenerator/GenerateAutoAsSourceGenerator.cs @@ -0,0 +1,323 @@ +#define PEEK_0 + +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System.Text; + +namespace BeaKona.AutoInterfaceGenerator; + +[Generator] +public sealed class GenerateAutoAsSourceGenerator : ISourceGenerator +{ + public void Initialize(GeneratorInitializationContext context) + { + // Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(GeneratorExecutionContext context) + { + Compilation compilation = context.Compilation; + if (compilation is CSharpCompilation) + { + //retrieve the populated receiver + if (context.SyntaxReceiver is SyntaxReceiver receiver) + { + // get newly bound attribute + if (compilation.GetTypeByMetadataName(typeof(GenerateAutoAsAttribute).FullName) is INamedTypeSymbol generateAutoAsAttributeSymbol) + { + GenerateAutoAsAttribute? GetGenerateAutoAsAttribute(INamedTypeSymbol type) + { + foreach (AttributeData attribute in type.GetAttributes()) + { + if (attribute.AttributeClass != null && attribute.AttributeClass.Equals(generateAutoAsAttributeSymbol, SymbolEqualityComparer.Default)) + { + var result = new GenerateAutoAsAttribute(); + + foreach (KeyValuePair arg in attribute.NamedArguments) + { + switch (arg.Key) + { + case nameof(GenerateAutoAsAttribute.EntireInterfaceHierarchy): + { + if (arg.Value.Value is bool b) + { + result.EntireInterfaceHierarchy = b; + } + } + break; + case nameof(GenerateAutoAsAttribute.SkipSystemInterfaces): + { + if (arg.Value.Value is bool b) + { + result.SkipSystemInterfaces = b; + } + } + break; + } + } + + return result; + } + } + + return null; + } + + var types = new List(); + + foreach (TypeDeclarationSyntax candidate in receiver.Candidates) + { + SemanticModel model = compilation.GetSemanticModel(candidate.SyntaxTree); + + if (model.GetDeclaredSymbol(candidate) is INamedTypeSymbol type) + { + if (GetGenerateAutoAsAttribute(type) is GenerateAutoAsAttribute attribute) + { + if (type.IsPartial() == false) + { + Helpers.ReportDiagnostic(context, "BKAG01", nameof(AutoInterfaceResource.AG01_title), nameof(AutoInterfaceResource.AG01_message), nameof(AutoInterfaceResource.AG01_description), DiagnosticSeverity.Error, type, + type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + continue; + } + + if (type.IsStatic) + { + Helpers.ReportDiagnostic(context, "BKAG02", nameof(AutoInterfaceResource.AG02_title), nameof(AutoInterfaceResource.AG02_message), nameof(AutoInterfaceResource.AG02_description), DiagnosticSeverity.Error, type, + type.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); + continue; + } + + if (type.TypeKind != TypeKind.Class && type.TypeKind != TypeKind.Struct) + { + Helpers.ReportDiagnostic(context, "BKAG08", nameof(AutoInterfaceResource.AG08_title), nameof(AutoInterfaceResource.AG08_message), nameof(AutoInterfaceResource.AG08_description), DiagnosticSeverity.Error, type); + continue; + } + + try + { + string? code = GenerateAutoAsSourceGenerator.ProcessClass(context, compilation, type, attribute); + if (code != null) + { + string name = type.Arity > 0 ? $"{type.Name}_{type.Arity}" : type.Name; +#if PEEK_1 + GeneratePreview(context, name, code); +#else + context.AddSource($"{name}_GenerateAutoAs.g.cs", SourceText.From(code, Encoding.UTF8)); +#endif + } + } + catch (Exception ex) + { + Helpers.ReportDiagnostic(context, "BKAG09", nameof(AutoInterfaceResource.AG09_title), nameof(AutoInterfaceResource.AG09_message), nameof(AutoInterfaceResource.AG09_description), DiagnosticSeverity.Error, type, + ex.ToString().Replace("\r", "").Replace("\n", "")); + } + } + } + } + } + } + } + } + +#if PEEK_1 + private static void GeneratePreview(GeneratorExecutionContext context, string name, string code) + { + var output = new StringBuilder(); + output.AppendLine("namespace BeaKona.Output {"); + output.AppendLine($"public static class Debug_{name}"); + output.AppendLine("{"); + output.AppendLine($"public static readonly string Info = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String(\"{Convert.ToBase64String(Encoding.UTF8.GetBytes(code ?? ""))}\"));"); + output.AppendLine("}"); + output.AppendLine("}"); + context.AddSource($"Output_Debug_{name}.g.cs", SourceText.From(output.ToString(), Encoding.UTF8)); + } +#endif + + private static string? ProcessClass(GeneratorExecutionContext context, Compilation compilation, INamedTypeSymbol type, GenerateAutoAsAttribute attribute) + { + var scope = new ScopeInfo(type); + + List interfaces; + + if (attribute.EntireInterfaceHierarchy) + { + interfaces = type.AllInterfaces.Where(i => i.CanBeReferencedByName).ToList(); + } + else + { + interfaces = new List(); + + //interface list is small, we will not use HashSet here + for (var t = type; t != null; t = t.BaseType) + { + foreach (var @interface in t.Interfaces) + { + if (@interface.CanBeReferencedByName && interfaces.Contains(@interface) == false) + { + interfaces.Add(@interface); + } + } + } + } + + if (attribute.SkipSystemInterfaces) + { + for (int i = 0; i < interfaces.Count; i++) + { + var @interface = interfaces[i]; + + if (@interface.ContainingNamespace is INamespaceSymbol @namespace) + { + if (@namespace.FirstNonGlobalNamespace() is INamespaceSymbol first) + { + if (first.Name.Equals("System", StringComparison.InvariantCulture) || first.Name.StartsWith("System.", StringComparison.InvariantCulture)) + { + interfaces.RemoveAt(i--); + } + } + } + } + } + + var options = SourceBuilderOptions.Load(context, null); + var builder = new SourceBuilder(options); + + ICodeTextWriter writer = new CSharpCodeTextWriter(context, compilation); + bool anyReasonToEmitSourceFile = false; + bool error = false; + + builder.AppendLine("// "); + //bool isNullable = compilation.Options.NullableContextOptions == NullableContextOptions.Enable; + builder.AppendLine("#nullable enable"); + builder.AppendLine(); + writer.WriteNamespaceBeginning(builder, type.ContainingNamespace); + + List containingTypes = []; + for (INamedTypeSymbol? ct = type.ContainingType; ct != null; ct = ct.ContainingType) + { + containingTypes.Insert(0, ct); + } + + foreach (INamedTypeSymbol ct in containingTypes) + { + builder.AppendIndentation(); + writer.WriteTypeDeclarationBeginning(builder, ct, new ScopeInfo(ct)); + builder.AppendLine(); + builder.AppendIndentation(); + builder.AppendLine('{'); + builder.IncrementIndentation(); + } + + builder.AppendIndentation(); + writer.WriteTypeDeclarationBeginning(builder, type, scope); + builder.AppendLine(); + builder.AppendIndentation(); + builder.AppendLine('{'); + builder.IncrementIndentation(); + + //type.TypeArguments + //type.Name + + //sort interfaces + + if (interfaces.Any()) + { + anyReasonToEmitSourceFile = true; + + string GetModifiedMethodName(string methodName) + { + if (methodName.StartsWith("I", StringComparison.InvariantCulture) && methodName.Length > 1 && char.IsUpper(methodName[1])) + { + methodName = methodName.Substring(1); + } + return "As" + methodName; + } + + void WriteMethod(INamedTypeSymbol @interface, int? index) + { + builder.AppendIndentation(); + builder.Append(@interface.DeclaredAccessibility == Accessibility.Internal ? "internal" : "public"); + builder.Append(' '); + writer.WriteTypeReference(builder, @interface, scope); + builder.Append(' '); + builder.Append(GetModifiedMethodName(@interface.Name)); + //writer.WriteIdentifier(builder, @interface); + if (index.HasValue) + { + builder.Append('_'); + builder.Append(index.Value); + } + builder.Append("() => "); + writer.WriteHolderReference(builder, type, scope); + builder.Append(';'); + builder.AppendLine(); + } + + foreach (var group in interfaces.GroupBy(i => i.Name)) + { + if (group.Count() == 1) + { + WriteMethod(group.First(), null); + } + else + { + int index = 0; + + foreach (var @interface in group.OrderBy(i => i.TypeArguments.Length).ThenBy(i => i.Name)) + { + WriteMethod(@interface, index++); + } + } + } + } + + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + + for (int i = 0; i < containingTypes.Count; i++) + { + builder.AppendLine(); + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + } + + if (type.ContainingNamespace != null && type.ContainingNamespace.ConstituentNamespaces.Length > 0) + { + builder.AppendLine(); + builder.DecrementIndentation(); + builder.AppendIndentation(); + builder.Append('}'); + } + + if (builder.Options.InsertFinalNewLine) + { + builder.AppendLine(); + } + + return error == false && anyReasonToEmitSourceFile ? builder.ToString() : null; + } + + /// + /// Created on demand before each generation pass + /// + private sealed class SyntaxReceiver : ISyntaxReceiver + { + public List Candidates { get; } = []; + + /// + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any type with at least one attribute is a candidate for source generation + if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax && typeDeclarationSyntax.AttributeLists.Count > 0) + { + this.Candidates.Add(typeDeclarationSyntax); + } + } + } +} diff --git a/BeaKona.AutoInterfaceGenerator/INamespaceSymbolExtensions.cs b/BeaKona.AutoInterfaceGenerator/INamespaceSymbolExtensions.cs new file mode 100644 index 0000000..49d17cf --- /dev/null +++ b/BeaKona.AutoInterfaceGenerator/INamespaceSymbolExtensions.cs @@ -0,0 +1,16 @@ +namespace BeaKona.AutoInterfaceGenerator; + +internal static class INamespaceSymbolExtensions +{ + public static INamespaceSymbol? FirstNonGlobalNamespace(this INamespaceSymbol @this) + { + INamespaceSymbol? last = null; + + for (var n = @this; n.IsGlobalNamespace == false; n = n.ContainingNamespace) + { + last = n; + } + + return last; + } +} \ No newline at end of file