0

背景

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

[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)
{
    ...
}

问题

到目前为止,我已经按照我想要的方式完成了所有工作,但是我对读取FieldType. LogMemberAttribute我可以看到是否LogMember存在,并且在调试器中,我可以看到它被设置为intor string,但在代码中我无法弄清楚如何将其拉出并使用它。

源代码 这是我的方法的(简化版本)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 和诸如此类的东西不是很好,所以似乎我可以做一些事情来给我想要的东西,但我只是不知道那是什么。

4

1 回答 1

3

这里有很多问题,但是:

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

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

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

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

var 目标 = 编译.SyntaxTrees

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

于 2021-09-29T21:30:33.863 回答