SourceGenerator之partial范式及测试

一、什么是partial范式

  • partial关键字允许将一个类或方法分散到多个文件中
  • 所以partial是代码生成的一个很好的抓手
  • 再配合Attribute特性,可以更准确定位需要生成代码的类或方法
  • 可以理解为静态的"依赖注入"
  • 对代码按规则自动补足,减少重复代码编写及其可能导致的失误
  • 还可以代替反射提高性能,AOT友好
  • 笔者称之为SourceGenerator的partial范式
  • 开源项目GenerateCore用于践行partial范式

二、Framework中有大量partial范式的应用

  • 正则、序列化、日志等SourceGenerator都是和partial配合使用的

1. 正则表达式的Case

复制代码
partial class Program
{

    [GeneratedRegex("abc|def", RegexOptions.IgnoreCase)]
    private static partial Regex AbcOrDefGeneratedRegex();
}

2. 正则表达式的生成结果

复制代码
partial class Program
{
    /// <remarks>
    /// Pattern:<br/>
    /// <code>abc|def</code><br/>
    /// Options:<br/>
    /// <code>RegexOptions.IgnoreCase</code><br/>
    /// Explanation:<br/>
    /// <code>
    /// ○ Match with 2 alternative expressions, atomically.<br/>
    ///     ○ Match a sequence of expressions.<br/>
    ///         ○ Match a character in the set [Aa].<br/>
    ///         ○ Match a character in the set [Bb].<br/>
    ///         ○ Match a character in the set [Cc].<br/>
    ///     ○ Match a sequence of expressions.<br/>
    ///         ○ Match a character in the set [Dd].<br/>
    ///         ○ Match a character in the set [Ee].<br/>
    ///         ○ Match a character in the set [Ff].<br/>
    /// </code>
    /// </remarks>
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Text.RegularExpressions.Generator", "8.0.14.7010")]
    private static partial global::System.Text.RegularExpressions.Regex AbcOrDefGeneratedRegex()
        => global::System.Text.RegularExpressions.Generated.AbcOrDefGeneratedRegex_0.Instance;
    // ...
}

三、partial范式的要素

1. Attribute

  • 通过Attribute特性来标记需要代码补足的位置
  • Attribute的命名最好与调用的SourceGenerator一致
  • 需要生成的类有相应的Attribute也可以增加可读性(有预期该类包含自动生成的代码)
  • 还可以通过Attribute给SourceGenerator传参数,SourceGenerator按参数调整生成代码
  • partial范式通过官方方法SyntaxValueProvider.ForAttributeWithMetadataName来标记定位

2. Filter

  • ISyntaxFilter是节点过滤接口
  • SyntaxFilter是默认实现,实现按节点类型和是否为partial来过滤
复制代码
interface ISyntaxFilter
{
    bool Match(SyntaxNode node, CancellationToken cancellation);
}
class SyntaxFilter(bool isPartial, params SyntaxKind[] kinds)
    : ISyntaxFilter;

3. GeneratorSource

  • GeneratorSource是转化源,是Transform的预处理结果
  • IGeneratorSource是转化源接口
  • GenerateFileName是生成文件名属性
  • Generate是生成代码方法
  • SyntaxGenerator是通过SyntaxTree生成代码的辅助类,是EasySyntax项目的功能
  • 查看介绍EasySyntax的文章
复制代码
interface IGeneratorSource
{
    string GenerateFileName { get; }
    SyntaxGenerator Generate();
}

4. Transform

  • Transform是对节点预处理
  • 把GeneratorAttributeSyntaxContext转化为需要的GeneratorSource类型
  • GeneratorAttributeSyntaxContext包含成员节点、节点符号、节点特性列表和SemanticModel
  • 如果不满足生成必要条件返回null会被自动过滤
  • ISyntaxTransform是转化接口
  • PassTransform是默认实现,直接返回官方对象
  • TSource一般实现接口IGeneratorSource
复制代码
interface IGeneratorTransform<TSource>
{
    TSource? Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellation);
}
class PassTransform : IGeneratorTransform<GeneratorAttributeSyntaxContext>
{
    public GeneratorAttributeSyntaxContext Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellation)
        => context;
}

5. Executor

  • IGeneratorExecutor是执行器接口
  • GeneratorExecutor是默认实现,一般可以直接使用
