对比正则表达式逐行匹配与 tree-sitter AST 指纹的页头图片。
博客 chevron_right 对源代码使用正则表达式是违法的

对源代码使用正则表达式是违法的

你用过的大多数克隆检测器——CPD、Simian、jscpd——本质上都是逐行匹配器。它们读取你的源代码,按行进行词法切分或哈希,然后找出匹配行的连续片段。这种方式有两个特点:它很快,而且它早于任何人写出快到不会成为瓶颈的解析器。tree-sitter 改变了第二个事实。Deslop 拒绝假装事实并非如此。

逐行匹配会漏掉什么

逐行匹配器无法越过以下这些:

  • 格式。 两个完全相同但格式不同的函数看起来像是不同的代码。
  • 重命名。 在一个方法中把 user 改成 customer 会破坏每一次匹配。
  • 重排。 交换两个相互独立的语句,在词法切分器的世界里产生零重叠。
  • 语法糖。 LINQ 对比 foreachasync/await 对比回调、列表推导式对比循环——全是同样的代码,对词法切分器而言却全都不可见。

你可以针对每一种情况单独打补丁。CPD 会归一化空白字符。jscpd 有模式开关。Simian 允许你配置什么算作一次匹配。每一个补丁都是一堆启发式规则,在下一个边界情况上又会失效。这套架构根本不支持做得更好。

tree-sitter 让我们能做什么

tree-sitter 解析器为仓库中的每个文件生成一棵 AST。从这棵树出发,我们可以:

  • 将标识符和字面量归一化为规范的占位符,使重命名坍缩到同一个指纹;
  • 独立地哈希子树,使一个方法的指纹无论它位于文件中的何处都保持稳定;
  • 在子树而非行上进行操作,使格式和空白字符变得无关紧要;
  • 输出能够在除语义改写之外的每一种源代码变换中存活下来的字节范围。

由此带来的流水线是线性的、确定性的、且开销低廉的。没有启发式规则。除语法之外没有任何按语言定制的特例。添加一种语言就是:实现 LanguageParser trait,固定语法版本,搞定。

为什么把"禁用正则表达式"写进规则手册

本仓库的 CLAUDE.md 把话说得很明白:对源代码使用正则表达式是被禁止的。 不是"避免",不是"优先用解析器"——而是违法。这条规则之所以存在,是因为对源代码使用正则表达式是一个滑坡。第一个正则用来处理一个解析器不易表达的小众情况。第二个用来修复第一个里的 bug。到第五个时,代码库就有了一层正则表达式遮盖着一层解析器,而没人能搞清楚哪一层先触发。

在 Deslop 中,tree-sitter 不是一个便利工具——它是整个根基。该工具检测的每一种克隆类型、它融合的每一个信号、它输出的每一个字节范围,都来自 AST。移除 tree-sitter 不会只是损失一个特性;它会让整个工具荡然无存。

这对你意味着什么

  • 重命名重构不会隐藏重复。 一个簇能在标识符重命名后存活,因为指纹是在归一化后的 AST 上计算的。
  • 格式变更不会制造误报。rustfmt 重新格式化一个文件,不会改变 Deslop 看到的内容。
  • 语言对等是真实的。 同一套指纹逻辑运行在 C#、Rust、Python、Dart 以及之后添加的每一种语言上。跨语言比较(在有意义的时候)使用同样的数学方法。

逐行匹配是 1990 年代针对早已不复存在的硬件所做的妥协。tree-sitter 就是那次升级。Deslop 把这次升级作为基线而非高级付费层级来交付。