Search code examples
c#roslynsourcegenerators

SourceGenerator: Attribute ConstructorArguments is empty


I am writing a source generator but am struggling to get the value of an argument passed to the constructor of my attribute.

I inject the following into the compilation:

namespace Richiban.Cmdr
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class CmdrMethod : System.Attribute
    {
        private readonly string _alias;

        public CmdrMethod(string alias)
        {
            _alias = alias;
        } 
    }
}

And then in my sample application I have the following:

public static class InnerContainerClass
{
    [CmdrMethod("test")]
    public static void AnotherMethod(Data data)
    {
        Console.WriteLine($"In {nameof(AnotherMethod)}, {new { data }}");
    }
}

This compiles without errors or warnings and I am successfully able to find all methods that have been decorated with my CmdrMethod attribute, but I am unable to get the value passed to the attribute because, for some reason, the ConstructorArguments property of the attribute is empty:

private static ImmutableArray<string> GetAttributeArguments(
            IMethodSymbol methodSymbol,
            string attributeName)
{
    var attr = methodSymbol
        .GetAttributes()
        .Single(a => a.AttributeClass?.Name == attributeName);

    var arguments = attr.ConstructorArguments;
    
    if (methodSymbol.Name == "AnotherMethod")
        Debugger.Launch();

    return arguments.Select(a => a.ToString()).ToImmutableArray();
}

See how the ConstructorArguments property is empty

Have I misunderstood this API? What am I doing wrong?


Solution

  • The Compilation is immutable. When you add the source code for your attribute, you still have a Compilation object that don't know anything about the attribute. This causes the AttributeClass to be an ErrorType as @jason-malinowski mentioned. This is very known for this kind of source generators, and the solution is just simple. Create a new Compilation with the symbol you injected, then get a SemanticModel from the new Compilation:

                // You should already have something similar to the following two lines.
                SourceText attributeSourceText = SourceText.From("CmdrMethod source code here", Encoding.UTF8);
    
                context.AddSource("CmdrMethod.g.cs" /* or whatever name you chose*/, attributeSourceText);
    
                // This is the fix.
                ParseOptions options = ((CSharpCompilation)context.Compilation).SyntaxTrees[0].Options;
                SyntaxTree attributeTree = CSharpSyntaxTree.ParseText(atttributeSourceText, (CSharpParseOptions)options);
                Compilation newCompilation = context.Compilation.AddSyntaxTrees(attributeTree);
                // Get the semantic model from 'newCompilation'. It should have the information you need.
    
    

    UPDATE: The experience became better starting from Microsoft.CodeAnalysis 3.9 packages, you can now add the attribute in Initialize instead of Execute:

            public void Initialize(GeneratorInitializationContext context)
            {
                // Register the attribute source
                context.RegisterForPostInitialization((i) => i.AddSource("CmdrMethod.g.cs", attributeText));
                // .....
            }
    

    Then, you can directly work with the compilation you get in Execute.

    See AutoNotify sample.