复制代码
interface IGeneratorExecutor<TSource>
{
    void Execute(SourceProductionContext context, TSource source);
}
class GeneratorExecutor<TSource> : IGeneratorExecutor<TSource>
    where TSource : IGeneratorSource
{
    public virtual void Execute(SourceProductionContext context, TSource source)
    {
        var cancellation = context.CancellationToken;
        if (cancellation.IsCancellationRequested)
            return;
        var builder = source.Generate();
        var code = builder.Build()
            .WithGenerated()
            .ToFullString();
        context.AddSource(source.GenerateFileName, code);
    }
}

6. 生成器基类ValuesGenerator

  • 通过ValuesGenerator简化代码生成器开发
  • 把业务逻辑都提取到TSource中
  • filter、transform和executor都会很简单
复制代码
class ValuesGenerator<TSource>(
    string attributeName, 
    ISyntaxFilter filter, 
    ISyntaxTransform<TSource> transform, 
    ISyntaxExecutor<TSource> executor);

四、通过ValuesGenerator实现代码生成器的Case

  • 定义类型HelloGenerator继承ValuesGenerator
  • 另外需要实现HelloGeneratorAttribute、HelloTransform和HelloSource

1. HelloGenerator代码非常简单

  • 含义是查找HelloGenerator标记
  • 查找含partial修饰的类
  • 使用HelloTransform转化HelloSource
  • 执行HelloSource生成代码
复制代码
class HelloGenerator()
    : ValuesGenerator<HelloSource>(
    "GenerateCoreTests.Hello.HelloGeneratorAttribute",
    new SyntaxFilter(true, SyntaxKind.ClassDeclaration),
    new HelloTransform(),
    new GeneratorExecutor<HelloSource>())
{
}

2. HelloGeneratorAttribute非常简单

复制代码
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
class HelloGeneratorAttribute : Attribute
{
}

3. HelloTransform非常简单

复制代码
class HelloTransform : IGeneratorTransform<HelloSource>
{
    public HelloSource? Transform(GeneratorAttributeSyntaxContext context, CancellationToken cancellation)
    {
        if (context.TargetNode is ClassDeclarationSyntax type && context.TargetSymbol is INamedTypeSymbol symbol)
            return new(type, symbol);
        return null;
    }
}

4. HelloSource是比较纯净的业务逻辑

复制代码
class HelloSource(ClassDeclarationSyntax type, INamedTypeSymbol symbol)
    : IGeneratorSource
{
    private readonly ClassDeclarationSyntax _type = type;
    private readonly INamedTypeSymbol _symbol = symbol;
    string GenerateFileName
        => $"{_symbol.ToDisplayString()}.Hello.g.cs";
    public SyntaxGenerator Generate()
    {
        var builder = SyntaxGenerator.Clone(_type);
        var method = GenerateMethod();
        builder.AddMember(method);
        return builder;
    }
    public static MethodDeclarationSyntax GenerateMethod()
    {
        var name = SyntaxFactory.IdentifierName("name");
        var expression = SyntaxGenerator.Interpolation()
            .Add("Hello: '")
            .Add(name)
            .Add("'")
            .Build();

        return SyntaxGenerator.VoidType.Method("SayHello", SyntaxGenerator.StringType.Parameter(name.Identifier))
            .Public()
            .Static()
            .ToBuilder()
            .Add(SyntaxFactory.IdentifierName("Console").Access("WriteLine").Invocation([expression]))
            .End();
    }
}

5. 测试代码如下

复制代码
namespace GenerateCoreTests.Hello;

[HelloGenerator]
public partial class HelloTests;

6. 生成代码如下

复制代码
// <auto-generated/>
namespace GenerateCoreTests.Hello;
partial class HelloTests
{
    public static void SayHello(string name)
    {
        Console.WriteLine($"Hello: '{name}'");
    }
}

五、GenerateCore更多功能

1. 分析编译符号

  • 有时需要分析现有代码才能更好的生成代码
  • 用SymbolTypeDescriptor和SymbolTypeBuilder分析代码
1.1 提取符号
  • 可以提取其中一种或多种符号
复制代码
SymbolTypeDescriptor GetDescriptor(Compilation compilation, INamedTypeSymbol symbol)
{
    // 提取字段、属性、构造函数、方法和运算符重载等信息
    var builder = new SymbolTypeBuilder()
        .WithField()
        .WithProperty()
        .WithConstructor()
        .WithOperator()
        .WithMethod();
    return builder.Build(compilation, symbol);
}
1.2 获取符号
  • 通过SymbolTypeDescriptor获取符号
  • 这样可以判断某个成员是否存在,用于生成排重或是否可以调用
  • 只有先配置了提取的符号类型才能获取到
  • 以下获取符号比直接分析成员简单且可读性好
