这个项目里反复出现同一类缺陷。累计已经十几次。 它们的形状几乎一模一样:
同一个概念在两个地方各写了一份规则,然后两份漂移了。
要命的是它们的长相:
- ❌ 不发生异常
- ❌ 不报错
- ✅ 只是行为不对
赋值"成功"了,不报错,只是没效果。
程序"跑完"了,不报错,只是答案不对。
所以不能靠「跑起来没报错」判断正确性。 这句话写在 ADR 索引的第一条教训里,因为它是这个项目最贵的一课。
案例一:兜底分支把数据变成 0
早期解码失败时会「返回 0」兜底。于是:
$([1, [2, [3]]]) → [1, 0, [0]] 内层数据全丢,但不报错
$(["a,b", "c"]) → 被切成 3 个元素
根因:列表原先用 "," 拼接、str.split(",") 切分。
但内层列表编码后本身含逗号:
L:[N:2,L:[N:3]]
被外层一刀切开就成了残缺片段,正则匹配失败, 于是又落进那个「返回 0」的兜底分支—— 同一个兜底分支连犯两案。
修法是改用长度前缀编码,元素边界由类型自己界定, 不依赖任何分隔符。这就是 ADR-0003 的由来。
兜底分支如果不区分「合法的空值」和「我解析失败了」, 它就会静默地把数据变成 0。
案例二:报错文不对题
在函数外写 全局「0」,报的是:
这里需要一个缩进的语句块(if 后面必须跟缩进)
而实际错误跟 if 毫无关系。
这条信息是加 while/for/def/class
之前写的,后来语句种类变多了,文案没跟着改。
这类坑的恶劣之处:它会报错,看起来很正常,
但把你引向完全错误的方向。你按提示去查 if,
查了半天,问题在别处。
案例三:一条永远绿的测试
这个我要单独讲,因为它比 bug 本身更值得警惕。
test_keyword_sync.py 从第二版起就在核对
「8 份关键字表是否同步」——这个项目里有 8 个地方各存了一份关键字表
(解释器、IDLE 的语法高亮、自动补全、光标提示……),
加一个关键字要改 8 个地方,漏一个就静默失效。
所以这个测试很重要。但它只 print 比对结果,
从不返回非 0。
结果是:在总入口里,它永远显示 [OK]——
即使某份表真的少了一个关键字。
一个永远绿的测试,比没有测试更坏。 没有测试你知道自己没测;永远绿的测试让你以为自己测了。
同一个教训还适用于用例的期望值。ADR-0008 里有个
短路求值的用例,第一版写成「输出里有 0 就算过」——
那样即使短路完全失效也会通过,等于没测。
改成检查「副作用函数的输出没有出现」才真正起作用。
案例四:改名不报错,只是不生效
这个坑我栽了不止一次,而且每次都是同一个形状。
想改一个东西的行为,改了「看起来该改的那个名字」, 跑起来什么都没发生——没报错,只是没效果。 因为真正被读取的是另一个地方的同名副本。
我甚至在某次提交信息里写了:
修复:又一个「看起来该改的名字不是真正被读的那个」(第 9 次)
所以本项目的可靠做法只有两条:
- 读源码找出真正的读取点,不要猜哪个名字「应该」生效
- 写一个会失败的对照测试——测不出问题的测试等于没测
为什么专门记这些
不是为了说「我们修了 31 个 bug」。是为了指出: 它们几乎全是同一个形状。
看清形状之后,防御手段才有针对性—— 不是「更仔细一点」,而是结构上消除重复: 能用一份的就不要写两份,必须写两份的就用测试钉死。
完整清单见 wiki 的踩坑记录。