Search code examples
c#csharp-source-generator

Get attribute property value in C# source generation - IIncrementalGenerator


I have following Attribute:

[AttributeUsage(AttributeTargets.Class)]
public class EventApplyAttribute : Attribute
{
    public string Aggregate { get; }

    public EventApplyAttribute(string aggregate)
    {
        Aggregate = aggregate;
    }
}

I use the Attribute like this:

[EventApplyAttribute(nameof(BaseClass))]
public class Test : BaseEvent{}

I use this source generator:

[Generator]
public class SourceGeneration : IIncrementalGenerator
{
    private const string EventApplyAttribute = "DomainLibrary.EventApplyAttribute";

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValuesProvider<ClassDeclarationSyntax> classDeclarations = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                EventApplyAttribute,
                predicate: (node, _) => node is ClassDeclarationSyntax,
                transform: (ctx, ct) => GetSemanticTargetForGeneration(ctx, ct))
            .Where(static m => m is not null);

        IncrementalValueProvider<(Compilation, ImmutableArray<ClassDeclarationSyntax>)> compilationAndClasses
        = context.CompilationProvider.Combine(classDeclarations.Collect());

        context.RegisterSourceOutput(compilationAndClasses,
            static (spc, source) => Execute(source.Item1, source.Item2, spc));

    }

    private static void Execute(Compilation compilation, ImmutableArray<ClassDeclarationSyntax> classes, SourceProductionContext context)
    {
    }

    private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context, CancellationToken ct)
    {
        string fullName = context.Attributes.First().AttributeClass.ToDisplayString();

        if (fullName == EventApplyAttribute)
        {
            if (context.TargetNode is not ClassDeclarationSyntax classDeclaration)
                return null;

            return classDeclaration;
        }

        return null;
    }
}

The classes array in the Execute method has the expected classes. If I loop through the array I get a lot of information. Is there a way to access also attribute property value. In this case it would be "BaseClass"? I can't find anything...


Solution

  • You could extract the attribute from the ClassDeclarationSyntax object using the AttributeList property, which contains the syntax of the attribute list. But that's tedious and very low-level.

    To make it easier and make the generator more efficient, you should do as @Youssef13 says and transform the value in GetSemanticTargetForGeneration. The GeneratorAttributeSyntaxContext already has the attribute:

    private static (ClassDeclarationSyntax, AttributeData) GetSemanticTargetForGeneration(GeneratorAttributeSyntaxContext context, CancellationToken ct)
    {
        if (!(context.TargetNode is ClassDeclarationSyntax classDeclaration))
            return (null, null);
    
        AttributeData attribute = context.Attributes
            .FirstOrDefault(a => a.AttributeClass.Name == "EventApplyAttribute");
    
        return (classDeclaration, attribute);
    }
    

    Now we're returning pairs of the class declaration and the attribute data. You can process that in the Execute method; I don't know what your goal is, so here I'm just generating a file containing a comment with the class name you're looking for, to show that it works:

    private static void Execute(Compilation compilation, ImmutableArray<(ClassDeclarationSyntax, AttributeData)> classes, SourceProductionContext context)
    {
        foreach (var (x, i) in classes.Select((x, i) => (x, i)))
        {
            TypedConstant aggregateParam = x.Item2.ConstructorArguments[0];
            
            if (aggregateParam.Kind == TypedConstantKind.Primitive &&
                aggregateParam.Value is string value)
            {
                context.AddSource(
                    $"generated_{i}.g.cs",
                    $"// <auto-generated/> using System; // Found param value '{value}'");
            }
        }
    }
    

    For completeness, you need to change the main Initialize method thusly:

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        IncrementalValuesProvider<(ClassDeclarationSyntax, AttributeData)> classDeclarations = context.SyntaxProvider
            .ForAttributeWithMetadataName(
                EventApplyAttribute,
                predicate: static (node, _) => node is ClassDeclarationSyntax,
                transform: static (ctx, ct) => GetSemanticTargetForGeneration(ctx, ct))
            .Where(m => m.Item1 is not null && m.Item2 is not null);
    
        IncrementalValueProvider<(Compilation, ImmutableArray<(ClassDeclarationSyntax, AttributeData)>)> compilationAndClasses
        = context.CompilationProvider.Combine(classDeclarations.Collect());
    
        context.RegisterSourceOutput(compilationAndClasses,
            static (spc, source) => Execute(source.Item1, source.Item2, spc));
    }