本层判断标准

删掉这条注释,读的人会不会少知道一件事?

答「不会」,就删掉它——它只是把代码又念了一遍。答「会」,却说不出少知道的是哪一件事,说明注释本身没说清,要重写,不是留着。

已经交给检查工具的

工具只检查文档注释在不在,检查不了写得对不对;公开接口缺文档,它能一次全找出来。

拦什么规则要不要开
公开的函数缺文档字符串Python D103(函数)、D101(类)、D100(模块)要开。加之前先跑一次 ruff check --select D . 数现有函数有多少,一次改不完就先只开 D103
公开成员缺 XML 注释C# CS1591,需要 <GenerateDocumentationFile>true</GenerateDocumentationFile>要开。打开之后,缺注释会变成警告
公开项缺文档注释Rust #![warn(missing_docs)] 写在 crate 根上要开。配合 -D warnings 让它变阻断
导出标识符缺注释Go revive 的 exported 规则要开,把严重度设成 warning,不要设 error,否则会挡住正在写的代码
注释周围有空行、格式不对排版工具不检查,Go 的 exported 会看是否紧贴声明不要为它单开工具

三条命令(并进 make check)

先看清现状再决定开哪几条,不要一次性把 D 全开——D 里还有一批管标点、管空行、管首字母大写的规则,它们和内容无关。

ruff check --select D103 .
golangci-lint run
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings

工具查不到的边界:测试函数、getter、私有辅助函数不要求文档注释,它们的名字已经说明了做什么。TS 与 Java 在现有配置下都不检查「注释在不在」,这一项只能靠评审看。

该写什么:反例对照

判据是这条注释说没说出代码本身说不出来的东西。做了什么、按什么顺序、用了什么类型,代码自己就能说。代码说不出来的是三样:为什么这样、约束是什么、失败时谁负责。

反例为什么无效改成什么
// 打开文件函数名和参数已经说了// 路径来自服务端配置。请求体里的路径不采用,避免读到其他目录。
// 增加计数纯复述删掉。计数有上限或并发要求,把那一条写下来
// 这里是特殊情况没说出特殊在哪,读者仍要读完代码// 首次写入时表还不存在,这里建表再插入。
// 参数 scope_id 是所属范围 id名字和类型已经说了// scope_id 从会话读取,不从请求参数读取。
// 这段实现很精妙,不要动无法验证,后来的改动没有判据写出它避开了哪个具体错误,以及改动后哪个测试会失败
// TODO 以后改成向量检索没有负责人和日期,等于不打算做// TODO(组员名, 2026-11-30): 换成向量检索。见 docs/decisions/0031-naming.md
// 下面是旧实现 后接十几行被注释掉的代码读的人要逐行判断它还能不能用整段删掉。历史在 git log 里,要用时 git log -p 找回来
写成这样的注释 注释要包含这五项:数据来源与信任边界(路径、scope_id、用户 id 从哪来);不变量(这个字段在本函数返回后一定非空);单位与时区;失败时谁处理;为什么不用另一种更直观的写法。
不要写成这样 复述下一行代码;把类型信息抄一遍;写「这里要小心」「性能敏感」却不说小心什么、哪个量级;把大段设计文档塞进函数体——那种内容属于 docs/,见文档。

工具查不到的边界:「不变量」这一条在算法与数学代码里放宽。快排的分区函数只写一句它保持什么性质,不必逐行解释交换逻辑。

文档注释模板

模板只解决一件事:在写之前先想清楚「约束」和「失败时」这两项。写不出约束就写「无」,空着说明没想过。

Python:紧贴 def 的三引号字符串

第一行写成完整的一句话,以句号结尾。约束与失败各占一段。

def visible(notes, scope_id):
    """返回属于这个所属范围的笔记。

    约束:scope_id 来自会话,不从请求参数读取。
    失败时:notes 为 None 时抛 TypeError,不做静默兜底。
    """
    return [n for n in notes if n["scope_id"] == scope_id]

Go:紧贴声明、以名字开头

