Skip to content

扩展专题 4:诊断、SourceText 与通用语法驱动

这个专题把 3 类经常同时出现的扩展能力放在一起:

  • 生成器如何主动报诊断
  • 什么时候需要 SourceText.From(...)
  • 如果不靠特性筛选,什么时候该用 CreateSyntaxProvider(...)

本页里的路径都按“仓库根目录相对路径”书写。

1. 生成器如何主动报诊断

很多新手会先有一个误解:

  • 生成器只能“生成代码”
  • 不能像分析器那样给用户报问题

实际上完全可以。最短链路是:

text
DiagnosticDescriptor
  -> Diagnostic.Create(...)
  -> ReportDiagnostic(...)

样例 1:先定义诊断模板

源码路径:

  • sample/05-dto-mapper-generator/DtoMapperGenerator/Analyzers/DiagnosticDescriptors.cs

关键片段:

csharp
public static readonly DiagnosticDescriptor IncompatibleTypes = new(
    id: "DTOMAPPER001",
    title: "属性类型不兼容",
    messageFormat: "无法将属性 '{0}' 从类型 '{1}' 映射到 '{2}'",
    category: Category,
    defaultSeverity: DiagnosticSeverity.Error,
    isEnabledByDefault: true);

为什么看它:

  • DiagnosticDescriptor 不是一次具体报错,而是“报错模板”
  • 这里先把 id、标题、消息模板、严重级别统一定义好
  • 真正的大一点的生成器通常都会先把诊断定义集中管理,而不是写死在业务逻辑里

样例 2:把模板变成具体诊断实例

源码路径:

  • sample/05-dto-mapper-generator/DtoMapperGenerator/Analyzers/DiagnosticReporter.cs

关键片段:

csharp
public static Diagnostic ReportIncompatibleTypes(
    string propertyName,
    string sourceTypeName,
    string targetTypeName,
    Location location)
{
    return Diagnostic.Create(
        DiagnosticDescriptors.IncompatibleTypes,
        location,
        propertyName,
        sourceTypeName,
        targetTypeName);
}

为什么看它:

  • Diagnostic.Create(...) 才是“把模板实例化”的动作
  • 这里把 Location 和消息参数一起填进去,IDE 才知道要把提示挂到哪里
  • 把创建逻辑单独封装成 DiagnosticReporter,能避免生成器主流程里散落大量重复代码

样例 3:在真实生成流程里汇总并上报

源码路径:

  • sample/08-validator-generator/ValidatorGenerator/ValidatorGenerator.cs

关键片段 1:

csharp
context.RegisterSourceOutput(classesGroupedByNamespace, (spc, results) =>
{
    foreach (var result in results)
    {
        foreach (var diagnostic in result.Diagnostics)
        {
            spc.ReportDiagnostic(diagnostic);
        }
    }
});

关键片段 2:

csharp
var diagnostic = Diagnostic.Create(
    DiagnosticDescriptors.NonPartialClass,
    classDeclaration.Identifier.GetLocation(),
    classSymbol.Name);
diagnostics.Add(diagnostic);

为什么看它:

  • 这个 sample 不只是“定义诊断”,而是在语法筛选阶段顺便收集诊断
  • 然后在最终输出阶段统一 ReportDiagnostic(...)
  • 这比“发现一个问题就当场终止”更接近真实工程,因为它允许一次汇总多个问题

2. 什么时候用 SourceText.From(...)

两种写法本质都能生成代码:

csharp
context.AddSource("A.g.cs", code);
context.AddSource("B.g.cs", SourceText.From(code, Encoding.UTF8));

最实用的判断:

  • 简单生成:直接传 string 就可以
  • 想显式控制编码,或统一文本处理:用 SourceText.From(...)

样例 1:固定输出时显式指定编码

源码路径:

  • sample/02-builder-generator/BuilderGenerator/BuilderGenerator.cs

关键片段:

csharp
var attributeSource = @"#nullable enable

namespace BuilderGenerator
{
    public sealed class GenerateBuilderAttribute : System.Attribute
    {
    }
}
";

ctx.AddSource("GenerateBuilderAttribute.g.cs", SourceText.From(attributeSource, Encoding.UTF8));

为什么看它:

  • 这是最简单的一种 SourceText.From(...) 用法
  • 它适合放在“固定输出”场景里理解,因为你不会被复杂业务逻辑干扰
  • 如果你想先理解 SourceText.From(...) 本身,这个 sample 比 GraphQL 那种大文件生成更容易读

