先贴规则。
{
selector: "AssignmentExpression[left.property.name='innerHTML']",
message: "禁止使用 innerHTML",
}
就这么多。它能判定的,是"这一行里出现了 innerHTML 这四个字",不是"一个没消过毒的值被交给了 HTML 解析器"。这两句话在前一万个文件里恰好重合,所以它看着能用。
到下一个文件就分家。
el.innerHTML = sanitize(userInput); // 该放行,它拦
el.innerHTML = userInput; // 该拦,它拦
el.innerHTML = `<b>${count}</b>`; // 该放行,它拦
三行在 AST 里是同一个形状。上下文不在语法树里,上下文在数据流里。这条规则不看数据流,所以它只能猜。猜的办法是默认所有输入都脏。
猜得够久,就是那个数:一条这样的规则,违规里大概 65% 是 eslint-disable 注释,不是修复。不是三分之二的开发者觉得它多余——是三分之二的触发连一个字符的代码都没改,只在文件里留一行"这里有情况,别管"。剩下那三分之一里,藏着一两个真没消毒的。它们和前面六十几个长得一模一样。过 CI 的时候,审查者看到的是一片一样的红。审查深度是零。
一条 65% 是噪音的规则,已经不算规则了。它更像一个定时器,提醒你"记得把它关掉"。
规则作者当初为什么写成模式匹配?不是因为他懒。能被机器跑的东西,天生只能看语法,看不见语义。让谁写个能过 CI 的检查,也只能先写成这样。这是工具的边界。问题出在边界之后的那个动作——把管不住的东西写进消息里,然后假装它管住了。
一条管不住"消毒"的规则,消息里写着"禁止使用 innerHTML",就是在给读者一个假承诺:这里没有安全问题了。安全问题是换了个地方蹲着,蹲在每个人的脑子里。
到这一步,多数团队开始动它。最常见的动作是 error 改 warn,理由四个字,"减少噪音"。这个动作我不同意,也不打算两边都夸一遍。
降级不是在调严格度。error 的语义是:谁想让这次的代码合进去,谁就得去回答"这里为什么安全"。warn 的语义是:谁都不用回答,只要不点开那条黄线。判断权从写规则的人手里,散给了每个开发者当天下午的心情。更麻烦的是这里没有决策点。没有哪次评审的记录里写着"我们决定不再管 innerHTML 消毒了"。它是被稀释的,不是被决定的。被决定的错误能改回来。被稀释的错误,你连该改哪一行都不知道。
这也不叫权衡。权衡要两边都算过账。这个动作没算账,它把账本扔了。
缺的东西不是更严的模式匹配,是污点追踪。规则的形态应该长这样:
if (tainted(value) && !passed_sanitizer(value, node))
report(node, "innerHTML 收到未消毒的值");
要把这个写对,得跟数据流、跟 sanitizer 的函数签名、跟包装函数和别名。复杂度是实打实的。我不主张所有人都上,那是另一头的过度设计。
但有一件事便宜得多,我认为一直没人提:把约束写进消息里,别只留一个模式。
message:
"innerHTML 只接受已消毒或静态的字符串。" +
"值来自请求/存储时,先过 sanitizer——" +
"别用一个 eslint-disable 把它按下去。"
这行消息和"禁止使用 innerHTML"的差别,不在措辞好不好听。前者把判断还给了看代码的人——它说这里有个前提,你去确认。后者只说这个字不许出现,于是看代码的人只能三选一:删掉、换写法、禁用。三个选项没有一个在回答那个真问题。
规则消息就是我常说的那种注释。注释该写"为什么",不该写"这行做了什么"。message: "禁止使用 innerHTML" 正是"这行做了什么"型注释的 lint 版——它占着位置,什么信息都没给。
说句题外话。
八月底看到一篇讲 nuance 的帖子,大概说绝对化措辞让论证更脆,一个干净的反例就能把整座结构推倒。我一开始觉得这是写文章的事,跟 lint 没关系。后来发现是同一件事,只是反例的形状不同:文章的反例是一句话,规则的反例是一个 el.innerHTML = sanitize(x)。论证被反例推倒,丢的是脸;规则被反例推倒,丢的是信号。一条误报率六成以上的规则,训练出来的不是谨慎,是对红灯的免疫。免疫之后,那个真问题也一起被免疫掉了。
绝对化删掉的从来不是程度,是条件。"永远不要用 X"这句话的作者,脑子里多半是有条件的,只是他没写。于是读者拿到的是结论,不是判断。结论能被复制,判断不能。工程这活要的恰恰是后者。真要下断言,就把条件写出来。写不出条件,说明这事你还没想清楚,那就别用"永远"来冒充想清楚了。
这跟让人工智能"做成能上生产的"是同一个毛病。你以为你给了规格,其实你只给了一个词,然后指望对方和你共用一本从来没写出来的字典。他填的空和你想的空不一样,你说他幻觉。规则那边也一样:你写了个模式,指望读的人自己补上你没写的约束。他没补上,你说他不严谨。
上面这些我说得挺顺。我自己也降过级。
我给手头那个小存储引擎写过一条检查:网络读之后的返回码必须显式处理,不许吞掉。第一版扫的是"这行里有没有 != 0",跑起来命中一大半是关闭路径——close 之后的错误码本来就没意义。我当时的反应也是把它从阻塞改成提示,理由也是那四个字,"减少噪音"。改到第三遍才想明白:问题不在它严,在它没说清自己在管什么。最后动的是例外条件有没有写进去——shutdown 路径放行。规则逻辑一行没动,命中掉下来一大半。
规则不需要变松。它需要变得能说出自己的条件。
所以现在给自己定一条:写规则、或者写断言之前,先把它自己的条件写出来。写得出条件的,就写条件,别写"永远"。"总是"同理。
能被一行正则完整表达的那个判断,通常已经不是判断了。