如何在 C# 源代码生成器中完全评估属性的参数?

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

在源代码生成器中,我在 class 上找到了一个属性,并用 GeneratorSyntaxContext.SemanticModel 解析了它的 FQN,例如,处理它的名字是否带有“属性”它。我怎样才能解决争论?基本上我想处理所有这些:

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

我能找到的大多数代码,包括官方示例,只是硬编码 ArgumentList[0]、[1] 等,以及以“短格式”编写的属性名称。获取属性对象本身或相同的副本将是理想的(它不是由源生成器注入的,而是“正常”的 ProjectReferenced,因此类型可用)但它可能超出 Roslyn,因此只需评估常量并确定哪个值在哪里够了

您可以使用syntax notifications收集必要的信息。这是详细的演练。

首先,在您的生成器中注册语法接收器。

[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 作用不大。它等待 AttributeSyntax 实例,然后创建 AttributeCollector 访问者并将其传递给 Accept() 方法。最后,它更新收集的属性定义列表。

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);
        }
    }
}

所有实际工作都发生在 AttributeCollector class 中。它使用 AttributeDefinition 记录的列表来存储所有找到的元数据。有关使用此元数据的示例,请参阅 AttributeDefinition.ToSource() 方法。

如果需要,您还可以评估 syntax.Expression 属性。我这里没有做。

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();
    }
}

据我的一个朋友说:

似乎没有任何 public API 来获取已解析的属性对象或其副本。最接近的是 SemanticModel.GetReferencedDeclaration ,它为您提供属性所在的声明,而不是属性本身。您可以从声明的名称 属性 中获取属性的名称,从类型 属性 中获取属性的类型,因此您可以编写如下代码:

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));

或者,您可以使用 Type.GetGenericTypeDefinition() 方法获取属性的类型,然后使用 Type.GetGenericArguments() 方法获取类型的参数。

具有 属性的 INamedTypeSymbol/member/whatever 上,您可以调用 GetAttributes(),它会为您提供一个 AttributeData 数组。这将为您提供所有属性,因此您必须过滤回您的属性类型。该 AttributeData 还为您提供了两个属性:

  • 涵盖您的 Property = ... 语法的 NamedArguments
  • ConstructorArguments,这是传递给构造函数的参数数组。您可以找出它正在查看 AttributeConstructor 属性 的构造函数。如果您想说“给我名为 foo 的构造函数参数的参数”,找出构造函数中参数的索引,然后查看 ConstructorArguments 集合中的相同索引。