Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions src/AutoFilterer.Generators/AttributeSyntaxReceiver.cs

This file was deleted.

149 changes: 84 additions & 65 deletions src/AutoFilterer.Generators/FilterGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,111 +2,130 @@
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)
{
context.RegisterForSyntaxNotifications(() => new AttributeSyntaxReceiver<GenerateAutoFilterAttribute>());
}

public void Execute(GeneratorExecutionContext context)
public void Initialize(IncrementalGeneratorInitializationContext context)
{
if (!(context.SyntaxReceiver is AttributeSyntaxReceiver<GenerateAutoFilterAttribute> receiver))
return;
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null);

//if (!Debugger.IsAttached)
//{
// Debugger.Launch();
//}
var compilationAndClasses = context.CompilationProvider.Combine(classDeclarations.Collect());

foreach (var classSyntax in receiver.Classes)
{
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.
context.RegisterSourceOutput(compilationAndClasses, (spc, source) => Execute(source.Left, source.Right, spc));
}

var model = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree);
var symbol = model.GetDeclaredSymbol(classSyntax);
var attrs = symbol.GetAttributes();
private static bool IsSyntaxTargetForGeneration(SyntaxNode node)
{
if (node is not ClassDeclarationSyntax classDeclaration || classDeclaration.AttributeLists.Count == 0) {
return false;
}

var realNamespace = GetNamespaceRecursively(symbol.ContainingNamespace);
return classDeclaration.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().EnsureEndsWith("Attribute").EndsWith(nameof(GenerateAutoFilterAttribute)));
}

var properties = symbol.GetMembers().OfType<IPropertySymbol>()
.Where(x => !x.IsStatic && !x.ContainingType.IsGenericType && x.Kind == SymbolKind.Property);
private static ClassDeclarationSyntax GetSemanticTargetForGeneration(GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;

context.AddSource($"{symbol.Name}FilterDto.g.cs",
SourceText.From(GetFilterDtoCode(symbol.Name, properties, namespaceParam?.ToString().Trim('\"') ?? realNamespace), Encoding.UTF8));
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
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;
}
}
}

return null;

Copilot AI Dec 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Returning null from GetSemanticTargetForGeneration could potentially cause issues with nullability warnings in projects with nullable reference types enabled. Consider using a more explicit pattern like returning a nullable type with the proper annotation or using an Option type pattern.

Copilot uses AI. Check for mistakes.

@curtisy1 curtisy1 Dec 24, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is used in a private method and shouldn't happen anyway because the filter is running first, so it's more of a catch-all situation. The calling method does explicitly filter for null afterwards, so I doubt this is an actual issue.

Nullable reference types are disabled so far, so returning a nullable type doesn't really add value here either.

That said, I'm open to suggestions and nullable reference types are something I do want to address in a future PR.

The only useful thing I could do would be wrapping the return type in an Option<T> but that just makes the calling function less readable in my opinion.

}

private static Dictionary<INamedTypeSymbol, IList<AttributeData>> GetClassAttributePairs(GeneratorExecutionContext context,
SyntaxReceiver receiver)
private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes, SourceProductionContext context)
{
var compilation = context.Compilation;
var classSymbols = new Dictionary<INamedTypeSymbol, IList<AttributeData>>();
foreach (var clazz in receiver.CandidateClasses)
if (classes.IsDefaultOrEmpty) {
return;
}

foreach (var classSyntax in classes.Distinct())
{
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 targetNamespace = attribute?.ConstructorArguments.FirstOrDefault().Value?.ToString().Trim('\"'); // Temporary... Attribute has only one argument for now.
if (string.IsNullOrEmpty(targetNamespace)) {
targetNamespace = GetNamespaceRecursively(symbol.ContainingNamespace);
}

var properties = symbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(x => !x.IsStatic && !x.IsIndexer && x.Kind == SymbolKind.Property);

Copilot AI Dec 24, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The property filtering logic now excludes generic types. The old code excluded properties from generic types with !x.ContainingType.IsGenericType, but the new code removes this check. This means that properties from generic classes will now be included in filter generation, which could be a breaking behavioral change.

Suggested change
.Where(x => !x.IsStatic && !x.IsIndexer && x.Kind == SymbolKind.Property);
.Where(x => !x.IsStatic && !x.IsIndexer && x.Kind == SymbolKind.Property && !x.ContainingType.IsGenericType);

Copilot uses AI. Check for mistakes.

@curtisy1 curtisy1 Dec 24, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was intentional because excluding the generic classes would break things like Entity<T> where T is int or Guid and used as a generic primary key I think.

I'm open to add it back though


var sourceCode = GetFilterDtoCode(symbol.Name, properties, targetNamespace);

context.AddSource($"{symbol.Name}FilterDto.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}

internal string GetFilterDtoCode(string className, IEnumerable<IPropertySymbol> properties,
private static string GetFilterDtoCode(string className, IEnumerable<IPropertySymbol> 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)
{
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))
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; }}");
}

var end = "\t}\n}";
generatedCode.AppendLine($"\t\tpublic virtual {propertyType} {property.Name} {{ get; set; }}");
}

generatedCode.AppendLine("\t}");
generatedCode.AppendLine("}");

return start + body.ToString() + end;
return generatedCode.ToString();
}

private string GetNamespaceRecursively(INamespaceSymbol symbol)
private static string GetNamespaceRecursively(INamespaceSymbol symbol)
{
if (symbol.ContainingNamespace == null)
{
Expand All @@ -115,4 +134,4 @@ private string GetNamespaceRecursively(INamespaceSymbol symbol)

return (GetNamespaceRecursively(symbol.ContainingNamespace) + "." + symbol.Name).Trim('.');
}
}
}
4 changes: 2 additions & 2 deletions src/AutoFilterer.Generators/GenerateAutoFilterAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ public GenerateAutoFilterAttribute()

public GenerateAutoFilterAttribute(string @namespace)
{
Namespace = @Namespace;
Namespace = @namespace;
}

public string Namespace { get; }
}
}
20 changes: 0 additions & 20 deletions src/AutoFilterer.Generators/SyntaxReceiver.cs

This file was deleted.

Loading