Appearance
扩展专题 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. 最推荐的配套文档
- 术语解释:看 术语分册 2:语法与语义 和 术语分册 7:附加文件、配置、诊断与文本
- 常见问题:看 FAQ 分册 5:高级输入与扩展链路
- 返回主教程收口:看 第 12 章