Terminal.Gui DocSnippetValidator 实战:让 AI 文档里的 C 代码块在 CI 中持续可编译
2026/9/23 14:58:05 网站建设 项目流程
  • UI组件
  • 跨平台
  • 桌面应用

【免费下载链接】Terminal.Gui

Cross Platform Terminal UI toolkit for .NET

项目地址:https://gitcode.com/gh_mirrors/te/Terminal.Gui
点击查看免费下载

面向 Terminal.Gui v2 仓库的文档代码片段校验器(DocSnippetValidator)是一套基于 Roslyn 的 C# 文档示例保鲜方案:它提取ai-v2-primer.md、Claude 任务文档等 AI Agent 文档中的每一个 ```csharp 代码块,并针对构建出的Terminal.Gui.dll进行真实编译,让 v1 API 残留、改名成员、无效构造函数等"示例腐烂"(example rot)在 CI 阶段直接失败,而不是误导 Agent 与用户。读完本文,你将掌握它的完整用法、两种编译模式、跳过机制、废弃 API 拦截策略与 CI 集成方式,并能把它复用到你自己的文档仓库中。

Terminal.Gui v2 是一次完全重写(见 ai-v2-primer.md 中的说明:"Terminal.Gui v2 is a complete rewrite"),旧版大量 API 已被替换或标记废弃。这意味着任何以 v1 知识写成的示例代码都可能"看起来合法、实际上无法编译"。为此,仓库在 Scripts/DocSnippetValidator 下提供了一套独立的校验工具,配合 .github/workflows/validate-doc-snippets.yml 在 CI 中持续执行。

工具定位:编译文档,而不是检查文档

DocSnippetValidator 的核心思路非常朴素但有效:把文档当作代码来编译。它遍历指定 Markdown 文件中的每一个csharp /cs 围栏代码块,将其送入 Roslyn(Microsoft.CodeAnalysis.CSharp)编译为真实的程序集,并以"是否存在编译错误"作为唯一判定标准。项目文件 DocSnippetValidator.csproj 中唯一的关键依赖正是:

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />

同时它把OutputType设为Exe,即它本身是一个命令行工具,入口逻辑在 Program.cs。

这个设计解决的是 AI Agent 协作场景下的特有痛点:Agent 训练数据与文档中残留的 v1 写法(如静态Application.Init())不会产生任何语法错误,但一旦被复制进真实项目就会编译失败。只有当示例代码"能真正编译通过"时,文档才是可信的。

快速开始:两条命令跑通校验

README 中给出的用法非常直接:先构建库,再运行校验器,把Terminal.Gui.dll的路径和要校验的文档列表作为参数传入:

dotnet build Terminal.Gui/Terminal.Gui.csproj -c Debug dotnet run --project Scripts/DocSnippetValidator -- \ Terminal.Gui/bin/Debug/net10.0/Terminal.Gui.dll \ ai-v2-primer.md .claude/tasks/build-app.md .claude/cookbook/common-patterns.md

命令行格式为:

DocSnippetValidator <path-to-Terminal.Gui.dll> <markdown-file>...

参数含义如下:

参数说明
<Terminal.Gui.dll>已构建的库路径。程序启动时会先做File.Exists检查,找不到则提示 "Library not found: ... — build Terminal.Gui first." 并以退出码 2 结束(见 Program.cs 第 16-23 行)
<markdown-file>...一个或多个待校验的 Markdown 文档路径;不存在的文件会计入失败数并输出 "File not found: ..."

校验结束后会打印汇总行:

Doc snippets: N compiled, M skipped, K failed.

退出码约定为:0表示全部通过,1表示存在编译失败的代码块,2表示命令行参数使用错误。这个退出码是 CI 能"让失败直接阻断流水线"的基础。

两种编译模式:完整单元与语句片段

文档中的代码块形态各异,有的包含完整的类型声明,有的只是孤立的语句(如X = Pos.Center ();)。SnippetCompiler.cs 的Compile方法通过解析语法树来自动区分这两种形态:

1. 完整单元(complete units)

