首页
/ Documentation

Documentation

2026-09-06 12:20:35作者:丁柯新Fawn

The following block does not have a language attribute and should be ignored:

This is a fenced block without attributes, it's invalid and it should be ignored.

The following block should be given a js extension on extraction:

console.log("js");

The following block should be given a ts extension on extraction:

console.log("ts");

The following block is inside a markdown blockquote:

console.log("blockquote");

The following example contains the ignore attribute and will be ignored:

const value: Invalid = "ignored";

The following example will trigger the type-checker to fail:

const a: string = 42;

六个代码块对应的规则如下(后文会给出源码依据):

1. **无语言属性的块**(第 5–7 行):无效,直接忽略,不产生任何虚拟文件;
2. **` ```js `**(第 11–14 行):提取为 JavaScript 虚拟文件,期望命名 `main.md#11-14.js`;
3. **` ```ts `**(第 17–20 行):提取为 TypeScript 虚拟文件,期望命名 `main.md#17-20.ts`;
4. **引用块内的 `> ```ts `**(第 23–26 行):引用块前缀(每行行首的 `> `)会被剥除后照常提取,期望命名 `main.md#23-26.ts`;
5. **` ```ts ignore `**(第 29–31 行):`ignore` 属性使其完全跳过——注意块内故意写了 `const value: Invalid = "ignored";` 这样的错误类型,若不跳过就会污染类型检查;
6. **带类型错误的 ` ```ts `**(第 35–38 行):用于验证类型检查失败时的错误输出。

## 提取管线源码:从 Markdown 到虚拟文件

上述规则的实现在 [cli/util/extract.rs](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files) 中。入口函数 [extract_doc_tests](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L36-L38) 的文档注释说明了它的职责:

> Extracts doc tests from a given file, transforms them into pseudo test files by wrapping the content of the doc tests in a `Deno.test` call.

内部流程可以概括为四步:

### 1. 找出所有围栏代码块

[extract_files_from_fenced_blocks](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L121-L147) 对 Markdown 文本逐行扫描,由 [extract_markdown_fenced_blocks](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L157-L227) 完成围栏(fence)的配对。关键行为有两条,都可直接对照 main.md 验证:

- **围栏至少 3 个反引号**:[parse_markdown_fence_opening](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L235-L248) 中 `tick_count < 3` 时返回 `None`;闭合围栏的反引号数量必须**不少于**开围栏([is_markdown_fence_closing](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L250-L255));
- **HTML 注释内的块被跳过**:扫描器维护 `in_html_comment` 状态([L173-L189](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L173-L189)),`<!-- ... -->` 内部的代码块不会被提取。文档中可以用 HTML 注释隐藏不想被测试的示例。

### 2. 根据属性决定媒体类型

每个块的属性串交给 [media_type_from_attributes](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L388-L412) 解析:

- 属性包含 `ignore` → 返回 `None`,该块被丢弃(对应 main.md 第 29–31 行的行为);
- 第一个属性是 `js` / `javascript` / `mjs` / `cjs` / `jsx` / `ts` / `typescript` / `mts` / `cts` / `tsx` 之一 → 映射到对应的 `MediaType`;
- 无属性或属性不认识 → `MediaType::Unknown`,随后在 [extract_file_from_block](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L430-L432) 中直接 `return None`。

这就解释了 main.md 第一个块"没有语言属性、应被忽略"的原因:它不是被显式丢弃,而是根本无法推断出媒体类型。

### 3. 剥除引用块前缀

main.md 中那个引用块(第 23–26 行)是这段逻辑里最精细的部分。提取时需要把每行行首的 `> `(甚至嵌套的 `> > `)去掉,同时**不能误伤代码中真实的 `>` 字符**(比如模板字符串、泛型比较)。源码的处理方式是:

- 打开围栏时,[strip_markdown_fence_prefix](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L257-L267) 先剥掉缩进和任意个 `>` 标记,记录 `is_markdown_blockquote` 标志;
- 处理正文时,只有当块被标记为引用块,才用 [strip_markdown_blockquote_marker](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L514-L533) 循环剥离行首的 `>` 标记;非引用块保持原样,避免破坏代码里的字面 `>`。相关注释在 [extract_file_from_block 内 L447-L464](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L447-L464) 中有明确说明。仓库里还有一个专门针对引用块的规格测试 [tests/specs/test/doc_blockquote/main.ts](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/tests/specs/test/doc_blockquote/main.ts?utm_source=gitcode_repo_files) 做补充覆盖。

### 4. 生成带行号的虚拟文件标识

提取出的每个块都会得到一个带行范围片段的 URI,格式为 `<原文件>#<起始行>-<结束行>`([extract_file_from_block L475-L494](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L475-L494)),再追加扩展名得到 `main.md#11-14.js` 这样的虚拟文件路径。媒体类型则通过 content-type 头携带([L499-L503](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L499-L503)),因为路径片段本身不用于推断媒体类型。这与 [main.out](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/tests/specs/test/markdown/main.out?utm_source=gitcode_repo_files) 中出现的 `Check [WILDCARD]main.md#11-14.js` 完全对应。

