【问题标题】:Reading attribute properties from inside a source generator从源生成器中读取属性属性
【发布时间】:2021-09-29 20:19:55
【问题描述】:

背景

我正在尝试使用 C# 源代码生成器。这主要是一个实验项目,但目标是创建一个库,以帮助用户使用键值对格式编写日志。在编译时应该有一组预定义的公共键。在用户声明enum 并用特殊属性装饰它之前,库不会知道这些是什么。 enum 成员可以选择用一个附加属性来装饰,该属性声明Type 的键是什么(如果该属性不存在,我们假设它是一个字符串)。这是我正在使用的enums:

[AttributeUsage(AttributeTargets.Enum)]
public class LogKeysAttribute : Attribute { }

[System.AttributeUsage(System.AttributeTargets.Field)]
public class LogMemberAttribute : Attribute
{
    public Type FieldType { get; set; }
}

所以,作为一个例子,我们可能有这样的事情:

[LogKeys]
public enum CommonKeys
{
    [LogMember(FieldType = typeof(int))]    FirstAsInt     = 1,
    [LogMember(FieldType = typeof(string))] SecondAsString = 2,
    ThirdAsImpliedString = 5,
}

现在库要做的是创建一个像这样的新方法:

public void Log(int? firstAsInt=null, string secondAsString=null, string thirdAsImpliedString = null)
{
    ...
}

问题

到目前为止,我已经按照我想要的方式进行了几乎所有工作,但是我对在LogMemberAttribute 中读取FieldType 的值的部分感到困惑。我可以看到LogMember 是否存在,在调试器中,我可以看到它被设置为intstring,但在代码中我不知道如何将其拉出并制作使用它。

源代码 这是我的Execute 方法的(简化版本)(注意:我意识到这里有些东西不是最理想的。没关系——现在我们只担心从属性中读取):

