【问题标题】:Generate code for classes with an attribute为具有属性的类生成代码
【发布时间】:2021-03-03 17:12:22
【问题描述】:

我有以下设置:

public class CustomAttribute : Attribute
{
   [...]
   public CustomAttribute(Type type)
   {
    [...]
   }
}

[Custom(typeof(Class2))]
public class Class1
{
    public void M1(Class2) {}
    public void M2(Class2) {}
}


public partial class Class2
{
[...]
}

我试图使用 .NET 5 中添加的新代码生成机制来实现的是在编译时,找到项目中引用我的生成器的每个类都被 Custom 属性注释,然后,创建一个部分其构造函数中包含具有相同名称和参数的方法的类型的类(它不会是相同的参数,只是为了简化一点)。

之前,我计划使用 TT 来生成部分文件,但为每种类型创建一个文件既乏味又难以维护。

事情是……

我有点失落。

我确实做到了:

  1. 创建一个生成器,确保它在生成时被调用并且它生成的代码是可用的(~一个hello world版本)
  2. 在编译上下文中找到我的属性符号(不确定我是否需要它,但我找到了)
  3. 找到了一种方法,通过依赖编译上下文中存在的语法树来识别由我的属性注释的类。

现在,虽然我不知道如何进一步进行,但语法树在同一级别具有我的属性的标识符节点和用作参数的类,这意味着如果我使用另一个属性,我担心它们都将处于同一级别(可能会使用顺序获取我的属性的标识符位置,然后获取下一个)。

但是即使我们省略了...我如何才能列出我拥有的给定类的所有方法及其参数?由于未加载程序集,因此反射显然超出了图片范围。

我只找到了 Rosly 示例,基于使用的解决方案或分析器,它们并没有真正具有相同类型的可用对象,因此建议的解决方案不适用。而且我不确定开始对单个文件进行另一次 Roslyn 分析是否真的应该这样做。

【问题讨论】:

  • 只是提醒关注这个问题的人,或者只是感兴趣,我终于设法实现了我的目标,但过程有点冗长,我需要一些时间来写一个体面的答案详细说明了我所做的并解释了它的工作原理。这可能不是一个完美的解决方案,所以我也会将其发布在 CodeReview StackExchange 上。

标签: c# code-generation .net-5


【解决方案1】:

请记住,这是我第一次尝试使用语义/语法 API。

准备工作:Introduction to C# source generators

这将指导您设置代码生成器项目。据我了解,工具正在使这部分自动化。

TL;DR 在这个答案的末尾会有完整的ExecuteMethod

过滤掉不包含用属性修饰的类的语法树

这是我们的第一步,我们只想使用由属性修饰的类,然后我们将确保它是我们感兴趣的类。这还具有过滤掉任何不包含类的源文件的次要好处(想想 AssemblyInfo.cs)

在我们新生成器的Execute 方法中,我们将能够使用Linq 过滤掉树:

var treesWithlassWithAttributes = context.Compilation.SyntaxTrees.Where(st => st.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>()
                    .Any(p => p.DescendantNodes().OfType<AttributeSyntax>().Any()));

然后我们将能够在我们的语法树上循环(据我所知,一棵语法树大致对应一个文件)

过滤掉没有用属性注释的类

下一步是确保在我们当前的语法树中,我们只处理由属性修饰的类(对于在一个文件中声明多个类的情况)。

var declaredClass = tree
                    .GetRoot()
                    .DescendantNodes()
                    .OfType<ClassDeclarationSyntax>()
                    .Where(cd => cd.DescendantNodes().OfType<AttributeSyntax>().Any())

这与上一步非常相似,tree 是我们在 treesWithClassWithAttributes 集合中获得的项目之一。

再一次,我们将循环该集合。

过滤掉没有用我们的特定属性注释的类

现在,我们正在处理单个类,我们可以深入研究并检查是否有任何属性是我们正在寻找的属性。这也是我们第一次需要语义API,因为属性标识符不是它的类名(PropertyAttribute,例如[Property]),而语义API可以让我们找到原始的类名无需我们猜测。

我们首先需要初始化我们的语义模型(这应该放在我们的顶层循环中):

var semanticModel = context.Compilation.GetSemanticModel(tree);

初始化后,我们开始搜索:

var nodes = declaredClass
                    .DescendantNodes()
                    .OfType<AttributeSyntax>()
                    .FirstOrDefault(a => a.DescendantTokens().Any(dt => dt.IsKind(SyntaxKind.IdentifierToken) && semanticModel.GetTypeInfo(dt.Parent).Type.Name == attributeSymbol.Name))
                    ?.DescendantTokens()
                    ?.Where(dt => dt.IsKind(SyntaxKind.IdentifierToken))
                    ?.ToList();

注意:attributeSymbol 是一个变量,包含我正在搜索的属性的Type

我们在这里所做的是,对于与我们的类声明相关的每个语法节点,我们只查看描述属性声明的那些。

然后我们取第一个(我的属性只能在一个类上放置一次),它的父节点是我的属性类型的 IdentifierToken(语义 API 不返回 Type 因此名称比较)。

对于接下来的步骤,我将需要 IdentifiersToken,所以如果我们找到我们的属性,我们将使用 Elvis 运算符来获取它们,否则我们将得到一个 null 结果,这将允许我们进入下一个迭代我们的循环。

获取用作我的属性参数的类类型

这是它真正针对我的用例的地方,但它是问题的一部分,所以无论如何我都会介绍它。

最后一步我们得到的是一个标识符标记列表,这意味着我们将只有两个用于我的属性:第一个标识属性本身,第二个标识我想要的类获取名称。

我们将再次使用语义 API,这样我就可以避免在所有语法树中查找我们识别的类:

var relatedClass = semanticModel.GetTypeInfo(nodes.Last().Parent);

这给了我们一个类似于我们之前操作的对象。

这是开始生成我们的新类文件的好点(所以一个新的字符串生成器,所有测试都需要在同一个命名空间中有一个部分类,另一个是,在我的情况下它总是一样的,所以我就直接去写了)

获取relatedClass中的类型名称 => relatedClass.Type.Name

列出类中使用的所有方法

现在,列出带注释的类中的所有方法。请记住,我们在这里循环类,来自我们的句法树。

为了获得在这个类中声明的所有方法的列表,我们将要求列出方法类型的成员

IEnumerable<MethodDeclarationSyntax> classMethod = declaredClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType<MethodDeclarationSyntax>()

我强烈建议转换为 MethodDeclarationSyntax 或分配给具有显式类型的变量,因为它作为基本类型存储,不会公开我们需要的所有属性。

一旦我们有了我们的方法,我们将再次循环它们。 以下是我的用例所需的几个属性:

methodDeclaration.Modifiers //public, static, etc...
methodDeclaration.Identifier // Quite obvious => the name
methodDeclaration.ParameterList //The list of the parameters, including type, name, default values

剩下的就是构造一个代表我的目标部分类的字符串,现在这很简单。

最终解决方案

请记住,这是我第一次尝试时提出的建议,我很可能会将其提交到 CodeReview StackExchange 以查看可以改进的地方。

RelatedModelaAttribute 基本上是我的问题中的CustomAttribute 类。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using SpeedifyCliWrapper.SourceGenerators.Annotations;
using System.Linq;
using System.Text;

namespace SpeedifyCliWrapper.SourceGenerators
{
    [Generator]
    class ModuleModelGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            var attributeSymbol = context.Compilation.GetTypeByMetadataName(typeof(RelatedModelAttribute).FullName);

            var classWithAttributes = context.Compilation.SyntaxTrees.Where(st => st.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>()
                    .Any(p => p.DescendantNodes().OfType<AttributeSyntax>().Any()));

            foreach (SyntaxTree tree in classWithAttributes)
            {
                var semanticModel = context.Compilation.GetSemanticModel(tree);
                
                foreach(var declaredClass in tree
                    .GetRoot()
                    .DescendantNodes()
                    .OfType<ClassDeclarationSyntax>()
                    .Where(cd => cd.DescendantNodes().OfType<AttributeSyntax>().Any()))
                {
                    var nodes = declaredClass
                    .DescendantNodes()
                    .OfType<AttributeSyntax>()
                    .FirstOrDefault(a => a.DescendantTokens().Any(dt => dt.IsKind(SyntaxKind.IdentifierToken) && semanticModel.GetTypeInfo(dt.Parent).Type.Name == attributeSymbol.Name))
                    ?.DescendantTokens()
                    ?.Where(dt => dt.IsKind(SyntaxKind.IdentifierToken))
                    ?.ToList();

                    if(nodes == null)
                    {
                        continue;
                    }

                    var relatedClass = semanticModel.GetTypeInfo(nodes.Last().Parent);

                    var generatedClass = this.GenerateClass(relatedClass);

                    foreach(MethodDeclarationSyntax classMethod in declaredClass.Members.Where(m => m.IsKind(SyntaxKind.MethodDeclaration)).OfType<MethodDeclarationSyntax>())
                    {
                        this.GenerateMethod(declaredClass.Identifier, relatedClass, classMethod, ref generatedClass);
                    }

                    this.CloseClass(generatedClass);

                    context.AddSource($"{declaredClass.Identifier}_{relatedClass.Type.Name}", SourceText.From(generatedClass.ToString(), Encoding.UTF8));
                }
            }
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            // Nothing to do here
        }

        private void GenerateMethod(SyntaxToken moduleName, TypeInfo relatedClass, MethodDeclarationSyntax methodDeclaration, ref StringBuilder builder)
        {
            var signature = $"{methodDeclaration.Modifiers} {relatedClass.Type.Name} {methodDeclaration.Identifier}(";

            var parameters = methodDeclaration.ParameterList.Parameters.Skip(1);

            signature += string.Join(", ", parameters.Select(p => p.ToString())) + ")";

            var methodCall = $"return this._wrapper.{moduleName}.{methodDeclaration.Identifier}(this, {string.Join(", ", parameters.Select(p => p.Identifier.ToString()))});";

            builder.AppendLine(@"
        " + signature + @"
        {
            " + methodCall + @"
        }");
        }

        private StringBuilder GenerateClass(TypeInfo relatedClass)
        {
            var sb = new StringBuilder();

            sb.Append(@"
using System;
using System.Collections.Generic;
using SpeedifyCliWrapper.Common;

namespace SpeedifyCliWrapper.ReturnTypes
{
    public partial class " + relatedClass.Type.Name);

            sb.Append(@"
    {");

            return sb;
        }
        private void CloseClass(StringBuilder generatedClass)
        {
            generatedClass.Append(
@"    }
}");
        }
    }
}

【讨论】:

【解决方案2】:

在生成器的 Execute 方法中,添加以下内容:

var classesWithAttribute = context.Compilation.SyntaxTrees
                .SelectMany(st => st.GetRoot()
                        .DescendantNodes()
                        .Where(n => n is ClassDeclarationSyntax)
                        .Select(n => n as ClassDeclarationSyntax)
                        .Where(r => r.AttributeLists
                            .SelectMany(al => al.Attributes)
                            .Any(a => a.Name.GetText().ToString() == "Foo")));

这基本上获取所有树的所有节点,过滤掉不是类声明的节点,并且对于每个类声明,查看它的任何属性是否与我们的自定义属性“Foo”匹配。

注意:如果您的属性名为 FooAttribute,那么您将查找 Foo,而不是 FooAttribute。

【讨论】:

    【解决方案3】:
    // In SourceGenerator
    public void Initialize(GeneratorInitializationContext context)
            {
    #if DEBUG
                if (!Debugger.IsAttached)
                {
                    //Debugger.Launch();
                }
    #endif 
                context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
            }
    
    public void Execute(GeneratorExecutionContext context)
    {
          MySyntaxReceiver syntaxReceiver = (MySyntaxReceiver)context.SyntaxReceiver;
    }
    
    class MySyntaxReceiver : ISyntaxReceiver
            {
                public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
                {
    // Note that the attribute name, is without the ending 'Attribute' e.g TestAttribute -> Test
                    if (syntaxNode is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0)
                    {
                        var syntaxAttributes = cds.AttributeLists.SelectMany(e => e.Attributes)
                            .Where(e => e.Name.NormalizeWhitespace().ToFullString() == "Test")
    
                        if (syntaxAttributes.Any())
                        {
                        // Do what you want with cds
                        }
                    }
                }
            }
    

    【讨论】:

      猜你喜欢
      • 2021-02-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-08-24
      • 1970-01-01
      • 1970-01-01
      • 2010-10-10
      • 2011-11-20
      相关资源
      最近更新 更多