## 从提取到执行:Deno.test 包装与权限

提取只是第一步。[extract_doc_tests](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L36-L38) 与 [extract_snippet_files](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L49-L51) 的区别在于包装方式:前者会把每个提取出的代码块包进一个 `Deno.test` 调用(`WrapKind::DenoTest`),后者不包装。

[generate_pseudo_file](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L958) 及其上方的文档注释([L857-L957](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L857-L957))描述了伪文件生成的完整变换:

1. **注入基础文件的导出**:若 Markdown 所在模块(或同一文件的 TS 代码部分)有 `export` 声明,会用 `ExportCollector`([L603-L635](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L603-L635))收集具名导出与默认导出,自动为文档测试注入 `import` 语句——文档示例因此可以直接使用所在模块导出的 API,无需手写导入;
2. **避免重复标识符**:如果代码块自己已经 import 了同名符号,自动注入会被跳过(文档注释中的 Edge case 1);
3. **剥掉 `export` 关键字**:`export const x = ...` 不能出现在 `Deno.test` 回调内部,AST 变换会把 `ExportDecl` 降级为普通声明([Transform L1032-L1076](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L1032-L1076),对应文档注释 Edge case 2);
4. **包装成测试**:[wrap_in_deno_test](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L1171-L1253) 生成形如 `Deno.test("<虚拟文件 URI>", async () => { ... })` 的语句,测试名就是虚拟文件路径,这也是测试输出中能看到 `main.md#35-38.ts` 定位的来源。

此外还有一个与 main.md 无直接关系但属于同一管线的能力:**shebang 权限声明**。若代码块第一行是 `#!/usr/bin/env -S deno run --allow-read`,[parse_shebang](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L554-L586) 会用 Deno 自己的 CLI 参数解析器解析这些标志,并把权限转发为 `Deno.test` 的 `{ permissions: ... }` 选项([permissions_options_object L1263-L1277](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L1263-L1277):`--allow-all` 变为 `{ permissions: "inherit" }`,无权限标志变为 `{ permissions: "none" }`)。源码注释同时列出了已知限制:按文件名匹配 `deno`、作用域 `--deny-*=<path>` 与 `--ignore-*` 尚不支持——不可解析的 shebang 会令生成的测试主动抛出 `invalid doc test hashbang` 错误([wrap_in_deno_test L1182-L1203](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/cli/util/extract.rs?utm_source=gitcode_repo_files#L1182-L1203))。

## 预期输出:类型错误如何被定位

运行 `test --doc --allow-all main.md` 后,[main.out](https://gitcode.com/GitHub_Trending/de/deno/blob/e5575e2c0ac3c6ea5281d685bab4c33be8c8b38d/tests/specs/test/markdown/main.out?utm_source=gitcode_repo_files) 给出了完整快照:

```text
Check [WILDCARD]main.md#11-14.js
Check [WILDCARD]main.md#17-20.ts
Check [WILDCARD]main.md#23-26.ts
Check [WILDCARD]main.md#35-38.ts
TS2322 [ERROR]: Type 'number' is not assignable to type 'string'.
    const a: string = 42;
          ^
    at [WILDCARD]/main.md#35-38.ts:2:11

error: Type checking failed.

  info: The program failed type-checking, but it still might work correctly.
  hint: Re-run with --no-check to skip type-checking.

把它与 main.md 逐行对照,可以验证全部规则:

  • 恰好 4 个 Check 行——无属性块(第 5–7 行)与 ignore 块(第 29–31 行)都没有出现在类型检查里,否则第 30 行的 const value: Invalid 也会报未定义类型错误;
  • 四个虚拟文件的行范围(#11-14#17-20#23-26#35-38)与 main.md 中围栏的起止行完全一致,其中 #23-26 证明引用块内的代码同样参与检查;
  • 唯一的类型错误 TS2322 精确落在 main.md#35-38.ts:2:11,即第 6 个代码块第 2 行 const a: string = 42;42 处——虚拟文件行号从块内首行计 1,列号指向赋值右侧;
  • 退出码为 1(对应 test.jsonc"exitCode": 1),末尾给出 --no-check 的提示。

延伸:同一管线的其他入口

  • JSDoc 注释提取extract_files_from_source_comments 用正则从 JSDoc 块注释中挑出 ``` 围栏,与 Markdown 走同一个 extract_file_from_block,因此语言属性、ignore、引用块剥除、shebang 规则完全一致;
  • deno doc 对 Markdown 的支持:仓库另有 tests/specs/doc/markdown 规格(含 README.mdexpected.out),验证 deno doc 能从 Markdown 文件输出 API 文档;
  • JSR 发布物中的文档测试tests/specs/doc/markdown_jsr 验证从 JSR 包中提取并测试文档代码块的路径。

小结

main.md 为例,deno test --doc 的行为可以归纳为一条清晰的规则表:

代码块写法 行为 源码依据
无语言属性的 ``` 忽略(媒体类型 Unknown) extract.rs#L388-L412#L430-L432
```js / `
登录后查看全文
热门项目推荐
相关项目推荐