TypeSpec 诊断 API 全解析:在自定义库中声明、报告与收集错误和警告
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
TypeSpec 编译器通过一套统一的**诊断 API(Diagnostic API)**来报告规范(specification)中的错误(error)与警告(warning),任何自定义库(library)、装饰器、校验钩子($onValidate)和发射器(emitter)都应通过这套 API 向用户呈现问题。本文基于 extending-typespec/diagnostics.md 文档,结合@typespec/compiler的实际源码实现,完整讲解诊断的声明、报告、收集机制、短名与别名解析规则,以及真实库(如@typespec/http)中的落地写法,帮助你写出符合官方规范、用户体验良好的 TypeSpec 库。
诊断 API 在 TypeSpec 中的定位
TypeSpec 编译器将"发现规范中的问题"与"把问题呈现给用户"这两件事解耦:无论是语法错误、语义错误、库内业务规则校验失败,还是 emitter 输出过程中的异常状况,最终都会归一化为统一的Diagnostic对象,通过Program的reportDiagnostic通道对外输出,并在 CLI、IDE(VSCode 扩展、tsp-server)等不同前端中得到一致的展示与抑制(suppress)能力。
诊断 API 的核心入口是createTypeSpecLibrary,它位于 packages/compiler/src/core/library.ts。当你的库通过该函数声明了diagnostics映射后,编译期即获得了完整的类型检查(as const+ 泛型约束),并自动生成reportDiagnostic与createDiagnostic两个工具函数。
最佳实践:用诊断 API 而非 throw
官方文档明确给出两条黄金准则:
- ❌避免用
throw报告错误。在 TypeSpec 中抛出的任何异常都会被用户视为你库中的 bug(compiler 内部会用compilerAssert兜底拦截并提示"compiler bug")。 - ✅使用诊断 API 报告预期内的错误与警告:
- ✅ 在装饰器(decorator)、
$onValidate或$onEmit中直接调用reportDiagnostic; - ❌不要在 accessor(被其他库或 emitter 复用的函数)中直接调用
reportDiagnostic,而应返回诊断元组,交由调用方决定如何处理(详见下文"收集诊断")。
- ✅ 在装饰器(decorator)、
这一约定的背后逻辑是:decorator /$onValidate/$onEmit是 TypeSpec 语义检查与发射流程的"终点",此时产生的问题必然需要呈现给用户;而 accessor 可能被多次调用(例如被多个 emitter 复用),若每次都直接上报,会出现重复诊断。
诊断的硬性要求
每个诊断必须满足以下约束(对应 packages/compiler/src/core/types.ts 中的Diagnostic接口):
| 要求 | 说明 |
|---|---|
code | 必须提供。完整代码为库名/本地代码(<lib-name>/<local-code>),例如@typespec/my-lib/no-array |
severity | 必须提供,取值为error或warning。error不能被抑制(suppress),只有warning和 lint 规则诊断才允许被#suppress指令屏蔽 |
| 消息 | 必须至少有一条。以default作为messageId的消息会被当作默认消息;当调用方未指定messageId时自动选中 |
| 消息参数 | 可选。通过paramMessage模板在消息中插值动态信息 |
在createDiagnosticCreator(packages/compiler/src/core/diagnostic-creator.ts)的实现中,未声明的code或未定义的messageId会直接抛出带提示的异常(列出所有已定义代码/消息),从而在开发期尽早暴露库定义与调用不一致的问题;最终生成的Diagnostic对象包含code(由库名自动拼接)、severity、message、target,并可选携带url(诊断文档链接)与codefixes(代码修复)。
声明你将上报的诊断
所有诊断必须先在库定义中声明。以下为文档给出的完整示例(同时展示了固定消息、参数化消息、多消息三种形态):
import { createTypeSpecLibrary, paramMessage } from "@typespec/compiler"; // lib.js export const $lib = createTypeSpecLibrary({ name: "@typespec/my-lib", diagnostics: { // 固定消息的基础诊断 "no-array": { severity: "error", messages: { default: `Array is not allowed in my-lib models.`, }, }, // 参数化消息 "duplicate-route": { severity: "error", messages: { default: paramMessage`Route '${"path"}' is being referenced in 2 different operations.`, }, }, // 同一 code 下的多条消息,通过 messageId 区分 "duplicate-name": { severity: "warning", messages: { default: paramMessage`Duplicate type name: '${"value"}'.`, parameter: paramMessage`Duplicate parameter key: '${"value"}'.`, }, }, }, } as const); // 重新导出辅助函数,便于直接调用 export const { reportDiagnostic, createDiagnostic } = $lib;声明后,上述三个诊断的完整名称为:
@typespec/my-lib/no-array@typespec/my-lib/duplicate-route@typespec/my-lib/duplicate-name
paramMessage的实现位于 packages/compiler/src/core/param-message.ts:它是一个标签模板函数,收集模板中的键名(keys),并在调用时用传入的字典(dict)替换占位符;未提供的键会被跳过而非报错,因此消息模板对参数的容忍度较高。注意as const至关重要——它让 TypeScript 能从字面量中推断出完整的 code 与 messageId 联合类型,从而在调用reportDiagnostic时获得编译期校验。
真实库中的声明写法
@typespec/http是官方库中非常典型的例子(packages/http/src/lib.ts),可以看到多消息、warning 与 error 混合声明的真实形态:
export const $lib = createTypeSpecLibrary({ name: "@typespec/http", diagnostics: { "http-verb-duplicate": { severity: "error", messages: { default: paramMessage`HTTP verb already applied to ${"entityName"}`, }, }, // ... 省略中间诊断 "double-slash": { severity: "warning", messages: { default: paramMessage`Route will result in duplicate slashes as parameter '${"paramName"}' use path expansion and is prefixed with a /`, optionalUnset: paramMessage`Route will result in duplicate slashes when optional parameter '${"paramName"}' is not set.`, optionalSet: paramMessage`Route will result in duplicate slashes when optional parameter '${"paramName"}' is set.`, }, }, }, } as const);短名(short name)解析机制
完整诊断名(@typespec/my-lib/no-array)冗长且含@scope/前缀,不利于在#suppress指令或 lint 配置中书写。因此编译器支持去掉包作用域的短名:
@typespec/my-lib/no-array→my-lib/no-array@typespec/http/duplicate-name→http/duplicate-name
短名解析逻辑集中在 packages/compiler/src/core/diagnostic-code.ts 的getPackageShortName,规则如下:
| 包名模式 | 短名 |
|---|---|
显式声明了alias | 使用alias(优先级最高) |
@typespec/<name> | <name>(如@typespec/http→http) |
@<scope>/typespec-<name> | <name>(如@azure-tools/typespec-client-generator-core→client-generator-core) |
typespec-<name>(无 scope) | <name> |
| 其他形式 | 无短名,只能使用完整名 |
无论是完整名还是短名,在抑制(suppress)或配置 lint 规则时都会被接受,编译器内部通过createDiagnosticCodeResolver(packages/compiler/src/core/diagnostic-code.ts)统一解析回规范化的完整形式。
自定义别名(alias)
当自动剥离作用域得到的短名不够直观、或与其他库冲突时,库可以声明自定义alias:
export const $lib = createTypeSpecLibrary({ name: "@azure-tools/typespec-client-generator-core", alias: "tcgc", diagnostics: { /* ... */ }, } as const);此时诊断与 lint 规则即可写作tcgc/<code>(例如#suppress "tcgc/no-foo")。
alias的合法性由 diagnostic-code.ts 中的正则^[a-z0-9]+(?:-[a-z0-9]+)*$校验:只能是 kebab-case——仅含小写字母、数字和单连字符,不能有大写、下划线、空格,也不能以连字符开头/结尾或出现连续连字符(如tcgc、my-lib-2均合法)。若alias非法,createTypeSpecLibrary会通过compilerAssert直接抛错(见 library.ts)。
短名歧义处理
如果两个已加载库解析出相同的短名(无论是自动剥离还是显式 alias),该短名即被视为歧义(ambiguous):
- 引用该短名时会收到警告提示,并列出所有候选库的完整名;
- 对这几个冲突的库,必须使用完整名(
@typespec/xxx/...)进行抑制或配置。
createDiagnosticCodeResolver通过shortToNames映射收集同短名候选,并在getAmbiguousShortName中返回候选列表用于生成警告(diagnostic-code.ts)。
报告诊断:reportDiagnostic 的三种用法
在装饰器、$onValidate或$onEmit中,直接调用从$lib解构出的reportDiagnostic:
import { reportDiagnostic } from "./lib.js"; // 1) 固定消息:只需 code + target reportDiagnostic(program, { code: "no-array", target: diagnosticTarget, }); // 2) 参数化消息:通过 format 注入模板占位符 reportDiagnostic(program, { code: "duplicate-route", format: { path: "/foo" }, target: diagnosticTarget, }); // 3) 多消息:用 messageId 选择具体消息 reportDiagnostic(program, { code: "duplicate-name", messageId: "parameter", format: { value: "$select" }, target: diagnosticTarget, });关键字段说明:
program:当前编译的Program实例;code:库内声明的本地代码(不含库名前缀,库名前缀由 creator 自动拼接);messageId:默认"default",用于多消息诊断选择具体文案;format:供paramMessage模板插值使用的键值对;target:诊断定位目标,可以是语法节点、TypeSpec 类型实体、Sym等(见 types.ts 的DiagnosticTarget定义),用于在 IDE 中高亮具体位置。
reportDiagnostic的底层实现(diagnostic-creator.ts)只是createDiagnostic后再调用program.reportDiagnostic(diag);真正组装诊断(拼接完整 code、解析消息、附加url/codefixes)的逻辑在createDiagnostic中完成。
在@typespec/http中,$onValidate流程里就有真实调用案例,例如报告"未找到服务"警告(packages/http/src/operations.ts):
if (namespace.operations.size > 0 && locationContext.type === "project") { reportDiagnostic(program, { code: "no-service-found", format: { namespace: namespace.name }, target: namespace, }); }收集诊断:accessor 中的标准模式
当问题可能产生于一个可复用的 accessor(例如库内部的getRoutes()、getParameters()等被多次调用的函数)时,不要直接上报到 program,而是返回诊断元组[结果, 诊断列表],让调用方决定上报时机。这能避免 accessor 被多次调用时产生重复诊断。
元组类型在源码中定义为DiagnosticResult<T> = [T, readonly Diagnostic[]](types.ts)。
方式一:借助 createDiagnosticCollector
import { createDiagnosticCollector, createDiagnostic } from "@typespec/compiler"; function getRoutes(): [Route[], readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); diagnostics.add( createDiagnostic(program, { code: "no-array", target: diagnosticTarget, }), ); // pipe:把 getParameters() 返回元组中的诊断并入当前收集器,并取出其数据 const params = diagnostics.pipe(getParameters()); const routes = computeRoutes(params); return diagnostics.wrap(routes); }DiagnosticCollector提供四个方法(实现见 diagnostics.ts):
| 方法 | 作用 |
|---|---|
add(diagnostic) | 向收集器追加一条诊断 |
pipe(result) | 解包一个DiagnosticResult元组:合并其中的诊断,返回数据本身(用于串联调用链) |
wrap(value) | 将最终结果包成[value, diagnostics]元组返回 |
join(result) | 合并另一个元组的诊断,并返回合并后的元组 |
@typespec/http的 payload.ts 中就大量使用了diagnostics.pipe(getContentTypes(...))这种链式收集写法。
方式二:手动收集
不引入 collector,直接用数组手动管理:
import { createDiagnostic } from "@typespec/compiler"; function getRoutes(): [Route[], readonly Diagnostic[]] { const diagnostics: Diagnostic[] = []; diagnostics.push( createDiagnostic(program, { code: "no-array", target: diagnosticTarget, }), ); return [routes, diagnostics]; }配套工具
createDiagnostic:与reportDiagnostic同源,但只构造Diagnostic对象、不立即上报,专供收集模式使用;ignoreDiagnostics(result)(diagnostics.ts):在明确想忽略 accessor 附带诊断时,直接取出元组中的数据部分。
与 Linter 规则的联动
诊断体系与 lint 规则共用同一套声明与短名机制。库可以用createLinterRule声明规则,并在规则上下文(LinterRuleContext)中通过context.reportDiagnostic上报诊断(linter.ts)。以编译器内置规则unused-using为例(unused-using.rule.ts):
createLinterRule({ name: "unused-using", severity: "warning", description: "Linter rules for unused using statement.", messages: { default: paramMessage`'using ${"code"}' is declared but never used.`, }, create(context) { return { root: (program) => { program.resolver.getUnusedUsings().forEach((target) => { context.reportDiagnostic({ format: { code: getUsingName(target.name) }, target, codefixes: [removeUnusedCodeCodeFix(target)], }); }); }, }; }, });可以看到,规则消息同样支持paramMessage参数化,并可附带codefixes(代码修复)——这也是为什么规则和诊断都能用短名(如tcgc/no-foo)在#suppress或 lint 配置中被引用:两者共享createDiagnosticCodeResolver的解析管线。
小结
TypeSpec 的诊断 API 以"声明 → 报告 → 收集"为主线,配合短名/别名解析形成了完整的错误上报与抑制体系:
- 声明:在
createTypeSpecLibrary的diagnostics映射中声明 code、severity、messages(默认消息用default,动态消息用paramMessage); - 报告:在 decorator、
$onValidate、$onEmit中通过reportDiagnostic(program, { code, format, messageId, target })上报,error不可被抑制; - 收集:在 accessor 中返回
[T, Diagnostic[]]元组,借助createDiagnosticCollector的add/pipe/wrap/join汇总诊断,交由调用方决定上报,避免重复诊断; - 引用:诊断与 lint 规则同时支持完整名(
@scope/pkg/code)与短名(pkg/code或自定义alias/code),别名需为 kebab-case,短名冲突时必须回退到完整名。
遵循这套约定,你的 TypeSpec 库就能与编译器、IDE、CLI 及所有官方工具链无缝协作,为用户提供定位精准、可抑制、可修复的高质量错误信息。
【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考