Prettier check-ignore-pragma 与@noformat标记:文件级格式化豁免机制全解析
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
Prettier 从 v3.6.0 起提供了checkIgnorePragma选项,允许单个文件通过在文件头部书写@noprettier/@noformat注释来主动"拒绝"被格式化。本文以仓库测试目录tests/format/misc/check-ignore-pragma/中的 Markdown 测试用例为切入点,讲解该选项的配置方式、启用与禁用两种状态下的行为差异,并深入src/main/core.js、src/language-markdown/pragma.js等源码揭示 pragma 检测的完整调用链。读完本文,你将掌握"如何让某个文件永久豁免格式化"以及"为什么默认不开启此功能"的底层原理,并能自行在 Markdown、YAML、GraphQL、HTML 等语言中正确书写豁免标记。
从一个两行测试文件说起
关联文档 with-noformat-pragma.md 是 Prettier 测试套件中一个典型的"测试输入文件",全文只有两行有效内容:
<!-- @noformat --> I should be formatted !!它位于disabled/目录下,配合同目录的 format.test.js 使用:
runFormatTest(import.meta, ["markdown"], { checkIgnorePragma: false });checkIgnorePragma: false表示关闭pragma 忽略检查——这正是该选项的默认值。此时即使文件头部带有<!-- @noformat -->标记,Prettier 依然会照常格式化文件内容。该目录的快照文件 format.test.js.snap 明确记录了这一点:输入中的I should be formatted !!被输出为规范的I should be formatted !!,而<!-- @noformat -->注释本身则原样保留。
简而言之,这组测试验证的核心语义是:pragma 只在checkIgnorePragma开启时才生效;不开启时,@noformat注释只是普通注释,不产生任何豁免效果。
check-ignore-pragma 选项:配置方式与默认值
该选项在源码中的正式定义位于 src/main/core-options.evaluate.js:
checkIgnorePragma: { category: CATEGORY_SPECIAL, type: "boolean", default: false, description: "Check whether the file's first docblock comment contains '@noprettier' or '@noformat' to determine if it should be formatted.", cliCategory: CATEGORY_OTHER, },官方文档 docs/options.md 对其说明如下:
| 默认值 | CLI 覆盖 | API 覆盖 |
|---|---|---|
false | --check-ignore-pragma | checkIgnorePragma: <bool> |
三种典型使用方式:
# CLI:启用后,带豁免标记的文件将不被格式化 prettier --check-ignore-pragma --write "**/*.md" # API:Node.js 中通过选项对象传入 await prettier.format(source, { parser: "markdown", checkIgnorePragma: true }); # 配置文件:在 prettier.config.js 中全局开启 export default { checkIgnorePragma: true };需要特别强调的是,文档明确指出:"Checking for these markers incurs a small upfront cost during formatting, so it's not enabled by default."(检测这些标记在格式化前会带来少量额外开销,因此默认不启用)。这是该选项默认false的根本原因——每个文件在真正进入格式化流程之前都要先扫描一遍头部注释,属于额外的前置成本。
启用与禁用:快照测试揭示的行为对比
仓库在tests/format/misc/check-ignore-pragma/下同时维护了两套互为对照的测试:根目录下的 markdown/ 以checkIgnorePragma: true运行,disabled/markdown/ 则以checkIgnorePragma: false运行,两套测试的输入文件几乎完全一致,仅正文内容不同(启用套件用won't、禁用套件用should,以便快照结果更易辨识)。
对比两边的快照可以直观看到行为差异:
| 输入 | checkIgnorePragma: true输出 | checkIgnorePragma: false输出 |
|---|---|---|
<!-- @noformat -->+I won't format !! | 原样保留,I won't format !!未被整理 | 正常格式化为I should be formatted !! |
无 pragma 的I will format !! | 正常格式化为I will format !! | 正常格式化 |
关键结论:
- 开启时:文件首个有效位置存在
@noformat/@noprettier标记 → 整个文件跳过格式化,输出等于输入(见 启用态快照 中with-noformat-pragma.md的用例)。 - 未开启时:pragma 标记被忽略,文件照常格式化(见 禁用态快照)。
- 没有任何 pragma 的文件:无论选项开与关,行为完全一致,都正常格式化(对照
without-pragma.md的用例)。 - marker 注释本身始终保留:无论是否触发豁免,
<!-- @noformat -->这行注释在输出中都原样存在,不会被删除或重排。
底层原理:从选项到豁免的完整调用链
checkIgnorePragma并不是在文档打印(print)阶段起作用的,而是在格式化的最前端——src/main/core.js的formatWithCursor函数中完成短路判断:
async function hasPragma(text, options) { const selectedParser = await resolveParser(options); return !selectedParser.hasPragma || selectedParser.hasPragma(text); } async function hasIgnorePragma(text, options) { const selectedParser = await resolveParser(options); return selectedParser.hasIgnorePragma?.(text); } async function formatWithCursor(originalText, originalOptions) { // ... if ( (options.rangeStart >= options.rangeEnd && text !== "") || (options.requirePragma && !(await hasPragma(text, options))) || (options.checkIgnorePragma && (await hasIgnorePragma(text, options))) ) { return { formatted: originalText, // 直接返回原文,一步格式化都不做 cursorOffset: originalOptions.cursorOffset, comments: [], }; } // ... }(见 src/main/core.js)
这段代码揭示了三件事:
hasIgnorePragma通过resolveParser找到当前语言对应的解析器插件,然后调用插件暴露的hasIgnorePragma方法;如果解析器没有实现该方法,则视为不存在豁免标记。- 检测命中后,
formatted直接等于originalText,也就是说 Prettier连光标偏移处理、range 处理、BOM 处理之后的格式化流程都不会进入,真正做到"零改动跳过"。 - 该短路判断与
requirePragma(要求文件必须带@format/@prettier标记才格式化)并列,二者都是"文件级准入/豁免"策略,属于同一套设计哲学。
Markdown 专属实现:pragma.js 与 front matter 处理
不同语言对"文件第一个注释"的定义并不相同,因此每个语言插件都各自实现了hasIgnorePragma。Markdown 的实现位于 src/language-markdown/pragma.js:
const hasIgnorePragma = (text) => parseFrontMatter(text) .content.trimStart() .match(MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP)?.index === 0;这里有两个值得注意的设计:
- front matter 先行剥离:
parseFrontMatter(实现见 src/main/front-matter/parse.js)会先识别并剥离文件开头的 YAML / TOML front matter(以---或+++开头,支持...作为 YAML 结束分隔符),再把剩余的正文用于 pragma 匹配。这意味着 pragma 既可以直接写在文件第一行,也可以写在 front matter 块之后的第一行——仓库测试文件 front-matter-with-noformat-pragma.md 专门验证了这种场景。 - 匹配必须命中索引 0:
?.index === 0要求匹配到的 pragma 必须位于去空白后的正文最开头。也就是说,@noformat注释必须是"文件(或 front matter 之后)的第一个有效内容",位置稍有偏移便不生效。
而 Markdown 的匹配正则定义在 src/utilities/pragma/pragma.evaluate.js,它支持三种书写形式:
export const [MARKDOWN_HAS_PRAGMA_REGEXP, MARKDOWN_HAS_IGNORE_PRAGMA_REGEXP] = [ FORMAT_PRAGMAS, // ["format", "prettier"] FORMAT_IGNORE_PRAGMAS, // ["noformat", "noprettier"] ].map((pragmas) => { const pragma = `@(?:${pragmas.join("|")})`; return new RegExp( [ String.raw`<!--\s*${pragma}\s*-->`, String.raw`\{\s*\/\*\s*${pragma}\s*\*\/\s*\}`, `<!--.*\r?\n[\\s\\S]*(^|\n)[^\\S\n]*${pragma}[^\\S\n]*($|\n)[\\s\\S]*\n.*-->`, ].join("|"), "m", ); });对应的三种合法写法分别是:
<!-- @noformat -->{/* @noformat */}<!-- 一些说明文字 @noformat 更多说明文字 -->注意第三种形式是"多行注释",只要@noformat出现在注释内部任意一行的行首即可命中。因此,你可以把豁免标记和一段说明(例如"此文件为自动生成,请勿修改")合写在一个多行注释里,这也是官方文档示例(JS docblock)之外的、Markdown 语言专属的灵活用法。
其他语言中的豁免标记写法
@noprettier/@noformat并不是 Markdown 专有机制。在 src/utilities/pragma/pragma.evaluate.js 中,YAML、GraphQL、HTML 各有对应的正则,且同样共享noformat/noprettier这两个标记词:
- YAML(YAML_HAS_IGNORE_PRAGMA_REGEXP):文件头部以
# @noformat注释开头; - GraphQL(GRAPHQL_HAS_IGNORE_PRAGMA_REGEXP):
# @noformat注释; - HTML(HTML_HAS_IGNORE_PRAGMA_REGEXP):
<!-- @noformat -->注释; - Markdown:如上文所述的三种形式;
- JS / CSS 等其余语言:复用"首个 docblock/注释"的通用约定(
@noprettier/@noformat均可)。
仓库测试套件 check-ignore-pragma 为 css、graphql、html、js、json5、markdown、mdx、vue、yaml 等九种语言都准备了启用/禁用双向的用例,且每种语言都覆盖了noformat与noprettier两种标记以及"无标记"对照,是理解该功能跨语言行为的最佳参考。
与 requirePragma、insertPragma 的关系
checkIgnorePragma在 src/main/core-options.evaluate.js 中与requirePragma、insertPragma同属"pragma 家族",三者构成一套完整的渐进式采用方案:
| 选项 | 作用 | 典型场景 |
|---|---|---|
insertPragma | 在文件头部插入@format标记 | 团队逐步迁移:参与者格式化一批文件并打上"已格式化"烙印 |
requirePragma | 只格式化带@format/@prettier标记的文件 | CI 与自动化工具只处理已迁移文件 |
checkIgnorePragma | 跳过带@noformat/@noprettier标记的文件 | 让特定文件(如第三方生成物、含特殊格式的文档)永久豁免 |
从 src/main/core.js 的formatWithCursor可以看出,requirePragma与checkIgnorePragma的判断发生在同一处短路逻辑中,两者都是"按文件头部标记决定是否进入格式化";而insertPragma则在正常格式化前调用printer.insertPragma(text)注入@format标记。官方文档 docs/options.md 对insertPragma与requirePragma的关系有专门说明:二者不建议同时开启,同时开启时requirePragma优先级更高。
实践建议
- 豁免标记应写在整个文件的最前面(若存在 front matter,则紧随其后),因为实现要求匹配命中正文的索引 0;Markdown 中可写在
<!-- -->单行注释、{/* */}注释或包含说明文字的多行注释内。 - 不要依赖默认开启:
checkIgnorePragma默认false,即便文件中写了@noformat也不会生效。要么在 CLI / API / 配置中显式开启,要么改用<!-- prettier-ignore -->等行内忽略注释(该功能与 pragma 机制相互独立)。 - 豁免是文件级的:一旦命中 pragma,整个文件原样返回,无法做到"只豁免文件中的某一段"。如果只需要局部豁免,应使用行级/块级 ignore 注释。
- 适合自动生成文件或特殊排版文档:当某个文件由脚本生成、或包含必须保持原始空白的表格/ASCII 图时,在文件头标注
@noformat并配合 CI 中的--check-ignore-pragma,可以避免格式化工具反复改写这些内容。
延伸阅读(仓库内相关文件)
- 选项定义与官方说明:src/main/core-options.evaluate.js、docs/options.md
- 短路判断与调用链:src/main/core.js
- Markdown 实现与正则:src/language-markdown/pragma.js、src/utilities/pragma/pragma.evaluate.js
- front matter 剥离逻辑:src/main/front-matter/parse.js
- 跨语言测试套件:tests/format/misc/check-ignore-pragma/
【免费下载链接】prettierPrettier is an opinionated code formatter.项目地址: https://gitcode.com/gh_mirrors/pr/prettier
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考