简体   繁体   中英

How to completely evaluate an attribute's parameters in a C# source generator?

In a source generator, I've found an attribute on a class and resolved its FQN with GeneratorSyntaxContext.SemanticModel to, eg, deal with its name being written with or without "Attribute" in it. How can I resolve the arguments? Basically I want to handle all of these:

// class MyAttribute : Attribute
// {
//   public MyAttribute(int first = 1, int second = 2, int third = 3) {...}
//   string Property {get;set;}
// }

[My]
[MyAttribute(1)]
[My(second: 8 + 1)]
[My(third: 9, first: 9)]
[My(1, second: 9)]
[My(Property = "Bl" + "ah")] // Extra, I can live without this but it would be nice

Most code I could find, including official samples, just hardcode ArgumentList[0], [1], etc. and the attribute's name written in "short form". Getting the attribute object itself or an identical copy would be ideal (it's not injected by the source generator but ProjectReferenced "normally" so the type is available) but it might be beyond Roslyn so just evaluating the constants and figuring out which value goes where is enough.

You can collect necessary information using syntax notifications . Here is a detailed walk-through.

First, register the syntax receiver in your generator.

[Generator]
public sealed class MySourceGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not MySyntaxReceiver receiver)
        {
            return;
        }

        foreach (var attributeDefinition in receiver.AttributeDefinitions)
        {
            var usage = attributeDefinition.ToSource();
            // 'usage' contains a string with ready-to-use attribute call syntax,
            // same as in the original code. For more details see AttributeDefinition.

            // ... some attributeDefinition usage here
        }
    }
}

MySyntaxReceiver does not do much. It waits for the AttributeSyntax instance, then creates and passes the AttributeCollector visitor to the Accept() method. Finally, it updates a list of the collected attribute definitions.

internal class MySyntaxReceiver : ISyntaxReceiver
{
    public List<AttributeDefinition> AttributeDefinitions { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode node)
    {
        if (node is AttributeSyntax attributeSyntax)
        {
            var collector = new AttributeCollector("My", "MyAttribute");
            attributeSyntax.Accept(collector);
            AttributeDefinitions.AddRange(collector.AttributeDefinitions);
        }
    }
}

All actual work happens in the AttributeCollector class. It uses a list of the AttributeDefinition records to store all found metadata. For an example of using this metadata, see the AttributeDefinition.ToSource() method.

You can also evaluate the syntax.Expression property if needed. I didn't do it here.

internal class AttributeCollector : CSharpSyntaxVisitor
{
    private readonly HashSet<string> attributeNames;

    public List<AttributeDefinition> AttributeDefinitions { get; } = new();

    public AttributeCollector(params string[] attributeNames)
    {
        this.attributeNames = new HashSet<string>(attributeNames);
    }

    public override void VisitAttribute(AttributeSyntax node)
    {
        base.VisitAttribute(node);

        if (!attributeNames.Contains(node.Name.ToString()))
        {
            return;
        }

        var fieldArguments = new List<(string Name, object Value)>();
        var propertyArguments = new List<(string Name, object Value)>();

        var arguments = node.ArgumentList?.Arguments.ToArray() ?? Array.Empty<AttributeArgumentSyntax>();
        foreach (var syntax in arguments)
        {
            if (syntax.NameColon != null)
            {
                fieldArguments.Add((syntax.NameColon.Name.ToString(), syntax.Expression));
            }
            else if (syntax.NameEquals != null)
            {
                propertyArguments.Add((syntax.NameEquals.Name.ToString(), syntax.Expression));
            }
            else
            {
                fieldArguments.Add((string.Empty, syntax.Expression));
            }
        }

        AttributeDefinitions.Add(new AttributeDefinition
        {
            Name = node.Name.ToString(),
            FieldArguments = fieldArguments.ToArray(),
            PropertyArguments = propertyArguments.ToArray()
        });
    }
}

internal record AttributeDefinition
{
    public string Name { get; set; }
    public (string Name, object Value)[] FieldArguments { get; set; } = Array.Empty<(string Name, object Value)>();
    public (string Name, object Value)[] PropertyArguments { get; set; } = Array.Empty<(string Name, object Value)>();

    public string ToSource()
    {
        var definition = new StringBuilder(Name);
        if (!FieldArguments.Any() && !PropertyArguments.Any())
        {
            return definition.ToString();
        }

        return definition
            .Append("(")
            .Append(ArgumentsToString())
            .Append(")")
            .ToString();
    }

    private string ArgumentsToString()
    {
        var arguments = new StringBuilder();

        if (FieldArguments.Any())
        {
            arguments.Append(string.Join(", ", FieldArguments.Select(
                param => string.IsNullOrEmpty(param.Name)
                    ? $"{param.Value}"
                    : $"{param.Name}: {param.Value}")
            ));
        }

        if (PropertyArguments.Any())
        {
            arguments
                .Append(arguments.Length > 0 ? ", " : "")
                .Append(string.Join(", ", PropertyArguments.Select(
                    param => $"{param.Name} = {param.Value}")
                ));
        }

        return arguments.ToString();
    }
}

According to a friend of mine:

There doesn't seem to be any public API to get the resolved attribute object or a copy of it. The closest thing is SemanticModel.GetReferencedDeclaration which gives you the declaration the attribute is on but not the attribute itself. You can get the attribute's name from the declaration's Name property and the attribute's type from the Type property, so you could write some code like this:

var attribute = declaration.GetReferencedDeclaration().Type.GetGenericTypeArguments()[0];

var first = int.Parse(attribute.Name.Substring(0, 1));
var second = int.Parse(attribute.Name.Substring(1));
var third = int.Parse(attribute.Name.Substring(2));

Alternatively, you could use the Type.GetGenericTypeDefinition() method to get the type of the attribute and then use the Type.GetGenericArguments() method to get the type's arguments.

On the INamedTypeSymbol/member/whatever that has the attributes, you can call GetAttributes(), which gives you an array of AttributeDatas. That'll give you all the attributes for that, so you'll have to filter back to your attribute type. That AttributeData also gives you two properties:

  • NamedArguments which covers your Property =... syntax
  • ConstructorArguments which is the array of the arguments as being passed to the constructor. You can figure out which constructor it is looking at the AttributeConstructor property. If you want to say "give me the argument for the constructor parameter named foo", figure out the index of the argument in the constructor and then look at that same index in the ConstructorArguments collection.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM