编写注释
目的
本仓库中的注释由贡献者在数月或数年后阅读,当时他们已经没有你现在拥有的上下文。本技能定义了那个读者是谁、每种注释的用途,以及哪些模式是被禁止的。
范围边界
本技能管理 TypeScript/JavaScript 源码中面向贡献者的注释。它不适用于最终用户文档:
- 在
packages/astro/src/types/public/config.ts和packages/astro/src/core/errors/errors-data.ts中带有@docs标签的 JSDoc 块,会被外部的docgen工具抓取并发布到 Astro 文档网站。请遵循packages/astro/src/core/errors/README.md中的说明处理这些内容,并获取文档团队的审查——当types/public/**发生变更时,CI 会重新生成参考文档。 types/public/**中的其他 JSDoc 会通过编辑器 IntelliSense 向用户展示。请为构建网站的 Astro 用户编写,而不是为阅读源码的贡献者。
以下所有内容都是关于贡献者在 HEAD 分支上阅读的源码。
读者
请为这样一位 Astro 贡献者编写:他精通 TypeScript,但没有你的当前上下文:没有这场对话,没有该拉取请求,没有该 issue,没有差别视图。他只能看到 HEAD 分支上的仓库。
这直接导致两个后果:
- 绝不叙述变更历史。 像“现在”、“以前”、“不再”、“新方法”这样的词在 HEAD 分支上是没有意义的,因为那里只存在一种方法。请陈述代码如何工作,而不是它如何变成这样。(
@deprecated通知是例外——参见本代码库的约定——因为它描述了合约的未来,读者需要知道。) - 绝不面向审阅者。 论证你的更改是正确的(“这正确处理了 X”)的注释,属于 PR 描述,而不属于源码。注释必须永久地证明代码的合理性。
三种注释,三种不同的工作
| 种类 | 语法 | 工作 | 包含内容 |
|---|---|---|---|
| 文件/模块概览 | 文件顶部的 /** */ | 解释 | 模块存在的理由、它定义的概念和术语、各部分如何关联、设计原理 |
| 条目文档 | 声明正上方的 /** */ | 参考 | 合约:行为、参数、返回值、抛出错误、不变项。中立且基于事实 |
| 内联注释 | 代码体内的 // | 理由 | 仅限代码无法表达的内容:约束、变通方法(附 issue 链接)、非明显的耦合 |
不要混淆这些工作。实现细节不属于 /** */ 合约——请将它们作为 // 注释放在代码体内。合约不应分散在内联注释中——请将其放在声明上。
行为文档
当需要编写条目文档时,请为人类读者编写合约,而不是作为实现的翻译。这并不要求每个函数都写 JSDoc;名称、类型和结构本身应该能够承载直白的行为。
- 以通俗语言描述函数返回或完成什么。
- 使用短句或中等长度的句子,每个句子表达一个主要思想。
- 当调用者不需要时,避免使用内部 Astro 术语。如果必须使用专业术语,请在同一段落中解释它。
- 描述调用者可见的、可能令人惊讶的注意事项:后备行为、工作限制、歧义结果、重载顺序、副作用,以及返回
undefined、null、空结果或其他不确定值的条件。 - 当预期调用者能够区分或从错误中恢复时,记录抛出的错误。
- 除非调用者需要理解行为或安全使用 API,否则不要描述实现细节。
当行为依赖于签名无法清晰展示的关系时,添加一个示例。常见的 Astro 和 TypeScript 情况包括:
- 选择哪个重载;
- 参数如何映射到可选或剩余参数;
- 哪个公开的 Astro 入口点暴露了在其他地方实现或重新导出的符号;
- 对于歧义路由、不完整配置或缺失内容的后备行为;
- 其结果含义从类型上不明显的结果。
在代码块之前用散文引入示例。说明它演示什么以及预期的结果是什么。保持代码片段简短、自包含,并从 Astro 用户或拥有合约的内部调用者的角度编写。
模块文档应描述持久的概念、架构边界或设计原因。不要仅仅为了总结文件而列出各个导出项;这样的清单会随着符号的添加或重命名而变得过时。如果模块没有需要解释的持久概念,请使用简短的单行描述或完全不写概览。
删除测试
在编写任何注释之前,问自己:它是否陈述了读者无法从代码本身恢复的信息?
- 如果信息已经由名称、类型或结构承载,则不要编写注释。如果名称未能承载它,请改进名称。
- 需要注释的合法信息:不变项、理由、与别处代码的耦合、带有链接的变通方法、依赖项令人惊讶的行为、模块定义的专业术语。
稍后编辑时,相同的测试反向适用:不再通过测试的注释应该被删除,而不是任其腐化。
为变通方法链接 Issue
本代码库一致地将变通方法锚定到某个来源。任何解释变通方法、HACK、回归防护或依赖项令人惊讶的行为的注释,必须链接触发它的 GitHub issue 或 PR。该链接让未来的读者能够判断变通方法是否仍然需要。
// 处理推荐的 nanostores。根据我们的测试,只需要 @nanostores/preact!
// 完整解释和相关错误报告:https://github.com/withastro/astro/pull/3667
'@nanostores/preact',
没有链接的变通方法与错误无异。
禁止的模式
叙述下一行。 看到即删除:
// 递增代计数器
generation += 1;
变更历史叙述。 改写为现在时态的理由:
// 错误:我们现在从用户的根目录而不是我们的根目录解析 lightningcss。
// 正确:lightningcss 是可选的 peer 依赖,因此它会从用户的项目根目录解析。
面向审阅者的论证。 将论点移到 PR 中:
// 错误:这正确处理了来自错误报告的多编码路径。
// 正确:在 MAX_DECODE_ITERATIONS 之后仍然编码的路径会被拒绝,因此
// 中间件和路由在解码后的路径上不会产生分歧。
重复的 JSDoc。 重述声明名称的 /** */ 块什么也没说:
// 错误:
/** 编译样式。 */
function compileStyles(...)
// 正确:
/** 将 `css` 中相对的 `url()` 引用针对 `base` 重写,
* 保留绝对 URL 和 data URL 不变。 */
function compileStyles(...)
模糊的搪塞。 “某些情况”、“各种原因”、“处理边界情况”、“等等”——要么明确指出,要么删除该句子。
表情符号。 禁止出现在源码中,包括注释(仓库范围的政策)。
即兴节标题(// ----- helpers -----、// ==== TYPES ====)。本代码库没有 // #region 折叠约定;不要添加标题。如果文件长到需要节标题,那是需要拆分的信号,而非装饰。
本代码库的约定
JSDoc 标签。 @param name - description、@returns 和 @throws 声明合约。仅在签名本身存在歧义时,才使用大括号包裹类型(@returns {Promise<string>})。对于非明显的用法,使用 @example 加围栏 ```js 代码块。
交叉引用。 使用 {@link Symbol} / {@linkcode Symbol} 而不是裸名称,这样重命名会更新引用,并且编辑器可以跳转到目标。
@internal。 标记不属于公开表面的 API。这只是一个约定——这里没有 typedoc 或 api-extractor 来剥离它——因此它记录意图,但不替代访问修饰符。
@deprecated。 说明迁移方法,然后说明移除时间窗口:
/** @deprecated 请使用实例方法 `cookies.consume()` 代替。将在 Astro 7 中移除 */
说清楚使用什么代替,而不仅仅说该符号已废弃。这个将来时态的信息是读者需要的合约信息,不属于禁止的变更历史叙述。
TODO。 使用 // TODO: 记录延期工作;当有 issue 跟踪时,链接该 issue。对于需要打破兼容性窗口的工作,使用既定惯用语 // TODO: remove in Astro <N>。本代码库中没有 FIXME——请勿引入。
编辑现有代码
- 保留现有注释。如果你的更改改变了行为,请扩展或修正特定的措辞——永远不要用通用文本替换。删除来之不易的上下文比保留稍微过时的注释更糟糕。
- 当你的更改导致注释变假时,在同一个差异中修复它。过时的注释比没有注释更糟糕。
- 匹配周围的密度。记录详尽的模块需要在新增条目上保持相同的水平;不要给稀疏的模块批量添加注释。
完成前的自我检查
在完成任何涉及注释的任务后,仅重新阅读你的差异中的注释,与代码更改隔离开来:
- 每个注释是否都通过了删除测试?
- 是否有任何注释引用了对话、更改本身或审阅者?
- 每个变通方法是否都链接了它的 issue 或 PR?
- 没有访问差异的读者能否理解每个注释?
修复或删除未通过的内容。删除是默认操作;遗漏的注释比误导性的注释代价更低。
参考文献
- Diátaxis——支撑上述解释/参考/理由划分的框架。
packages/astro/src/core/errors/README.md——针对带有@docs标签的错误条目,这些是最终用户文档。- TSDoc——TypeScript 文档注释的标签参考。