样例 2:批量生成模型和构建器时统一走 SourceText

源码路径:

  • sample/09-graphql-query-generator/GraphQLGenerator/GraphQLGenerator.cs

关键片段:

csharp
var code = generator.GenerateModel(type, rootNamespace, typeMapper, knownTypes, circularTypes);
context.AddSource($"{type.Name}.g.cs", SourceText.From(code, Encoding.UTF8));

以及:

csharp
context.AddSource("GraphQLQueries.g.cs", SourceText.From(code, Encoding.UTF8));

为什么看它:

  • 这里展示了 SourceText.From(...) 在真实工程里的常见位置:大批量输出、统一编码
  • 当生成内容来源已经不是手写模板,而是复杂拼装逻辑时,显式包一层 SourceText 更稳定
  • 这个 sample 适合你建立“SourceText 不是另一种生成器,只是更显式的输出载体”的认识

3. 如果不靠特性筛选目标

主案例一直走的是:

  • ForAttributeWithMetadataName(...)
  • GeneratorAttributeSyntaxContext

但继续看其他 sample 时,很快会遇到另一条路:

  • CreateSyntaxProvider(...)
  • GeneratorSyntaxContext

最短判断原则

你的入口是什么更适合哪个
明确由特性触发ForAttributeWithMetadataName(...)
由某种语法形状触发CreateSyntaxProvider(...)

样例 1:最简单的语法形状驱动

源码路径:

  • sample/03-enum-extensions-generator/EnumExtensionsGenerator/EnumExtensionsGenerator.cs

关键片段:

csharp
var enumDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
    predicate: IsEnumDeclaration,
    transform: TransformEnum
);

以及:

csharp
private static bool IsEnumDeclaration(SyntaxNode node, CancellationToken token)
{
    return node is EnumDeclarationSyntax;
}

为什么看它:

  • 这是 CreateSyntaxProvider(...) 最干净的一种样子
  • 入口条件非常明确:只要是 EnumDeclarationSyntax 就进来
  • 如果你刚从主案例切到非特性驱动,这个 sample 最适合做第一站

样例 2:先按语法筛,再用语义确认

源码路径:

  • sample/07-mvvm-observable-generator/MvvmObservableGenerator/ObservablePropertyGenerator.cs

关键片段:

csharp
var fieldDeclarations = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: static (node, _) => IsFieldWithAttribute(node),
        transform: static (ctx, _) => GetFieldSymbol(ctx))
    .Where(static field => field is not null);

以及:

csharp
var hasAttribute = fieldSymbol.GetAttributes()
    .Any(attr => attr.AttributeClass?.Name == "ObservablePropertyAttribute");

if (!hasAttribute)
{
    return null;
}

为什么看它:

  • 这个 sample 展示了更真实的两段式处理:
    • 语法阶段先找“像是目标”的字段声明
    • 语义阶段再确认它是否真的带目标特性
  • 这比直接检查 SyntaxNode 更接近实际项目,因为很多规则最终都要靠符号信息确认

样例 3:语法驱动 + 诊断收集一起做

源码路径:

  • sample/08-validator-generator/ValidatorGenerator/ValidatorGenerator.cs

关键片段:

csharp
var classDeclarations = context.SyntaxProvider
    .CreateSyntaxProvider(
        predicate: static (node, _) => IsCandidateClass(node),
        transform: static (ctx, _) => GetClassInfoWithDiagnostics(ctx))
    .Where(static result => result.ClassInfo != null || result.Diagnostics.Count > 0);

以及:

csharp
if (!isPartial)
{
    var diagnostic = Diagnostic.Create(
        DiagnosticDescriptors.NonPartialClass,
        classDeclaration.Identifier.GetLocation(),
        classSymbol.Name);
    diagnostics.Add(diagnostic);
    return (null, diagnostics);
}

为什么看它:

  • 这个 sample 说明 CreateSyntaxProvider(...) 不只是“挑目标”
  • 你还可以在 transform 阶段把“类信息 + 诊断信息”一起打包出来
  • 这比主案例更进一层,因为它把筛选、验证、诊断准备整合到了同一条增量链路里

4. 这三个主题为什么放在一起

因为它们都属于“主案例之后立刻会遇到,但不适合再塞回第 1-11 章正文”的能力:

  • 诊断:让生成器不仅能生成,还能解释失败原因
  • SourceText:让输出更显式、更可控
  • 通用语法驱动:让目标筛选不再依赖特性

5. 最推荐的配套文档

基于当前仓库文档副本构建的 VitePress 站点