news 2026/9/18 12:34:21

TypeSpec 诊断 API 全解析:在自定义库中声明、报告与收集错误和警告

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeSpec 诊断 API 全解析:在自定义库中声明、报告与收集错误和警告

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对象,通过ProgramreportDiagnostic通道对外输出,并在 CLI、IDE(VSCode 扩展、tsp-server)等不同前端中得到一致的展示与抑制(suppress)能力。

诊断 API 的核心入口是createTypeSpecLibrary,它位于 packages/compiler/src/core/library.ts。当你的库通过该函数声明了diagnostics映射后,编译期即获得了完整的类型检查(as const+ 泛型约束),并自动生成reportDiagnosticcreateDiagnostic两个工具函数。

最佳实践:用诊断 API 而非 throw

官方文档明确给出两条黄金准则:

  • 避免用throw报告错误。在 TypeSpec 中抛出的任何异常都会被用户视为你库中的 bug(compiler 内部会用compilerAssert兜底拦截并提示"compiler bug")。
  • 使用诊断 API 报告预期内的错误与警告
    • ✅ 在装饰器(decorator)、$onValidate$onEmit中直接调用reportDiagnostic
    • 不要在 accessor(被其他库或 emitter 复用的函数)中直接调用reportDiagnostic,而应返回诊断元组,交由调用方决定如何处理(详见下文"收集诊断")。

这一约定的背后逻辑是:decorator /$onValidate/$onEmit是 TypeSpec 语义检查与发射流程的"终点",此时产生的问题必然需要呈现给用户;而 accessor 可能被多次调用(例如被多个 emitter 复用),若每次都直接上报,会出现重复诊断

诊断的硬性要求

每个诊断必须满足以下约束(对应 packages/compiler/src/core/types.ts 中的Diagnostic接口):

要求说明
code必须提供。完整代码为库名/本地代码<lib-name>/<local-code>),例如@typespec/my-lib/no-array
severity必须提供,取值为errorwarningerror不能被抑制(suppress),只有warning和 lint 规则诊断才允许被#suppress指令屏蔽
消息必须至少有一条。以default作为messageId的消息会被当作默认消息;当调用方未指定messageId时自动选中
消息参数可选。通过paramMessage模板在消息中插值动态信息

createDiagnosticCreator(packages/compiler/src/core/diagnostic-creator.ts)的实现中,未声明的code或未定义的messageId会直接抛出带提示的异常(列出所有已定义代码/消息),从而在开发期尽早暴露库定义与调用不一致的问题;最终生成的Diagnostic对象包含code(由库名自动拼接)、severitymessagetarget,并可选携带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-arraymy-lib/no-array
  • @typespec/http/duplicate-namehttp/duplicate-name

短名解析逻辑集中在 packages/compiler/src/core/diagnostic-code.ts 的getPackageShortName,规则如下:

包名模式短名
显式声明了alias使用alias(优先级最高)
@typespec/<name><name>(如@typespec/httphttp
@<scope>/typespec-<name><name>(如@azure-tools/typespec-client-generator-coreclient-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——仅含小写字母、数字和单连字符,不能有大写、下划线、空格,也不能以连字符开头/结尾或出现连续连字符(如tcgcmy-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 以"声明 → 报告 → 收集"为主线,配合短名/别名解析形成了完整的错误上报与抑制体系:

  1. 声明:在createTypeSpecLibrarydiagnostics映射中声明 code、severity、messages(默认消息用default,动态消息用paramMessage);
  2. 报告:在 decorator、$onValidate$onEmit中通过reportDiagnostic(program, { code, format, messageId, target })上报,error不可被抑制;
  3. 收集:在 accessor 中返回[T, Diagnostic[]]元组,借助createDiagnosticCollectoradd/pipe/wrap/join汇总诊断,交由调用方决定上报,避免重复诊断;
  4. 引用:诊断与 lint 规则同时支持完整名(@scope/pkg/code)与短名(pkg/code或自定义alias/code),别名需为 kebab-case,短名冲突时必须回退到完整名。

遵循这套约定,你的 TypeSpec 库就能与编译器、IDE、CLI 及所有官方工具链无缝协作,为用户提供定位精准、可抑制、可修复的高质量错误信息。

【免费下载链接】typespec项目地址: https://gitcode.com/GitHub_Trending/ty/typespec

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 12:34:21

Oh My Zsh redis-cli 插件:为 Redis 命令行客户端提供智能补全

Oh My Zsh redis-cli 插件&#xff1a;为 Redis 命令行客户端提供智能补全 【免费下载链接】ohmyzsh &#x1f643; A delightful community-driven (with 2,500 contributors) framework for managing your zsh configuration. Includes 300 optional plugins (rails, git, ma…

作者头像 李华
网站建设 2026/9/18 12:33:29

Kafka流式血缘追踪:物联网元数据治理与可视化

简介&#xff1a;面向物联网数据平台架构师、数据治理与 Kafka 开发运维人员的一份流式数据血缘追踪设计参考&#xff0c;聚焦高吞吐、分布式场景下血缘断链、元数据分散与链路难以追溯等痛点&#xff0c;适合具备一定 Kafka 基础的中高级读者系统研读。资源包仅含 1 个 PDF 文…

作者头像 李华
网站建设 2026/9/18 12:29:21

同一把 TaoToken Key,把 Sub-Agent 的 Checker 换到另一个模型

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华