复制代码
/// <summary>
/// 获取字段
/// </summary>
IFieldSymbol? GetField(string name);
/// <summary>
/// 获取属性
/// </summary>
IPropertySymbol? GetProperty(string name);
/// <summary>
/// 获取构造函数
/// </summary>
IMethodSymbol? GetConstructor(params INamedTypeSymbol[] parameterTypes)
/// <summary>
/// 获取方法
/// </summary>
IMethodSymbol? GetMethod(string name, bool isPartial, params INamedTypeSymbol[] parameterTypes);
/// <summary>
/// 获取方法
/// </summary>
IMethodSymbol? GetMethod(string name, params INamedTypeSymbol[] parameterTypes);
/// <summary>
/// 按返回类型获取方法
/// </summary>
IEnumerable<IMethodSymbol> GetMethodsByReturnType(INamedTypeSymbol returnType);
/// <summary>
/// 获取运算符重载
/// </summary>
IMethodSymbol? GetOperator(string name, params INamedTypeSymbol[] parameterTypes);
/// <summary>
/// 获取相等重载符
/// </summary>
IMethodSymbol? GetEqualOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取不等重载符
/// </summary>
IMethodSymbol? GetUnEqualOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取相加重载符
/// </summary>
IMethodSymbol? GetAddOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取相减重载符
/// </summary>
IMethodSymbol? GetSubtractOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取相乘重载符
/// </summary>
IMethodSymbol? GetMultiplyOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取相除重载符
/// </summary>
IMethodSymbol? GetDivideOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取求余重载符
/// </summary>
IMethodSymbol? GetModOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取逻辑与重载符
/// </summary>
IMethodSymbol? GetAndOperator(INamedTypeSymbol otherType);
/// <summary>
/// 获取逻辑或重载符
/// </summary>
IMethodSymbol? GetOrOperator(INamedTypeSymbol otherType);

2. SymbolAttributeHelper解析Attribute

复制代码
/// <summary>
/// 获取标记
/// </summary>
IEnumerable<AttributeData> GetAttributesByType(IEnumerable<AttributeData> attributes, INamedTypeSymbol attributeType);
/// <summary>
/// 获取标记
/// </summary>
IEnumerable<AttributeData> GetAttributesByType(ISymbol symbol, INamedTypeSymbol attributeType);
/// <summary>
/// 获取标记参数值
/// </summary>
TValue? GetArgumentValue<TValue>(ISymbol symbol, INamedTypeSymbol? attributeType, string name);
/// <summary>
/// 获取标记参数值
/// </summary>
TValue? GetArgumentValue<TValue>(ISymbol symbol, INamedTypeSymbol? attributeType, int index);
/// <summary>
/// 获取标记参数值
/// </summary>
TValue? GetArgumentValue<TValue>(AttributeData attribute, string name);
/// <summary>
/// 获取标记参数值
/// </summary>
TValue? GetArgumentValue<TValue>(AttributeData attribute, int index);

六、SourceGenerator测试

  • SourceGenerator的测试是有点麻烦

1. Debugger.Launch用于测试

  • 在需要测试的代码前增加Debugger.Launch()
  • vs生成代码会触发断点
  • 缺点是不能单元测试且多线程执行影响调试体验还容易导致vs崩溃

2. Microsoft.CodeAnalysis.Analyzer.Testing用于测试

  • 优点是官方支持、支持单元测试
  • 感兴趣的可以体验一下,笔者有点用不习惯

3. 开源项目SyntaxScripting测试SourceGenerator

  • SyntaxScripting非常简单,只有1个类SyntaxTreeScript
  • 用于解析、编译代码和执行SourceGenerator
  • 执行SourceGenerator是通过官方的GeneratorDriver实现的
  • 是本次重点推荐的组件
3.1 SyntaxScripting执行SourceGenerator的Case
  • Generate返回GeneratorDriver,再通过GetRunResult可以获取生成结果
  • GeneratedTrees为本次生成的表达式树,如果为空表示没有生成成功
  • Diagnostics返回编译信息,一般为出错信息
复制代码
var source = "partial class Greeting;";
var service = SyntaxTreeScript.Create()
    .Using("System");
var result = service.Generate<HelloGenerator>(source)
    .GetRunResult();