中间不空行。第一行以函数名开头、以句号结尾,这样 go doc 输出才成句。

// Open 打开 path 指向的笔记文件。
//
// 约束:path 来自服务端配置,请求体里的路径不采用。
// 失败时:返回包装过的 error,调用方用 errors.Is 判断。
// 调用方负责关闭返回的文件。
func Open(path string) (*os.File, error) {
	return nil, nil
}

TypeScript:一行够用就写一行

约束多于一句时才展开成多行 JSDoc。eslint:recommended 不检查这段文字在不在。

/** 返回笔记标题,供列表显示。title 为空时返回空串。 */
export function label(note: { title: string }): string {
  return note.title;
}

工具查不到的边界:三行以上的模板在只有一种参数、一种返回的简单函数上会变成形式主义,写一行就够。但「约束」这一项只要存在就必须写——多数项目里出问题的恰好是这一项。

评审注释时改问一句话

评审不问「有没有注释」——助手会给每一行都配上,数量从来不是问题。改问「删掉它会不会少知道一件事」。

把本次新增的注释筛出来

只扫新增行。-U0 去掉上下文,避免把没改的老注释卷进来。

git diff main...HEAD -U0 \
  | grep -nE '^\+[[:space:]]*(//|/\*|\*|#|""")'

把没有负责人和日期的 TODO 筛出来

逐条确认每条 TODO 带没带姓名与日期。

git diff main...HEAD -U0 \
  | grep -nE '^\+.*(TODO|FIXME|XXX|HACK)'
可识别现象说明什么评审动作
新增注释数量远多于新增代码行数的一半多半是逐行复述要求删到只剩有信息量的那几条
注释与紧邻的代码对不上(注释说返回值非空,代码会返回 nil)注释过期了按代码对、注释错处理,要求同一次改动里改掉;不接受「下次一起改」
代码里出现 scope_id、路径、口令但没有来源说明信任边界没写下来要求补一句来源,并与数据边界的判定对照

工具查不到的边界:注释与代码对不上时,如果是代码本身错了,改代码,不动注释。这条不算例外,只是提醒不要在错代码上把注释改得「一致」。

助手交出来的样子

// 遍历笔记列表,把可见的追加到结果
for _, n := range notes {
    if n.Visible {
        result = append(result, n)
    }
}

// GetNote 获取笔记
// 参数:id 笔记 ID
// 返回:笔记与错误
func GetNote(id int64) (*Note, error)

认它的办法:删掉这条注释,读的人会不会少知道一件事。上面两条都答「不会」——第一条念的是循环体,第二条念的是签名,删掉它们,代码一行信息都没少。真正缺的那句恰恰没写:这个 id 要不要先经所属范围过滤、查不到时算错误还是算空结果。

必须死守的四条

信任边界的说明留在代码旁凡是 scope_id、文件路径、用户 id、口令、令牌出现的位置,紧邻处要写一句它从哪来。判据:跑 grep -rn 'scope_id' --include='*.go' .,每一处附近都有来源说明。违反后果:下一个人或下一次助手会照着请求参数写,数据边界立刻被绕开。
注释与代码不一致,在同一次改动里改掉判据:评审时抽三条注释对照实现。违反后果:读的人开始不信注释,全部退回读实现,注释反而增加成本。
不许留被注释掉的代码判据:跑 git diff main...HEAD -U0 | grep -nE '^\+[[:space:]]*(//|#)[[:space:]]*(if|for|func|def|return|import)',输出应为空。违反后果:读的人要逐行判断它还能不能用,而 git log -p 本来就能找回来。
每条 TODO 写姓名与日期写成 TODO(姓名, YYYY-MM-DD):。判据:上面那条 grep 的输出里每一条都符合格式。违反后果:没人认领,到期也不会有人发现。

工具查不到的边界:第三条在有生成代码的仓库里放宽——生成器产出的文件本来就不该手改,把那类文件整目录排除在评审范围之外,而不是靠注释说明。生成文件的路径写进 .gitattributes 并标注 linguist-generated。