From 5b7c6a864f369b65b502083eb2ca2f7abd16c0b9 Mon Sep 17 00:00:00 2001 From: Ahmed Zamil Date: Thu, 28 Aug 2025 00:10:20 +0300 Subject: [PATCH] 1 --- Esiur/Esiur.csproj | 2 +- Esiur/Proxy/ResourceGenerator.cs | 493 ++++++++++++---------- Esiur/Proxy/ResourceGeneratorClassInfo.cs | 14 +- 3 files changed, 278 insertions(+), 231 deletions(-) diff --git a/Esiur/Esiur.csproj b/Esiur/Esiur.csproj index 18399d0..f086228 100644 --- a/Esiur/Esiur.csproj +++ b/Esiur/Esiur.csproj @@ -43,7 +43,7 @@ - + diff --git a/Esiur/Proxy/ResourceGenerator.cs b/Esiur/Proxy/ResourceGenerator.cs index fd94914..b532bf0 100644 --- a/Esiur/Proxy/ResourceGenerator.cs +++ b/Esiur/Proxy/ResourceGenerator.cs @@ -1,188 +1,81 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Linq; -using System.Text.RegularExpressions; +// ================================ +// FILE: ResourceIncrementalGenerator.cs +// Replaces: ResourceGenerator.cs + ResourceGeneratorReceiver.cs +// ================================ +using Esiur.Core; +using Esiur.Data; using Esiur.Net.IIP; using Esiur.Resource; using Esiur.Resource.Template; -using Esiur.Data; -using System.IO; -using Esiur.Core; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; -namespace Esiur.Proxy; - -[Generator] -[System.Diagnostics.CodeAnalysis.SuppressMessage("MicrosoftCodeAnalysisCorrectness", "RS1036:Specify analyzer banned API enforcement setting", Justification = "")] -public class ResourceGenerator : IIncrementalGenerator +namespace Esiur.Proxy { - - private KeyList cache = new(); - // private List inProgress = new(); - - public void Initialize(GeneratorInitializationContext context) + [Generator(LanguageNames.CSharp)] + public sealed class ResourceGenerator : IIncrementalGenerator { - // Register receiver - - context.RegisterForSyntaxNotifications(() => new ResourceGeneratorReceiver()); - } - - - void ReportError(GeneratorExecutionContext context, string title, string msg, string category) - { - context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("MySG001", title, msg, category, DiagnosticSeverity.Error, true), Location.None)); - } - - - void GenerateModel(GeneratorExecutionContext context, TypeTemplate[] templates) - { - foreach (var tmp in templates) + public void Initialize(IncrementalGeneratorInitializationContext context) { - if (tmp.Type == TemplateType.Resource) + // 1) Discover candidate classes via a cheap syntax filter + var perClass = context.SyntaxProvider.CreateSyntaxProvider( + (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0, + (ctx, _) => AnalyzeClass(ctx) + ) + .Where( x => x is not null)!; + + // 2) Aggregate import URLs (distinct) + var importUrls = perClass + .SelectMany((x, y) => x.Value.ImportUrls) + .Collect() + .Select( (urls, y) => urls.Distinct(StringComparer.Ordinal).ToImmutableArray()); + + // 3) Aggregate class infos and merge partials by stable key + var mergedResources = perClass + .Select( (x, y) => x.Value.ClassInfo) + .Where( ci => ci is not null)! + .Select( (ci, y) => ci!) + .Collect() + .Select( (list, y) => MergePartials(list)); + + // 4) Generate: A) remote templates (from ImportAttribute URLs) + context.RegisterSourceOutput(importUrls, (spc, urls) => { - var source = TemplateGenerator.GenerateClass(tmp, templates, false); - context.AddSource(tmp.ClassName + ".Generated.cs", source); - } - else if (tmp.Type == TemplateType.Record) - { - var source = TemplateGenerator.GenerateRecord(tmp, templates); - context.AddSource(tmp.ClassName + ".Generated.cs", source); - } - } - - // generate info class - - - var typesFile = "using System; \r\n namespace Esiur { public static class Generated { public static Type[] Resources {get;} = new Type[] { " + - string.Join(",", templates.Where(x => x.Type == TemplateType.Resource).Select(x => $"typeof({x.ClassName})")) - + " }; \r\n public static Type[] Records { get; } = new Type[] { " + - string.Join(",", templates.Where(x => x.Type == TemplateType.Record).Select(x => $"typeof({x.ClassName})")) - + " }; " + - - "\r\n } \r\n}"; - - context.AddSource("Esiur.Generated.cs", typesFile); - - } - - - - public static string SuggestExportName(string fieldName) - { - if (Char.IsUpper(fieldName[0])) - return fieldName.Substring(0, 1).ToLower() + fieldName.Substring(1); - else - return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); - } - - public static string FormatAttribute(AttributeData attribute) - { - if (!(attribute.AttributeClass is object)) - throw new Exception("AttributeClass not found"); - - var className = attribute.AttributeClass.ToDisplayString(); - - if (!attribute.ConstructorArguments.Any() & !attribute.ConstructorArguments.Any()) - return $"[{className}]"; - - var strBuilder = new StringBuilder(); - - strBuilder.Append("["); - strBuilder.Append(className); - strBuilder.Append("("); - - strBuilder.Append(String.Join(", ", attribute.ConstructorArguments.Select(ca => FormatConstant(ca)))); - - strBuilder.Append(String.Join(", ", attribute.NamedArguments.Select(na => $"{na.Key} = {FormatConstant(na.Value)}"))); - - strBuilder.Append(")]"); - - return strBuilder.ToString(); - } - - public static string FormatConstant(TypedConstant constant) - { - if (constant.Kind == TypedConstantKind.Array) - return $"new {constant.Type.ToDisplayString()} {{{string.Join(", ", constant.Values.Select(v => FormatConstant(v)))}}}"; - else - return constant.ToCSharpString(); - } - - - public void Execute(GeneratorExecutionContext context) - { - - try - { - - - if (!(context.SyntaxContextReceiver is ResourceGeneratorReceiver receiver)) - return; - - //if (receiver.Imports.Count > 0 && !Debugger.IsAttached) - //{ - // Debugger.Launch(); - //} - - foreach (var path in receiver.Imports) - { - if (!TemplateGenerator.urlRegex.IsMatch(path)) - continue; - - - - if (cache.Contains(path)) + if (urls.Length == 0) return; + foreach (var path in urls) { - GenerateModel(context, cache[path]); - continue; + try + { + if (!TemplateGenerator.urlRegex.IsMatch(path)) + continue; + + var parts = TemplateGenerator.urlRegex.Split(path); + var con = Warehouse.Default.Get($"{parts[1]}://{parts[2]}").Wait(20000); + var templates = con.GetLinkTemplates(parts[3]).Wait(60000); + + EmitTemplates(spc, templates); + } + catch (Exception ex) + { + Report(spc, "Esiur", ex.Message, DiagnosticSeverity.Error); + } } + }); - // Syncronization - //if (inProgress.Contains(path)) - // continue; - - //inProgress.Add(path); - - var url = TemplateGenerator.urlRegex.Split(path); - - - try - { - var con = Warehouse.Default.Get(url[1] + "://" + url[2]).Wait(20000); - var templates = con.GetLinkTemplates(url[3]).Wait(60000); - - cache[path] = templates; - - // make sources - GenerateModel(context, templates); - - } - catch (Exception ex) - { - ReportError(context, ex.Source, ex.Message, "Esiur"); - } - - //inProgress.Remove(path); - } - - - //#if DEBUG - - //#endif - - //var toImplement = receiver.Classes.Where(x => x.Fields.Length > 0); - - foreach (var ci in receiver.Classes.Values) + // 4) Generate: B) per resource partials (properties/events, base IResource impl if needed) + context.RegisterSourceOutput(mergedResources, static (spc, classes) => { - try + foreach (var ci in classes) { - - var code = @$"using Esiur.Resource; + try + { + var code = @$"using Esiur.Resource; using Esiur.Core; #nullable enable @@ -190,68 +83,230 @@ using Esiur.Core; namespace {ci.ClassSymbol.ContainingNamespace.ToDisplayString()} {{ "; - if (ci.IsInterfaceImplemented(receiver.Classes)) - code += $"public partial class {ci.Name} {{\r\n"; - else - { - code += - @$" public partial class {ci.Name} : IResource {{ + if (IsInterfaceImplemented(ci, classes)) + code += $"public partial class {ci.Name} {{\r\n"; + else + { + code += +$@" public partial class {ci.Name} : IResource {{ public virtual Instance? Instance {{ get; set; }} public virtual event DestroyedEvent? OnDestroy; public virtual void Destroy() {{ OnDestroy?.Invoke(this); }} "; - if (!ci.HasTrigger) - code += - "\tpublic virtual AsyncReply Trigger(ResourceTrigger trigger) => new AsyncReply(true);\r\n\r\n"; + if (!ci.HasTrigger) + code += "\tpublic virtual AsyncReply Trigger(ResourceTrigger trigger) => new AsyncReply(true);\r\n\r\n"; + } + + foreach (var f in ci.Fields) + { + var givenName = f.GetAttributes().FirstOrDefault(x => x.AttributeClass?.Name == "ExportAttribute")?.ConstructorArguments.FirstOrDefault().Value as string; + + var fn = f.Name; + var pn = string.IsNullOrEmpty(givenName) ? SuggestExportName(fn) : givenName; + + var attrs = string.Join("\r\n\t", f.GetAttributes().Select(x => FormatAttribute(x))); + + if (f.Type.Name.StartsWith("ResourceEventHandler") || f.Type.Name.StartsWith("CustomResourceEventHandler")) + { + code += $"\t{attrs}\r\n\t public event {f.Type} {pn};\r\n"; + } + else + { + code += $"\t{attrs}\r\n\t public {f.Type} {pn} {{ \r\n\t\t get => {fn}; \r\n\t\t set {{ \r\n\t\t this.{fn} = value; \r\n\t\t Instance?.Modified(); \r\n\t\t}}\r\n\t}}\r\n"; + } + } + + code += "}}\r\n"; + + spc.AddSource(ci.Name + ".g.cs", code); } - - //Debugger.Launch(); - - foreach (var f in ci.Fields) + catch (Exception ex) { - var givenName = f.GetAttributes().Where(x => x.AttributeClass.Name == "ExportAttribute").FirstOrDefault()?.ConstructorArguments.FirstOrDefault().Value as string; - - var fn = f.Name; - var pn = string.IsNullOrEmpty(givenName) ? SuggestExportName(fn) : givenName; - - // copy attributes - //Debugger.Launch(); - - var attrs = string.Join("\r\n\t", f.GetAttributes().Select(x => FormatAttribute(x))); - - //Debugger.Launch(); - if (f.Type.Name.StartsWith("ResourceEventHandler") || f.Type.Name.StartsWith("CustomResourceEventHandler")) - { - code += $"\t{attrs}\r\n\t public event {f.Type} {pn};\r\n"; - } - else - { - code += $"\t{attrs}\r\n\t public {f.Type} {pn} {{ \r\n\t\t get => {fn}; \r\n\t\t set {{ \r\n\t\t this.{fn} = value; \r\n\t\t Instance?.Modified(); \r\n\t\t}}\r\n\t}}\r\n"; - } + spc.AddSource(ci.Name + ".Error.g.cs", $"/*\r\n{ex}\r\n*/"); } - - code += "}}\r\n"; - - context.AddSource(ci.Name + ".g.cs", code); - } - catch (Exception ex) + }); + } + + + // === Analysis === + private static PerClass? AnalyzeClass(GeneratorSyntaxContext ctx) + { + var cds = (ClassDeclarationSyntax)ctx.Node; + var cls = ctx.SemanticModel.GetDeclaredSymbol(cds) as ITypeSymbol; + if (cls is null) return null; + + var attrs = cls.GetAttributes(); + + // Collect ImportAttribute URLs + var importUrls = attrs + .Where(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ImportAttribute") + .SelectMany(a => a.ConstructorArguments.Select(x => x.Value?.ToString())) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Cast() + .ToImmutableArray(); + + // If class has ResourceAttribute, gather details + var hasResource = attrs.Any(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ResourceAttribute"); + ResourceClassInfo? classInfo = null; + if (hasResource) + { + bool hasTrigger = cds.Members + .OfType() + .Select(m => ctx.SemanticModel.GetDeclaredSymbol(m) as IMethodSymbol) + .Where(s => s is not null) + .Any(s => s!.Name == "Trigger" && s.Parameters.Length == 1 && s.Parameters[0].Type.ToDisplayString() == "Esiur.Resource.ResourceTrigger"); + + var exportedFields = cds.Members + .OfType() + .SelectMany(f => f.Declaration.Variables.Select(v => (f, v))) + .Select(t => ctx.SemanticModel.GetDeclaredSymbol(t.v) as IFieldSymbol) + .Where(f => f is not null && !f!.IsConst) + .Where(f => f!.GetAttributes().Any(a => a.AttributeClass?.ToDisplayString() == "Esiur.Resource.ExportAttribute")) + .Cast() + .ToList(); + + bool hasInterface = cls.AllInterfaces.Any(x => x.ToDisplayString() == "Esiur.Resource.IResource"); + + var key = $"{cls.ContainingAssembly.Name}:{cls.ContainingNamespace.ToDisplayString()}.{cls.Name}"; + + classInfo = new ResourceClassInfo + ( + Key: key, + Name: cls.Name, + ClassDeclaration: cds, + ClassSymbol: cls, + Fields: exportedFields, + HasInterface: hasInterface, + HasTrigger: hasTrigger + ); + } + + return new PerClass(importUrls, classInfo); + } + + private static ImmutableArray MergePartials(ImmutableArray list) + { + var byKey = new Dictionary(StringComparer.Ordinal); + foreach (var item in list) + { + if (byKey.TryGetValue(item.Key, out var existing)) { - context.AddSource(ci.Name + ".Error.g.cs", $"/*\r\n{ex}\r\n*/"); + // merge fields + flags + var mergedFields = existing.Fields.Concat(item.Fields).ToList(); + byKey[item.Key] = existing with + { + Fields = mergedFields, + HasInterface = existing.HasInterface || item.HasInterface, + HasTrigger = existing.HasTrigger || item.HasTrigger + }; + } + else + { + byKey[item.Key] = item with { Fields = item.Fields.ToList() }; } } + return byKey.Values.ToImmutableArray(); } - catch (Exception ex) + + // Determine if the base already implements IResource (either directly or via another generated part) + private static bool IsInterfaceImplemented(ResourceClassInfo ci, ImmutableArray merged) { - - context.AddSource("Error.g.cs", $"/*\r\n{ex}\r\n*/"); + if (ci.HasInterface) return true; + var baseType = ci.ClassSymbol.BaseType; + if (baseType is null) return false; + var baseKey = $"{baseType.ContainingAssembly.Name}:{baseType.ContainingNamespace.ToDisplayString()}.{baseType.Name}"; + return merged.Any(x => x.Key == baseKey); } - } - public void Initialize(IncrementalGeneratorInitializationContext context) - { - context.RegisterForSyntaxNotifications(() => new ResourceGeneratorReceiver()); + // === Emission helpers (ported from your original generator) === + private static void EmitTemplates(SourceProductionContext spc, TypeTemplate[] templates) + { + foreach (var tmp in templates) + { + if (tmp.Type == TemplateType.Resource) + { + var source = TemplateGenerator.GenerateClass(tmp, templates, false); + spc.AddSource(tmp.ClassName + ".Generated.cs", source); + } + else if (tmp.Type == TemplateType.Record) + { + var source = TemplateGenerator.GenerateRecord(tmp, templates); + spc.AddSource(tmp.ClassName + ".Generated.cs", source); + } + } + + var typesFile = "using System; \r\n namespace Esiur { public static class Generated { public static Type[] Resources {get;} = new Type[] { " + + string.Join(",", templates.Where(x => x.Type == TemplateType.Resource).Select(x => $"typeof({x.ClassName})")) + + " }; \r\n public static Type[] Records { get; } = new Type[] { " + + string.Join(",", templates.Where(x => x.Type == TemplateType.Record).Select(x => $"typeof({x.ClassName})")) + + " }; " + + + "\r\n } \r\n}"; + + spc.AddSource("Esiur.Generated.cs", typesFile); + } + + private static void Report(SourceProductionContext ctx, string title, string message, DiagnosticSeverity severity) + { + var descriptor = new DiagnosticDescriptor("ESIUR001", title, message, "Esiur", severity, true); + ctx.ReportDiagnostic(Diagnostic.Create(descriptor, Location.None)); + } + + // === Formatting helpers from your original code === + private static string SuggestExportName(string fieldName) + { + if (char.IsUpper(fieldName[0])) + return fieldName.Substring(0, 1).ToLowerInvariant() + fieldName.Substring(1); + else + return char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1); + } + + private static string FormatAttribute(AttributeData attribute) + { + if (attribute.AttributeClass is null) + throw new Exception("AttributeClass not found"); + + var className = attribute.AttributeClass.ToDisplayString(); + if (!attribute.ConstructorArguments.Any() & !attribute.NamedArguments.Any()) + return $"[{className}]"; + + var sb = new StringBuilder(); + sb.Append('[').Append(className).Append('('); + if (attribute.ConstructorArguments.Any()) + sb.Append(string.Join(", ", attribute.ConstructorArguments.Select(FormatConstant))); + if (attribute.NamedArguments.Any()) + { + if (attribute.ConstructorArguments.Any()) sb.Append(", "); + sb.Append(string.Join(", ", attribute.NamedArguments.Select(na => $"{na.Key} = {FormatConstant(na.Value)}"))); + } + sb.Append(")] "); + return sb.ToString(); + } + + private static string FormatConstant(TypedConstant constant) + { + if (constant.Kind == TypedConstantKind.Array) + return $"new {constant.Type?.ToDisplayString()} {{{string.Join(", ", constant.Values.Select(FormatConstant))}}}"; + return constant.ToCSharpString(); + } + + // === Data carriers for the pipeline === + private readonly record struct PerClass( + ImmutableArray ImportUrls, + ResourceClassInfo? ClassInfo + ); + + private sealed record ResourceClassInfo( + string Key, + string Name, + ClassDeclarationSyntax ClassDeclaration, + ITypeSymbol ClassSymbol, + List Fields, + bool HasInterface, + bool HasTrigger + ); } -} +} \ No newline at end of file diff --git a/Esiur/Proxy/ResourceGeneratorClassInfo.cs b/Esiur/Proxy/ResourceGeneratorClassInfo.cs index 2570843..c0817c0 100644 --- a/Esiur/Proxy/ResourceGeneratorClassInfo.cs +++ b/Esiur/Proxy/ResourceGeneratorClassInfo.cs @@ -4,25 +4,17 @@ using System; using System.Collections.Generic; using System.Text; + namespace Esiur.Proxy; public struct ResourceGeneratorClassInfo { public string Name { get; set; } public bool HasInterface { get; set; } - public bool HasTrigger { get; set; } public List Fields { get; set; } public ITypeSymbol ClassSymbol { get; set; } - public ClassDeclarationSyntax ClassDeclaration { get; set; } - public bool IsInterfaceImplemented(Dictionary classes) - { - if (HasInterface) - return true; - - // Are we going to generate the interface for the parent ? - var fullName = ClassSymbol.BaseType.ContainingAssembly + "." + ClassSymbol.BaseType.Name; - return classes.ContainsKey(fullName); - } + // Deprecated in incremental path. Use IsInterfaceImplemented(ResourceClassInfo, merged) instead. + public bool IsInterfaceImplemented(System.Collections.Generic.Dictionary classes) => HasInterface; }