若代码块包含类型声明或using指令,则将其视为独立编译单元,在块内容前自动拼接一组标准using(见下文),按原样编译。若块中还包含顶层语句(top-level statements),则以OutputKind.ConsoleApplication编译成可执行程序;否则以DynamicallyLinkedLibrary编译成库。

2. 语句片段(statement fragments)

若代码块既没有类型声明也没有using,则视为"片段"。编译器会把它包装进一个继承自Runnable<string?>的宿主类__SnippetHost的方法体内,使块中的语句可以直接引用下面这些已经"在作用域内"的公共字段:

class __SnippetHost : Runnable<string?> { IApplication app = null!; View view = null!; View otherView = null!; Button button = null!; Button loginButton = null!; TextField textField = null!; TextField usernameField = null!; ListView listView = null!; CheckBox checkbox = null!; Label label = null!; }

选择字段而非参数是有讲究的:源码注释明确指出,字段允许片段中的局部变量与字段同名(shadow),不会触发CS0136(局部变量与字段重名错误)。如果"包成方法体"编译失败,编译器会退而求其次,把片段当作类成员重新包装编译——这覆盖了那些"片段本身就是方法声明"的文档用例(见 SnippetCompiler.cs 第 85-99 行)。

标准 using 集合

无论哪种模式,代码块都会在开头拼接如下标准using,保证片段无需自行引入命名空间即可引用 v2 的主要类型(这正是 v2 去扁平化的体现——using Terminal.Gui;裸命名空间已不再存在,取而代之的是按功能拆分的子命名空间):

using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.IO; using System.Linq; using Terminal.Gui.App; using Terminal.Gui.Configuration; using Terminal.Gui.Drawing; using Terminal.Gui.Input; using Terminal.Gui.Text; using Terminal.Gui.ViewBase; using Terminal.Gui.Views;

缩进还原与错误行号

提取器 SnippetExtractor.cs 会记录围栏起始行(StartLine),并剥离围栏自身的缩进前缀,保证嵌套在列表或引用块中的代码块也能被正确还原。编译错误会换算回"片段内相对行号"输出(减去拼接的前缀行数),例如:

<file.md>(12): snippet does not compile: (snippet line 4) CS0246: The type or namespace name 'Toplevel' could not be found ...

如何让某个代码块跳过校验

并非所有代码块都应该通过编译。文档里常出现的反例(anti-pattern)示例是"故意写错"的,用于对比新旧 API。提取器内置了三类自动跳过标记(见 SnippetExtractor.cs 中的_wrongMarkers):

  • 块内包含// WRONG
  • 块内包含
  • 块内包含

另外,也可以在围栏的前两行内放置 HTML 注释<!-- snippet: ignore -->显式跳过某个块:

<!-- snippet: ignore --> ```csharp Application.Init (); // 故意展示的 v1 写法,不参与编译
这两种方式分别对应 [README.md](https://link.gitcode.com/i/19517b4f6b5120704a0e912b64f7e212) 中"Opting a block out"一节的两种途径,覆盖了"自动识别反例"与"人工显式豁免"两类需求。被跳过的块会计入汇总行的 `skipped` 计数。 ## 废弃 API 被视为失败:拦截 v1 腐烂的关键策略 这是整个工具最有价值的设计决策。默认情况下,Roslyn 对 `[Obsolete]` 成员的调用只产生 **警告**(`CS0618`/`CS0612`),代码依然能编译通过。但 DocSnippetValidator 在 `CompileSource` 中通过 `specificDiagnosticOptions` 把这两条警告**升级为错误**: ```csharp new ("CS0105", ReportDiagnostic.Suppress), // 消除标准 usings 拼接导致的重复 using 噪音 new ("CS0612", ReportDiagnostic.Error), // 废弃 API 使用 = 失败 new ("CS0618", ReportDiagnostic.Error),

同时,源码特意不使用blanket 的#pragma warning disable——因为那会把上述两条升级规则一并压制掉。这样做的直接后果是:即使某个废弃成员仍然以[Obsolete]垫片(shim)的形式存在于库中、语法上"能编译",任何用到它的文档示例也会失败。

