请记住,这是我第一次尝试使用语义/语法 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(
@" }
}");
}
}
}