使用属性为 类 生成代码

Generate code for classes with an attribute

我有以下设置:

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

之前,我打算使用 TT 来生成部分文件,但是为每种类型创建一个文件本身就很乏味且难以维护。

事情是...

有点迷茫

我确实做到了:

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

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

但是即使我们省略了...我如何列出给定 class 的所有方法及其参数?由于未加载程序集,反射显然不在画面中。

我只找到了 Rosly 示例,这些示例基于使用的解决方案或分析器实际上并没有可用的相同类型的对象,因此建议的解决方案不适用。而且我不确定对单个文件开始另一个 Roslyn 分析是否真的是应该完成的方式。

请记住,这是我第一次尝试 Semantic/Syntaxic API。

准备工作:Introduction to C# source generators

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

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

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

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

在我们新的Generator的Execute方法中,我们将能够使用Linq过滤出树:

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

然后我们就可以在我们的语法树上循环(据我所知,一个语法树大致对应一个文件)

Filter-out class没有用属性注释

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

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

这与上一步非常相似,tree 是我们在 treesWithClassWithAttributes collection.

中获得的项目之一

我们将再次循环 collection。

Filter-out class没有用我们的特定属性注释的那些

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

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

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

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

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

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

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

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

我们在最后一步结束时得到的是一个标识符令牌列表,这意味着我的属性只有两个:第一个标识属性本身,第二个标识 class我要获取名字.

我们将再次使用语义 API,这使我可以避免在所有语法树中查找我们确定的 class:

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

这给了我们一个 object 类似于我们到现在为止一直在操纵的那些。

这是开始生成我们新的 class 文件的好时机(所以一个新的 stringbuilder,所有测试都需要在与另一个相同的命名空间中有一个部分 class,在我的情况下它总是一样的,所以我去直接写了)

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

列出class

中使用的所有方法

所以现在,列出注释中的所有方法class。请记住,我们在这里循环 classes,来自我们的语法树。

要获取在此class中声明的所有方法的列表,我们将要求列出类型方法

的成员
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

剩下的只是构造一个代表我的目标部分的字符串 class,现在这是一件非常简单的事情。

最终解决方案

请记住这是我第一次尝试时想到的,我很可能会在 CodeReview StackExchange 上提交它以查看可以改进的地方。

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

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

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

在生成器的 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")));

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

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