以 v1 的静态Application.Init为例,它在 Terminal.Gui/App/Legacy/Application.Lifecycle.cs 中仍以[Obsolete("The legacy static Application object is going away. Use Application.Create() for new code.")]的形式保留(见该文件第 18 行),但其用法一旦出现在文档代码块中就会被判为失败。这确保了 "旧写法仍然能运行" 不等于 "旧写法可以写进文档"。

测试与 CI 集成:负向测试防止回归

工具的自身行为也受到回归保护。仓库提供了 testdata/obsolete-api.md 作为负向测试夹具:该文件使用废弃的 v1 API(Application.Init ();Application.Shutdown ();),且没有任何// WRONG标记,因此一个行为正确的校验器必须拒绝它、返回非零退出码。

CI 工作流 .github/workflows/validate-doc-snippets.yml 中的最后一步专门验证这一点:它故意对夹具运行校验器,若命令意外"成功"(退出码为 0),则输出::error::validator accepted an obsolete-API snippet并使整个 CI 失败;只有命令如预期地返回非零,才打印negative test passed。这样,"校验器不再对废弃 API 报错"这一回归本身也会被抓出来。

工作流触发与执行步骤

validate-doc-snippets.ymlpush(develop 分支)与pull_request时触发,且带有paths过滤,只有下列内容发生变化时才运行:

  • ai-v2-primer.md
  • .claude/tasks/build-app.md
  • .claude/cookbook/common-patterns.md
  • llms.txt
  • Terminal.Gui/**(库代码变化会破坏既有示例,因此也要触发)
  • Scripts/DocSnippetValidator/**(工具自身变化)
  • .github/workflows/validate-doc-snippets.yml(工作流变化)

执行流程共四步:检出代码(fetch-depth: 0,供 GitVersion.MsBuild 使用)→ 安装 .NET 10(dotnet-version: 10.x)→ 以 Debug 配置构建Terminal.Gui.csproj→ 对四个文档(含llms.txt)运行校验器。注意,CI 校验的文档集合比 README 示例多一个llms.txt,实际以工作流中的命令为准。

端到端调用链一览

综合 Program.cs 与各组件,一次完整校验的执行链路为:

Program.cs(参数校验,计数) └─ SnippetExtractor.Extract(mdPath) # 提取 ```csharp/```cs 块,识别 WRONG/ignore 标记 └─ SnippetCompiler.Compile(snippet) # 判定完整单元/片段,包装并编译 └─ CSharpCompilation(引用 TPA 全量程序集 + Terminal.Gui.dll) └─ 错误收集 → 输出 "file(line): snippet does not compile:" + 行号对齐的错误

其中编译器构造时会将运行时TRUSTED_PLATFORM_ASSEMBLIES中的全部框架程序集逐一声明为元数据引用,再追加传入的Terminal.Gui.dll(见 SnippetCompiler.cs 第 56-66 行),因此编译环境与正常 .NET 项目一致。所有编译均启用可空上下文(NullableContextOptions.Enable),并使用LanguageVersion.Preview解析语法。

结语与复用建议

DocSnippetValidator 给出了一个可迁移的通用范式:凡是面向 AI Agent 或外部用户的文档,都应该把示例代码纳入编译验证。本仓库通过"完整单元原样编译 + 语句片段注入宿主类"的双模式策略兼容了各种代码块形态,通过"WRONG 标记自动跳过 +snippet: ignore显式豁免"处理了反例示例,通过"废弃 API 升级为错误"拦截了最隐蔽的 v1 腐烂,最后用负向测试夹具锁住了工具自身的行为。若你维护自己的文档仓库,只需仿照 validate-doc-snippets.yml 将文档路径加入触发列表,即可为文档示例建立同样的持续保鲜机制。相关实现与测试均可在仓库中直接查阅:Program.cs、SnippetCompiler.cs、SnippetExtractor.cs 与 testdata/obsolete-api.md。

  • UI组件
  • 跨平台
  • 桌面应用

【免费下载链接】Terminal.Gui

Cross Platform Terminal UI toolkit for .NET

项目地址:https://gitcode.com/gh_mirrors/te/Terminal.Gui
点击查看免费下载

相关推荐

上一篇:2024-01-15 周一
下一篇:PDFarranger终极指南:三步学会免费PDF页面重排与合并

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询