如何在 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 集合中的相同索引。
在源代码生成器中,我在 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 集合中的相同索引。