var tree = result.GeneratedTrees.FirstOrDefault();
Assert.NotNull(tree);
var diagnostics = result.Diagnostics;
Assert.Empty(diagnostics);
3.2 SyntaxScripting添加引用
  • 通过Reference增加该类的程序集为引用
复制代码
var service = SyntaxTreeScript.Create()
    .Reference<DateTime>();
3.3 Default
  • Default是默认实例,静态共享
  • 默认using System
  • 默认引用CurrentDomain的所有程序集
  • 易错点是当前引用通过AppDomain.CurrentDomain.GetAssemblies()不一定能获取到
  • 如果没有类被调用程序集可能就不会被加载
  • 也就是说即使用Default也可能需要添加引用
复制代码
var source = "partial class Greeting;";
var result = SyntaxScripting.Default.Generate<HelloGenerator>(source)
    .GetRunResult();
var tree = result.GeneratedTrees.FirstOrDefault();
Assert.NotNull(tree);
var diagnostics = result.Diagnostics;
Assert.Empty(diagnostics);
3.4 CreateDefault
  • 同Default一样using System并引用CurrentDomain的所有程序集
  • 不同之处是非共享,每次调用CreateDefault都是新实例
复制代码
var source = "partial class Greeting;";
var result = SyntaxScripting.CreateDefault()
    .Generate<HelloGenerator>(source)
    .GetRunResult();
var tree = result.GeneratedTrees.FirstOrDefault();
Assert.NotNull(tree);
var diagnostics = result.Diagnostics;
Assert.Empty(diagnostics);
3.5 new可以配置更多参数
复制代码
SyntaxTreeScript(
    CSharpParseOptions options, 
    string path, 
    List<UsingDirectiveSyntax> usings, 
    List<MetadataReference> references);

4. 直接测试转化源对象

  • 其他的逻辑都可以理解为配置
  • 转化源对象是真正的业务逻辑
  • 是更适合单元测试的对象
4.1 测试代码如下
  • 代码虽然看着多,其实很简单
  • 而且绕过了SourceGenerator机制,直接执行业务逻辑
  • 这里还是用了SyntaxScripting组件,用来简化测试代码
  • 首先编译源代码code0
  • 其次从编译信息从提取type和symbol
  • 执行HelloSource
复制代码
var code0 = @"
namespace GenerateCoreTests.Hello;

[HelloGenerator]
partial class HelloTests;
";
var compilation = SyntaxTreeScript.Default
    .Compile(code0);
var syntaxTree = compilation.SyntaxTrees.FirstOrDefault();
Assert.NotNull(syntaxTree);
var type = syntaxTree.GetRoot()
    .DescendantNodes()
    .OfType<ClassDeclarationSyntax>()
    .FirstOrDefault();
Assert.NotNull(type);
var semanticModel = compilation.GetSemanticModel(syntaxTree);
var symbol = semanticModel.GetDeclaredSymbol(type);
Assert.NotNull(symbol);
HelloSource source = new(type, symbol);
var builder = source.Generate();
var code = builder.Build()
    .WithGenerated()
    .ToFullString();
Assert.Contains("SayHello", code);

七、总结

  • partial范式(GenerateCore+EasySyntax)简化SourceGenerator开发
  • partial范式支持把业务(GeneratorSource)独立拆分,增加代码可读性和可测试性
  • SyntaxScripting(+EasySyntax)支持对SourceGenerator执行和测试
相关推荐
玖釉-2 小时前
图形 API 的前沿试车场:Vulkan 扩展体系深度解析与引擎架构实践
c++·架构·图形渲染
玖釉-2 小时前
告别 Shared Memory 瓶颈:Vulkan Subgroup 架构解析与硬核实战指南
开发语言·c++·windows·图形渲染
吴梓穆2 小时前
UE5 C++ 两种枚举
开发语言·c++·ue5
星辰徐哥2 小时前
C++测试与调试:确保代码质量与稳定性
开发语言·c++
jghhh012 小时前
VC++ 屏幕锁定、关机、托盘工具源代码
开发语言·c++
邪修king2 小时前
【UE4/UE5 萌新向】有C++基础如何快速入门虚幻引擎?超详细图文全揭秘!
c++·ue5·ue4
Imxyk2 小时前
P9244 [蓝桥杯 2023 省 B] 子串简写
数据结构·c++·算法
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(四):线程ID及进程地址空间布局,线程封装
java·linux·运维·服务器·c语言·c++·学习
方便面不加香菜2 小时前
C++ 类和对象(二)
开发语言·c++