Get attribute property value in C# source generation - IIncrementalGenerator

432 Views Asked by At

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...

2

There are 2 best solutions below

3
V0ldek On BEST ANSWER

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));
}
0
Youssef13 On

The GeneratorAttributeSyntaxContext type has an Attributes property that you can access. Each of the attributes will have a ConstructorArguments property which has what you want. You should extract this info in the transform method, into a value-equatable model.