public void Execute(GeneratorExecutionContext context)
{
    var compilation      = context.Compilation;

    //Declare our attributes that will be used
    //I had hoped adding it here would make the type available to use below, but no such luck
    var attributeDefinitionsSrc = @"
using System;
namespace LogWrapper
{
  [AttributeUsage(AttributeTargets.Enum)]
  public class LogKeysAttribute : Attribute { }

  [System.AttributeUsage(System.AttributeTargets.Field)]
  public class LogMemberAttribute : Attribute
  {
    public Type FieldType { get; set; }
  }
}";

    //Add the attributes to the compilation
    context.AddSource("CustomAttributes.cs", attributeDefinitionsSrc);
    var attribSyntaxTree = CSharpSyntaxTree.ParseText(attributeDefinitionsSrc, (CSharpParseOptions)context.ParseOptions);
    compilation = compilation.AddSyntaxTrees(attribSyntaxTree);

    //Now we can get those symbols to use later
    var keyAttribute  = compilation.GetTypeByMetadataName("LogWrapper.LogKeyAttribute");
    var memberTypeAttr = compilation.GetTypeByMetadataName("LogWrapper.LogMemberAttribute");

    //targets will be anything that is an Enum declaration
    var targets = compilation.SyntaxTrees
                                 .SelectMany(x => x.GetRoot()
                                                   .DescendantNodesAndSelf()
                                                   .OfType<EnumDeclarationSyntax>());
    foreach (var t in targets)
    {
//bonus points if you can explain to me what the "SemanticModel" is and how it differs from a syntax tree.
//I see you can view the syntax tree at sharplab.io. Is there a place/way to view the semantic model?
        var targetType = ModelExtensions
                           .GetDeclaredSymbol(compilation.GetSemanticModel(t.SyntaxTree), t);

        //We are only interested in the first enum that has our KeyAttribute associated
        if (targetType != null &&
            targetType.GetAttributes().Any(x => x.AttributeClass.Equals(keyAttribute)) &&
            targetType is ITypeSymbol its)
        {
            //Here we get all the enum members (FirstAsInt, SecondAsString, etc.)
            //They are sorted by their enum value (FirstAsInt is 1, so it goes first)
            var iSymbolList = its.GetMembers()
                              .Where(x=>x is IFieldSymbol ifs && ifs.HasConstantValue)
                              .OrderBy(x => ((IFieldSymbol)x).ConstantValue)
                              .ToList();

            //Next we iterate over all the members
            foreach (var sym in iSymbolList)
            {
                //If this member has a LogMember attribute associated with it,
                // then we get that attribute.
                var attrib = sym.GetAttributes()
                          .FirstOrDefault(x=>x.AttributeClass?.Equals(memberTypeAttr) == true);
                if (attrib != default)
                {
                    //There was a LogMember attribute. This means I want to read the
                    // value of FieldType so I know what Type it should be.

                    // **** THIS IS THE PART THAT I CAN'T DO *****

   //Debugger shows list of 1 with value "{[FieldType, {Microsoft.CodeAnalysis.TypedConstant}]}"
                    var namedArgsList = attrib.NamedArguments;

   //Debugger shows an empty list
                    var constArgsList = attrib.ConstructorArguments;

   //Debugger shows "{[FieldType, {Microsoft.CodeAnalysis.TypedConstant}]}"
                    var firstArg      = namedArgsList.FirstOrDefault(x => x.Key == "FieldType");

   //Debugger shows value "{int}" and it shows the type as:
   //"object { Microsoft.CodeAnalysis.CSharp.Symbols.PublicModel.NonErrorNamedTypeSymbol}"
   //It seems the data I want is here, but I can't get to it...
                    var valAsObj      = firstArg.Value.Value;

   //Debugger shows null. We can't cast to a Type.
                    var asType        = valAsObj as Type;
                    }
                    //else assume string (code removed)
                }

                //by this point we have all the data we need to generate the code
                break;
            }
        }

那么可以获取这些数据吗?恐怕我对 SyntaxTrees 之类的东西不是很好,所以似乎我可以做一些事情来给我想要的东西,但我只是不知道那是什么。

【问题讨论】:

  • 如果您阅读 Roslyn Github 源代码,您会看到 NonErrorNamedTypeSymbol 在任何时候都不会继承自 System.Type,因此您无法进行该转换。你可以使用Type.GetType(string) 来解析NonErrorNamedTypeSymbol.Name 属性中的类型吗?
  • 我确实尝试过解决这个问题,但我在这里遇到的一个问题是,对于第一个问题,它显示“int”并且Type.GetType("int") 不起作用(必须是@ 987654340@。同样,字符串必须是GetType("System.String")。我想我可以做一个 switch 语句或者说“如果你得到“int”,返回类型 int,如果你得到字符串,返回一个字符串......” ,但如果可能的话,我真的宁愿避免这种情况。@AndrewH
  • LogMemberAttribute可以定义什么类型有什么限制吗?它们都是原始类型吗?
  • 理想情况下,它可以是任何类型。在实践中,您建议的 hack 可能会在 10 次中至少工作 9 次。

标签: c# roslyn sourcegenerators


【解决方案1】:

这里有很多问题,但是:

//调试器显示为空。我们不能强制转换为类型。

这种情况下的值应该可以转换为 INamedTypeSymbol,这是 Roslyn 的类型概念。如果你想问“它是一个 int”,还有一个 ITypeSymbol 上的 .SpecialType 属性,它为你提供了一些常见情况的枚举。

如果您能向我解释什么是“SemanticModel”以及它与语法树有何不同,则可以加分。

语法树就是语法,就是这样。给我们一些文字,我们知道树是什么。语义模型让您可以在编译中询问有关树的问题,其中上下文是所有树加上引用。

var 目标 = 编译.SyntaxTrees

您需要查看ISyntaxReciever 以了解一种对我们的性能更友好的走树方式,因为我们可以更好地安排事情发生时的时间。

【讨论】:

  • 嗯,这不是我想要的,但它是迄今为止最好的解决方案,并且在大多数情况下都可以工作。我所做的是将var asType 更改为valAsObj as INamedTypeSymbol。现在我可以设置Type symbolType = Type.GetType($"{asType.ContainingNamespace.Name}.{asType.Name}"); 这似乎对大多数类型都很好,但是在尝试执行long? 之类的操作时确实会出现问题987654326@ 部分(尽管如果我这样做,它确实会出现asType.ToString()
猜你喜欢
  • 1970-01-01
  • 2013-08-09
  • 2023-03-13
  • 2023-01-30
  • 2015-11-19
  • 2017-12-31
  • 1970-01-01
  • 2016-09-08
  • 1970-01-01
相关资源
最近更新 更多