From 4d9004081cad72850aae1ef94ba5a7b3f9ab5bee Mon Sep 17 00:00:00 2001 From: curtisy Date: Wed, 24 Dec 2025 16:31:55 +0100 Subject: [PATCH 1/3] Refactor FilterGenerator to use IIncrementalGenerator instead of deprecated ISourceGenerator This also gets rid of unused code --- .../AttributeSyntaxReceiver.cs | 26 ----- .../FilterGenerator.cs | 107 ++++++++++-------- .../GenerateAutoFilterAttribute.cs | 4 +- src/AutoFilterer.Generators/SyntaxReceiver.cs | 20 ---- 4 files changed, 60 insertions(+), 97 deletions(-) delete mode 100644 src/AutoFilterer.Generators/AttributeSyntaxReceiver.cs delete mode 100644 src/AutoFilterer.Generators/SyntaxReceiver.cs diff --git a/src/AutoFilterer.Generators/AttributeSyntaxReceiver.cs b/src/AutoFilterer.Generators/AttributeSyntaxReceiver.cs deleted file mode 100644 index 2d0a641..0000000 --- a/src/AutoFilterer.Generators/AttributeSyntaxReceiver.cs +++ /dev/null @@ -1,26 +0,0 @@ -using AutoFilterer.Generators.Extensions; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace DotNurse.CodeAnalysis; - -public class AttributeSyntaxReceiver : ISyntaxReceiver - where TAttribute : Attribute -{ - public IList Classes { get; } = new List(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && - classDeclarationSyntax.AttributeLists.Count > 0 && - classDeclarationSyntax.AttributeLists - .Any(al => al.Attributes - .Any(a => a.Name.ToString().EnsureEndsWith("Attribute").Equals(typeof(TAttribute).Name)))) - { - Classes.Add(classDeclarationSyntax); - } - } -} \ No newline at end of file diff --git a/src/AutoFilterer.Generators/FilterGenerator.cs b/src/AutoFilterer.Generators/FilterGenerator.cs index 62815d6..6d1d854 100644 --- a/src/AutoFilterer.Generators/FilterGenerator.cs +++ b/src/AutoFilterer.Generators/FilterGenerator.cs @@ -2,74 +2,85 @@ using Microsoft.CodeAnalysis.Text; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using System.Text; -using System.Threading.Tasks; using Microsoft.CodeAnalysis.CSharp.Syntax; -using DotNurse.CodeAnalysis; -using AutoFilterer.Generators.Extensions; using Microsoft.CodeAnalysis.CSharp; -using System.Diagnostics; namespace AutoFilterer.Generators; +using Extensions; + [Generator] -public class FilterGenerator : ISourceGenerator +public class FilterGenerator : IIncrementalGenerator { - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new AttributeSyntaxReceiver()); + var classDeclarations = context.SyntaxProvider + .CreateSyntaxProvider( + predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) + .Where(static m => m is not null); + + var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect()); + + context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Left, source.Right, spc)); } - public void Execute(GeneratorExecutionContext context) + private static ClassDeclarationSyntax GetSemanticTargetForGeneration(GeneratorSyntaxContext context) { - if (!(context.SyntaxReceiver is AttributeSyntaxReceiver receiver)) - return; + var classDeclaration = (ClassDeclarationSyntax)context.Node; - //if (!Debugger.IsAttached) - //{ - // Debugger.Launch(); - //} - - foreach (var classSyntax in receiver.Classes) + foreach (var attributeList in classDeclaration.AttributeLists) { - var attribute = classSyntax.AttributeLists.SelectMany(sm => sm.Attributes).FirstOrDefault(x => x.Name.ToString().EnsureEndsWith("Attribute").Equals(typeof(GenerateAutoFilterAttribute).Name)); - var namespaceParam = attribute.ArgumentList?.Arguments.FirstOrDefault(); // Temprorary... Attribute has only one argument for now. - - var model = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree); - var symbol = model.GetDeclaredSymbol(classSyntax); - var attrs = symbol.GetAttributes(); - - var realNamespace = GetNamespaceRecursively(symbol.ContainingNamespace); + foreach (var attribute in attributeList.Attributes) + { + var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute); + var attributeSymbol = symbolInfo.Symbol as IMethodSymbol; - var properties = symbol.GetMembers().OfType() - .Where(x => !x.IsStatic && !x.ContainingType.IsGenericType && x.Kind == SymbolKind.Property); + var fullName = attributeSymbol?.ContainingType.ToDisplayString() + ?? attribute.Name.ToString(); - context.AddSource($"{symbol.Name}FilterDto.g.cs", - SourceText.From(GetFilterDtoCode(symbol.Name, properties, namespaceParam?.ToString().Trim('\"') ?? realNamespace), Encoding.UTF8)); + if (fullName.EnsureEndsWith("Attribute").EndsWith(nameof(GenerateAutoFilterAttribute))) + { + return classDeclaration; + } + } } + + return null; } - private static Dictionary> GetClassAttributePairs(GeneratorExecutionContext context, - SyntaxReceiver receiver) + private static void Execute(Compilation compilation, ImmutableArray classes, SourceProductionContext context) { - var compilation = context.Compilation; - var classSymbols = new Dictionary>(); - foreach (var clazz in receiver.CandidateClasses) + if (classes.IsDefaultOrEmpty) { + return; + } + + foreach (var classSyntax in classes) { - var model = compilation.GetSemanticModel(clazz.SyntaxTree); - var classSymbol = model.GetDeclaredSymbol(clazz); - var attributes = classSymbol.GetAttributes(); - if (attributes.Any(ad => ad.AttributeClass.Name == nameof(GenerateAutoFilterAttribute))) + var model = compilation.GetSemanticModel(classSyntax.SyntaxTree); + if (model.GetDeclaredSymbol(classSyntax) is not { } symbol) { - classSymbols.Add((INamedTypeSymbol)classSymbol, attributes.ToList()); + continue; } - } - return classSymbols; + var attribute = symbol.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.Name == nameof(GenerateAutoFilterAttribute)); + var namespaceParam = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temprorary... Attribute has only one argument for now. + var realNamespace = GetNamespaceRecursively(symbol.ContainingNamespace); + + var properties = symbol.GetMembers().OfType() + .Where(x => !x.IsStatic && !x.ContainingType.IsGenericType && x.Kind == SymbolKind.Property); + + var sourceCode = GetFilterDtoCode(symbol.Name, properties, namespaceParam ?? realNamespace); + + context.AddSource($"{symbol.Name}FilterDto.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); + } } - internal string GetFilterDtoCode(string className, IEnumerable properties, + private static string GetFilterDtoCode(string className, IEnumerable properties, string @namespace = null) { var start = $@" @@ -88,10 +99,11 @@ public partial class {className}Filter : PaginationFilterBase foreach (var property in properties) { - string propertyType = property.Type.ToDisplayString(NullableFlowState.None); + var propertyType = property.Type.ToDisplayString(NullableFlowState.None); - if (TypeMapping.Mappings.TryGetValue(propertyType, out var mapped)) + if (TypeMapping.Mappings.TryGetValue(propertyType, out var mapped)) { propertyType = mapped; + } if (propertyType.Equals(nameof(String), StringComparison.InvariantCultureIgnoreCase)) { @@ -100,13 +112,10 @@ public partial class {className}Filter : PaginationFilterBase body.AppendLine($"\t\tpublic virtual {propertyType} {property.Name} {{ get; set; }}"); } - var end = "\t}\n}"; - - - return start + body.ToString() + end; + return start + body + "\t}\n}"; } - private string GetNamespaceRecursively(INamespaceSymbol symbol) + private static string GetNamespaceRecursively(INamespaceSymbol symbol) { if (symbol.ContainingNamespace == null) { @@ -115,4 +124,4 @@ private string GetNamespaceRecursively(INamespaceSymbol symbol) return (GetNamespaceRecursively(symbol.ContainingNamespace) + "." + symbol.Name).Trim('.'); } -} +} \ No newline at end of file diff --git a/src/AutoFilterer.Generators/GenerateAutoFilterAttribute.cs b/src/AutoFilterer.Generators/GenerateAutoFilterAttribute.cs index cf7b2bc..f31bbad 100644 --- a/src/AutoFilterer.Generators/GenerateAutoFilterAttribute.cs +++ b/src/AutoFilterer.Generators/GenerateAutoFilterAttribute.cs @@ -9,8 +9,8 @@ public GenerateAutoFilterAttribute() public GenerateAutoFilterAttribute(string @namespace) { - Namespace = @Namespace; + Namespace = @namespace; } public string Namespace { get; } -} +} \ No newline at end of file diff --git a/src/AutoFilterer.Generators/SyntaxReceiver.cs b/src/AutoFilterer.Generators/SyntaxReceiver.cs deleted file mode 100644 index c5ff6de..0000000 --- a/src/AutoFilterer.Generators/SyntaxReceiver.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using System.Collections.Generic; - -namespace AutoFilterer.Generators; - -public class SyntaxReceiver : ISyntaxReceiver -{ - public IList CandidateClasses { get; } = new List(); - - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - // any field with at least one attribute is a candidate for property generation - if (syntaxNode is ClassDeclarationSyntax classDeclarationSyntax && - classDeclarationSyntax.AttributeLists.Count > 0) - { - CandidateClasses.Add(classDeclarationSyntax); - } - } -} From dfb48015a9c60cc4ea698230a8214260f264c76d Mon Sep 17 00:00:00 2001 From: curtisy Date: Wed, 24 Dec 2025 16:55:43 +0100 Subject: [PATCH 2/3] Filter out more irrelevant classes and use StrinBuilder for complete generation --- .../FilterGenerator.cs | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/AutoFilterer.Generators/FilterGenerator.cs b/src/AutoFilterer.Generators/FilterGenerator.cs index 6d1d854..f9cf7a2 100644 --- a/src/AutoFilterer.Generators/FilterGenerator.cs +++ b/src/AutoFilterer.Generators/FilterGenerator.cs @@ -19,7 +19,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context) { var classDeclarations = context.SyntaxProvider .CreateSyntaxProvider( - predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 }, + predicate: static (s, _) => IsSyntaxTargetForGeneration(s), transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx)) .Where(static m => m is not null); @@ -28,6 +28,17 @@ public void Initialize(IncrementalGeneratorInitializationContext context) context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Left, source.Right, spc)); } + private static bool IsSyntaxTargetForGeneration(SyntaxNode node) + { + if (node is not ClassDeclarationSyntax classDeclaration || classDeclaration.AttributeLists.Count == 0) { + return false; + } + + return classDeclaration.AttributeLists + .SelectMany(al => al.Attributes) + .Any(a => a.Name.ToString().EnsureEndsWith("Attribute").EndsWith(nameof(GenerateAutoFilterAttribute))); + } + private static ClassDeclarationSyntax GetSemanticTargetForGeneration(GeneratorSyntaxContext context) { var classDeclaration = (ClassDeclarationSyntax)context.Node; @@ -36,12 +47,8 @@ private static ClassDeclarationSyntax GetSemanticTargetForGeneration(GeneratorSy { foreach (var attribute in attributeList.Attributes) { - var symbolInfo = context.SemanticModel.GetSymbolInfo(attribute); - var attributeSymbol = symbolInfo.Symbol as IMethodSymbol; - - var fullName = attributeSymbol?.ContainingType.ToDisplayString() - ?? attribute.Name.ToString(); - + var attributeSymbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol as IMethodSymbol; + var fullName = attributeSymbol?.ContainingType.ToDisplayString() ?? attribute.Name.ToString(); if (fullName.EnsureEndsWith("Attribute").EndsWith(nameof(GenerateAutoFilterAttribute))) { return classDeclaration; @@ -58,7 +65,7 @@ private static void Execute(Compilation compilation, ImmutableArray a.AttributeClass?.Name == nameof(GenerateAutoFilterAttribute)); - var namespaceParam = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temprorary... Attribute has only one argument for now. - var realNamespace = GetNamespaceRecursively(symbol.ContainingNamespace); + var targetNamespace = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temprorary... Attribute has only one argument for now. + if (string.IsNullOrEmpty(targetNamespace)) { + targetNamespace = GetNamespaceRecursively(symbol.ContainingNamespace); + } - var properties = symbol.GetMembers().OfType() - .Where(x => !x.IsStatic && !x.ContainingType.IsGenericType && x.Kind == SymbolKind.Property); + var properties = symbol.GetMembers() + .OfType() + .Where(x => !x.IsStatic && !x.IsIndexer && x.Kind == SymbolKind.Property); - var sourceCode = GetFilterDtoCode(symbol.Name, properties, namespaceParam ?? realNamespace); + var sourceCode = GetFilterDtoCode(symbol.Name, properties, targetNamespace); context.AddSource($"{symbol.Name}FilterDto.g.cs", SourceText.From(sourceCode, Encoding.UTF8)); } @@ -83,19 +93,15 @@ private static void Execute(Compilation compilation, ImmutableArray properties, string @namespace = null) { - var start = $@" -using System; -using AutoFilterer; -using AutoFilterer.Attributes; -using AutoFilterer.Types; - -namespace {@namespace ?? "AutoFilterer.Filters"} -{{ - public partial class {className}Filter : PaginationFilterBase - {{ -"; - - var body = new StringBuilder(); + var generatedCode = new StringBuilder(); + generatedCode.AppendLine("using System;"); + generatedCode.AppendLine("using AutoFilterer.Attributes;"); + generatedCode.AppendLine("using AutoFilterer.Types;"); + generatedCode.AppendLine(); + generatedCode.AppendLine($"namespace {@namespace}"); + generatedCode.AppendLine("{"); + generatedCode.AppendLine($"\tpublic partial class {className}Filter : PaginationFilterBase"); + generatedCode.AppendLine("\t{"); foreach (var property in properties) { @@ -105,14 +111,18 @@ public partial class {className}Filter : PaginationFilterBase propertyType = mapped; } - if (propertyType.Equals(nameof(String), StringComparison.InvariantCultureIgnoreCase)) + if (property.Type.SpecialType == SpecialType.System_String) { - body.AppendLine("\t\t[ToLowerContainsComparison]"); + generatedCode.AppendLine("\t\t[ToLowerContainsComparison]"); } - body.AppendLine($"\t\tpublic virtual {propertyType} {property.Name} {{ get; set; }}"); + + generatedCode.AppendLine($"\t\tpublic virtual {propertyType} {property.Name} {{ get; set; }}"); } - return start + body + "\t}\n}"; + generatedCode.AppendLine("\t}"); + generatedCode.AppendLine("}"); + + return generatedCode.ToString(); } private static string GetNamespaceRecursively(INamespaceSymbol symbol) From 98205d57c712fb9bcb5b4971174487fdcbb3ec90 Mon Sep 17 00:00:00 2001 From: curtisy Date: Wed, 24 Dec 2025 23:24:32 +0100 Subject: [PATCH 3/3] Fix typo in comment --- src/AutoFilterer.Generators/FilterGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AutoFilterer.Generators/FilterGenerator.cs b/src/AutoFilterer.Generators/FilterGenerator.cs index f9cf7a2..cac4204 100644 --- a/src/AutoFilterer.Generators/FilterGenerator.cs +++ b/src/AutoFilterer.Generators/FilterGenerator.cs @@ -75,7 +75,7 @@ private static void Execute(Compilation compilation, ImmutableArray a.AttributeClass?.Name == nameof(GenerateAutoFilterAttribute)); - var targetNamespace = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temprorary... Attribute has only one argument for now. + var targetNamespace = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temporary... Attribute has only one argument for now. if (string.IsNullOrEmpty(targetNamespace)) { targetNamespace = GetNamespaceRecursively(symbol.ContainingNamespace); }