大型项目里的代码很少是一次性长出来的。一个功能开关判断、一个灰度开关、一个历史功能 flag,最开始可能只是为了控制一条业务路径。时间久了,它会散落到页面、服务、工具类、埋点、资源加载、兜底逻辑里。
等这个判断终于确定下来,比如永远走国内逻辑,或者永远关闭某条旧链路,源码并不会自动变干净。if (false) 只是最表层的问题。更麻烦的是它后面会拖着一串 helper、空方法、恒返回方法、无意义的回调和废弃分支。
手动清理当然可以,但在几百万行代码里做这件事,很容易变成一场漫长的找茬游戏。IDE 能提示一部分,编译器和构建期代码收缩与优化工具(例如 Android 的 R8)也能在产物层面消掉一部分,但源码里的复杂度仍然留在那里。
所以写了一个工具来专门处理这类场景:dead-code-pruner。
这个工具已经在一个 100 万行代码级的大型 Android 项目中完成过全量清理,结果如下:
| 指标 | 结果 |
|---|---|
| 完整清理耗时 | 约 10 分钟 |
| 修改文件 | 2300+ |
| 净删除代码 | 40000+ 行 |
| 编译结果 | 首次通过 |
清理解决的不是几行 if
死代码清理的收益,不只是少几行代码。
第一是主路径会变短。旧分支留在代码里,读代码的人必须不断判断”这里现在还会走吗”。当判断本身已经没有业务意义时,这种理解成本就是纯噪音。
第二是后续重构会更稳。废弃逻辑经常引用旧模型、旧接口、旧资源、旧埋点。只要它们还在,重构时就要额外考虑一堆其实不会运行的路径。
第三是代码搜索和 code review 会更干净。搜索一个方法名时,结果里混着大量历史分支,很容易误判影响面。review 里看到一段旧分支,也会浪费精力确认它是不是还有效。
还有一个现在越来越明显的收益:它会让 AI 更容易写对代码。
AI Agent 在理解项目时,本质上也要从文件、引用、调用链和局部上下文里判断意图。死代码越多,它读到的无效模式越多,检索到的干扰样本也越多。
对于大项目尤其明显:
- 旧分支会占掉上下文窗口,让真正相关的代码更晚出现,甚至进不来
- 废弃 helper 会让 AI 误以为还有另一套业务路径需要兼容
- 重复但失效的判断会增加实现分支,生成更多不必要的防御代码
- 调用链里混着不会运行的节点,会干扰 bug 定位和逻辑补全
方案思考
| 方案 | 适合场景 | 问题 |
|---|---|---|
| 手动清理 | 小范围、强业务语义 | 成本高,不可重复,容易漏掉级联代码 |
| IDE Inspection | 单文件、简单 if(true/false) | 对跨文件引用、恒返回方法、级联删除不够用 |
| 编译器 / 构建期代码收缩与优化工具(如 Android R8) | 优化最终产物 | 源码复杂度仍然存在,review 和维护成本不变 |
| 正则脚本 | 很窄的文本替换 | 难区分注释、字符串、声明、调用和控制流边界 |
| AI Agent | 辅助分析、生成工具、解释 diff | 每个文件都要读、思考、修改、校验,处理大型项目会非常慢,结果也不够稳定 |
| AST 工具 | 规则明确、规模大、需要重复执行 | 需要为语法和安全边界写工程化规则 |
AI Agent 很适合帮忙写脚本、补测试、解释某个 case 为什么不能删。但不适合让它直接”把全项目不用的历史功能代码删掉”。
这类任务不是一次判断,而是几千次小判断。每一次都要稳定地区分代码和注释、声明和调用、普通方法和框架入口、同名方法和真实引用。只要其中一小部分判断不稳定,后面的 diff 就会变得很难 review。
吞吐也是问题。Claude Code、Cursor、Codex 这类 Agent 真正处理文件时,并非只扫一遍文本,而是要检索上下文、推理变更、生成修改并检查结果。全仓库处理因此会引入明显的延迟和成本,而确定性的本地分析可以直接处理同类语法模式。
AI Agent 仍然很适合协助开发这个工具:分析失败 case、解释 AST、生成测试夹具,再由人确认边界。但真正扫描和改写全仓库时,我最终选择了基于 AST 的确定性方案:输入是一份明确的常量映射,清理规则写在代码里,典型边界固化成测试。同样的输入重复执行,应该得到同样的输出。
从源码到 AST
AST(Abstract Syntax Tree,抽象语法树)是源码的结构化表示。它不关心空格和换行长什么样,而是把程序拆成“条件”、“操作符”、“调用”、“方法声明”等带层级的节点。
dead-code-pruner 使用 tree-sitter 做语法解析。tree-sitter 能给出每个节点的类型、子节点和字节范围,又支持多种语言,适合做这种需要保留原文大部分格式的源码改写。
例如这段 Java:
if (true && (false || isLegacyMode())) { RetiredFeature.renderLegacy();}在 AST 中可以简化理解为:
if_statement├─ condition: binary_expression (&&)│ ├─ left: true│ └─ right: parenthesized_expression│ └─ binary_expression (||)│ ├─ left: false│ └─ right: method_invocation: isLegacyMode()└─ consequence: block └─ method_invocation: RetiredFeature.renderLegacy()工具因此知道 (false || isLegacyMode()) 是 && 的完整右操作数,可以先利用短路规则折叠两层表达式,而不会删掉半截条件:
if (true && (false || isLegacyMode())) {if (isLegacyMode()) { RetiredFeature.renderLegacy();}如果只靠正则找 true && ...,就还要自己处理括号嵌套、多行调用、注释和字符串。规则一多,实际上就是在重写一个不完整的解析器。
AST 带来的关键边界
配置模式只会应用在 AST 判定的注释和字符串区间之外。因此 Java/Kotlin 文本块、Go 反引号字符串、Swift 扩展字符串、Dart raw string、嵌套块注释和插值边界,不会被当成普通代码盲目替换。
全局清理机制
单个 AST 只能回答“这个文件里有什么”,而死代码清理还必须回答“谁在其他文件里引用它”。所以主流程分成两层:Phase 1 在单文件内把已知常量传播到收敛;Phase 2 建立全项目索引,再通过“删声明 → 简化变化文件 → 刷新索引”迭代到稳定。
flowchart LR
P1["Phase 1<br/>源码简化<br/>文件内收敛"] --> QG1["Phase 间<br/>质量门"]
QG1 --> S1["Phase 2 · Step 1<br/>统一项目扫描"]
S1 --> S2["Step 2<br/>清理死声明"]
S2 -->|"有变化"| S3["Step 3<br/>变化文件重跑 Phase 1"]
S3 --> S4["Step 4<br/>增量刷新索引"]
S4 --> S2
S2 -->|"无变化,收敛"| S5["Step 5<br/>删除空类与空文件"]
S5 --> QG2["终态<br/>质量门"]
QG2 --> Done((完成))
布尔方法内联的位置
完整流程里,布尔恒返回方法由 Phase 2 的死声明清理统一处理。项目仍保留了一个独立的布尔方法内联工具,用于聚焦测试和自定义集成,但它不再是主流程的单独 Phase。
为什么要循环?因为死代码清理经常是级联的。
final boolean legacyEnabled = FeatureFlags.LEGACY_MODE;if (!legacyEnabled && (LegacyFeatureController.RETIRED_DEFAULT || isLegacyMode())) { RetiredFeature.renderLegacy();} else { renderCurrent();}当两个配置常量都固定为 false,Phase 1 可以把整个条件简化为 isLegacyMode(),但还不能只凭单文件视角删掉它。Phase 2 证明 isLegacyMode() 是可内联的布尔恒返回方法后,调用处变成 false,又触发一轮 Phase 1 分支清理,方法定义和跨文件的空类也随之失去引用。
final boolean legacyEnabled = FeatureFlags.LEGACY_MODE;if (!legacyEnabled && (LegacyFeatureController.RETIRED_DEFAULT || isLegacyMode())) { RetiredFeature.renderLegacy();} else { renderCurrent();}renderCurrent();只跑一遍,后面的代码就会留下来。循环到收敛,才是这类清理真正需要的形态。
流程详解
Phase 1:源码简化
第一个 Phase 处理单文件里的表达式和控制流。八个 Step 按实际执行顺序在文件内部循环到不再变化:
Step 1:替换配置常量
把配置里的模式替换成源码级字面量:
final boolean legacyEnabled = FeatureFlags.LEGACY_MODE;final boolean legacyEnabled = false;if (!legacyEnabled && (LegacyFeatureController.RETIRED_DEFAULT || isLegacyMode())) {if (!legacyEnabled && (false || isLegacyMode())) { RetiredFeature.renderLegacy();}Step 2:传播局部常量
检测 final boolean / val / let 等不可变布尔局部变量,将其使用处替换为字面量:
final boolean legacyEnabled = false;if (!legacyEnabled && (false || isLegacyMode())) {if (!false && (false || isLegacyMode())) { RetiredFeature.renderLegacy();}这一步是 scope-aware 的:只在变量声明所在的作用域内传播,不会跨方法或跨类误替换。
Step 3:简化基础布尔表达式
| 输入 | 输出 |
|---|---|
!true | false |
!false | true |
true == false | false |
false != true | true |
if (!false && (false || isLegacyMode())) {if (true && (false || isLegacyMode())) { RetiredFeature.renderLegacy();}Step 4:简化复合表达式
Step 4 处理短路布尔运算和三元表达式:
| 输入 | 输出 |
|---|---|
true && expr | expr |
false && expr | false |
true || expr | true |
false || expr | expr |
true ? A : B | A |
false ? A : B | B |
这两步常常连续发生:先由 Step 3 折叠取反,再由 Step 4 利用短路规则消掉整个右侧表达式。
if (true && (false || isLegacyMode())) {if (isLegacyMode()) { RetiredFeature.renderLegacy();}Step 5:简化语言特有表达式
此时由 Language Adapter 执行语言特有的表达式清理。例如,Kotlin if expression 会在普通死分支消除之前完成简化:
val title = if (false) legacyTitle else currentTitleval title = currentTitleStep 6:消除死分支
| 输入 | 输出 |
|---|---|
if (true) { A } | A |
if (true) { A } else { B } | A |
if (false) { A } | 删除 |
if (false) { A } else { B } | B |
if (false) { A } else if (X) { B } | if (X) { B } |
真实改写时,分支外壳也会一起移除,不会留下无意义的 if (true):
if (false) { RetiredFeature.renderLegacy();} else { renderCurrent();}renderCurrent();Step 7:删除不可达代码
return、throw、break、continue 后面的语句,以及 if-else 所有分支都终止后的后续语句:
if (isLegacyMode()) { RetiredFeature.renderLegacy(); return; unreachableLegacyCleanup();}Step 8:清除无用布尔变量
经过传播后不再有使用处的 final boolean 声明被移除。
final boolean legacyEnabled = false;if (isLegacyMode()) { RetiredFeature.renderLegacy();}这里最容易出问题的不是判断本身,而是工程细节:
- 分支展开后会重新缩进,保证生成的 diff 仍然可读。
- Java 分支包含局部变量声明时,会按需保留
{},避免变量作用域泄漏。 - 删除不会跨过
case/default标签进入相邻的 switch 分支。
Phase 2:项目清理
Phase 2 把跨文件能力放到同一次项目扫描和同一组索引上,再用五个 Step 完成增量收敛。
Step 1:统一扫描项目
工具会对整个项目做一次统一扫描,一次性收集所有分析数据:
flowchart LR
A["读取源码文件"] --> B["扫描方法候选"]
A --> C["建立引用索引"]
A --> D["提取契约与类层级"]
A --> E["扫描字段候选"]
B --> F["统一项目快照"]
C --> F
D --> F
E --> F
统一扫描只读取一次源码并收集:
- 方法候选:方法名、返回类型、参数数量、修饰符、所在类(含起止行号作为唯一标识)、声明行范围
- 引用索引:方法名到可能出现调用的文件集合(含 Kotlin property 访问形式)
- 逐文件契约事实:abstract 契约、继承、显式实现、Go 结构化接口、Swift protocol、Dart abstract/implements
- 字段候选:不可变声明、编译器生成的访问器别名、可见性、模块边界及完整声明范围
引用索引不只看源码里的 method()。Swift #selector、Storyboard/XIB action、Android XML callback、static import、成员 receiver,以及 XML/JSON/plist 中的标识符值都会进入索引。契约事实按文件保存;增量重扫会先移除旧事实再合并新事实,删除掉的接口方法不会残留成过期安全约束。
Step 2:清理死声明
这一步基于统一扫描结果,依次完成唯一标识、安全决策、调用处改写、定义删除和无用字段检查。布尔恒返回方法的内联也发生在这里,因为它可以直接复用同一份引用索引和定义删除逻辑。
方法唯一标识
每个方法候选在扫描时会记录:
semantic_method_key:(module, package, class, name, param_count)— 用于多模块项目的变体冲突检测_method_key:(filepath, class, name, param_count, decl_start, decl_end)— 用于跟踪已处理方法
同名方法在不同类中的处理通过两层机制保证精确:
- 同文件:
class_lines(类的起止行号)限定替换作用域 - 跨文件:只替换
ClassName.method()限定调用
安全决策
死方法候选的 safe_to_inline 标记决定了是否可以被安全处理:
| 声明类型 | 机制 |
|---|---|
Java/Kotlin private、Swift private/fileprivate、Dart _ 私有、Go 未导出 | Adapter 只赋予候选资格,仍需同文件和跨文件索引证明零引用 |
| interface/protocol/abstract/override/带注解成员 | 在调用改写前直接保留 |
| public 实例方法 | 只有引用、继承、动态元数据和契约检查全部证明不可达时才处理 |
| 不可变字段 | 删除范围遵循下文说明的 open/closed 项目边界 |
字段清理遵循与方法清理相同的项目边界模型:
| 项目边界 | 字段删除规则 |
|---|---|
| Open world | 只有无引用、语言级私有的不可变字段会成为候选 |
| Closed world | 还允许删除无引用的 static 常量和顶层不可变字段 |
| 始终保留 | 带注解,以及仍能通过源码、生成访问器或动态引用分析找到的字段 |
对不同类型的候选,工具刻意保留不同的改写边界:
| 候选类型 | 调用点处理 | 定义处理 |
|---|---|---|
| 零参数布尔恒返回方法 | 替换为 true 或 false | 引用清空后删除 |
| 空 void 方法 | 删除无副作用的调用语句 | 引用清空后删除 |
String、数字或 null/nil getter | 保留存活调用的封装 | 项目级零引用证明成立后可删除 |
| 有参方法 | 不替换,避免丢失参数副作用 | 只在 Adapter 能证明全部引用形式时删除 |
| Kotlin/JVM 属性 | 将 getter/property 别名纳入引用索引 | 任一别名有引用时保留源属性 |
空 void 和布尔恒返回 helper 在所有支持语言中都有明确的死代码改写语义,因此可以统一处理。非布尔常量 getter 不会展开到仍存活的调用处,但在零引用证明成立后仍可删除定义。任意非恒定方法体需要更强的适配器级证明:对存在尾随 lambda、函数值、tear-off、selector 或框架调度的语言,“私有且文本零引用”并不足以证明可删除。当前自动删除仅对 Java static helper 等引用形式已完整覆盖的场景开放;Kotlin、Go、Swift 和 Dart 的任意方法体在适配器无法证明所有引用形式时会保留。Kotlin 引用索引同时识别 withLegacyFeature { ... } 这类无括号尾随 lambda 调用。
调用处处理 + 定义删除
调用处处理完之后,工具会重建引用索引,再决定能不能删除方法定义。因为前一步已经改过源码,引用关系可能变化了。这个顺序很关键。
例如,布尔 helper 的调用点会先变成字面量,定义和已经无意义的空 void 调用会在引用校验通过后删除:
private boolean isLegacyMode() { return false;}
private void recordLegacyExposure() {}
void render() { recordLegacyExposure(); if (isLegacyMode()) { if (false) { RetiredFeature.renderLegacy(); } else { renderCurrent(); }}字段也以完整声明为单位删除;对 int used, unused; 这种共享声明范围的写法,只有所有 declarator 都可删时才会删整行。
private static final boolean RETIRED_DEFAULT = false;Step 3:变化文件重跑 Phase 1
只有被死声明清理修改过的文件会再次进入 Phase 1 简化循环,让新出现的常量表达式和死分支继续收敛,不需要重扫整个项目。
上一步留下的 if (false) 会在这里被继续清理:
void render() { if (false) { RetiredFeature.renderLegacy(); } else { renderCurrent(); } renderCurrent();}Step 4:刷新变化文件
进入下一轮前,统一扫描只刷新本轮发生变化的文件。这样既能发现新的清理候选,也能避免再次全项目扫描。
flowchart LR
Changed["本轮变化文件"] --> Drop["移除这些文件的旧事实"]
Drop --> Rescan["重新解析与扫描"]
Rescan --> Merge["合并到项目索引"]
Merge --> Next["下一轮死声明清理"]
Step 5:删除空类与空文件
项目清理收敛后,工具会检测所有成员被删除后剩下的空类。只有跨文件引用也为空时,才会删除类声明;文件如果只剩 package / import 等非声明内容,整个源文件也会被删除。
package com.example.legacy;
final class RetiredFeature {}这个 diff 表示 RetiredFeature.java 整个文件被删除,而不是留下一个只有 package/import 的空壳。
全流程合并后的最终 diff
上面每个 Step 的小 diff,都是同一个 Legacy 功能清理过程中的局部切片。把 Phase 1 的文件内收敛、Phase 2 的声明清理与索引刷新合在一起,最终项目级 diff 会是下面这种形态:
diff --git a/LegacyFeatureController.java b/LegacyFeatureController.java--- a/LegacyFeatureController.java+++ b/LegacyFeatureController.java@@ final class LegacyFeatureController {- private static final boolean RETIRED_DEFAULT = false;-- private boolean isLegacyMode() {- return false;- }-- private void recordLegacyExposure() {}- void render() {- final boolean legacyEnabled = FeatureFlags.LEGACY_MODE;- recordLegacyExposure();- if (!legacyEnabled- && (LegacyFeatureController.RETIRED_DEFAULT || isLegacyMode())) {- RetiredFeature.renderLegacy();- return;- unreachableLegacyCleanup();- } else {- renderCurrent();- }+ renderCurrent(); } }
diff --git a/LegacyScreen.kt b/LegacyScreen.kt--- a/LegacyScreen.kt+++ b/LegacyScreen.kt@@-val title = if (false) legacyTitle else currentTitle+val title = currentTitle
diff --git a/RetiredFeature.java b/RetiredFeature.javadeleted file mode 100644--- a/RetiredFeature.java+++ /dev/null@@-package com.example.legacy;--final class RetiredFeature {- static void renderLegacy() {}-}这个结果同时体现了常量替换、局部传播、布尔折叠、死分支与不可达代码删除、布尔 helper 内联、空 void 清理、无用字段删除,以及最后的空类和空文件清理。
贯穿两个 Phase 的能力
两个 Phase 描述的是执行顺序。安全边界、Language Adapter 和多模块分析不是额外的清理阶段,而是贯穿流水线的约束与上下文:逐次转换检查保护 Phase 1,Adapter 规则参与两个 Phase,多模块与项目边界则主要约束 Phase 2 的删除决策。
安全边界
这个工具故意保守。
4 层安全门控
| 层次 | 时机 | 机制 |
|---|---|---|
| Per-transformation | 每次修改后 | AST 重解析,新增语法错误则回滚到修改前 |
| Phase 间 | Phase 1 后 | 进入项目级删除前全量检查所有已修改文件 |
| Dangling check | 删除定义前 | 验证文件中无残留调用 |
| 终态质量门 | 全流程结束后 | 再次全量检查,回滚有问题的文件 |
保守规则
- 带注解的方法不删,
@Override、DI、路由、AOP、序列化入口都可能靠运行时调用 abstract、open、override、native方法不删- interface、protocol、abstract class 的方法作为契约保留
- 参数数量必须匹配,
render()和render(dialog)不是一个方法 - 同名不同类通过 class_lines 作用域和类名限定来精确处理
- 链式调用不乱改,
method().subscribe()这种不是普通独立调用 - Swift selector、Storyboard / XIB action 这类动态入口进入引用索引
- 有参方法不替换调用处(参数可能有副作用),但定义仍可被删除
- 多模块项目的同名方法按模块隔离分析,不会跨模块冲突
保守会留下一些可以人工继续处理的代码,但这是值得的。自动清理工具最重要的不是看起来删得多,而是能让人放心重复跑。
Language Adapter
每种语言有专属适配器:
| Adapter | 语言特殊边界 |
|---|---|
| Java | Android/JVM 回调、注解、重载参数、interface/abstract class、static import、XML/JSON 元数据、switch 作用域 |
| Kotlin | Android/JVM 回调、顶层 main、注解/default 参数、表达式函数、if expression、JVM property 生成访问器、companion/private 字段、冒号契约 |
| Go | 分组参数、receiver 所属类型、导出/test/init/main 入口、结构化接口、未导出方法与常量、反引号字符串 |
| Swift | 外部/内部参数标签、隐式恒返回、protocol、private/fileprivate、扩展与多行字符串、selector 和 Storyboard/XIB/plist |
| Dart | signature/body 相邻节点、箭头函数、可选参数、abstract/implements、metadata 注解、下划线私有方法与字段、raw string |
多模块项目支持
工具会自动检测项目的构建系统和模块结构:
| 构建系统 | 检测方式 | 模块隔离 |
|---|---|---|
| Gradle (Android / JVM) | settings.gradle / settings.gradle.kts | 每个 :module 独立分析 |
| Maven | pom.xml 子模块 | 每个子目录独立分析 |
| Go | go.mod | 根模块 + 嵌套模块 |
| Dart/Flutter | pubspec.yaml | 根包 + packages/ 子包 |
| Xcode | .xcworkspace / .xcodeproj | 单一工作区 |
模块感知的作用是:不同模块中同名的 Utils.isEnabled() 会被独立跟踪和分析,不会互相冲突。
同一份模块映射还会驱动明确的项目边界模型:
- Application、可执行程序、服务、Flutter App、根 Go executable、Xcode App 和部署元数据属于 closed-world 证据
- App/服务 Gradle 构建中未发布的 library module 跟随宿主使用 closed;library 插件描述的是编译形态,不等于对外发布
- 发布配置和独立 library 产品属于 open-world 证据,并在冲突时优先
- 无法判断的独立项目默认 open,优先保留可能被仓库外部调用的 API
这一区分很关键:在 App 中,零引用意味着所有源码消费者都不可达,因此可以删除无引用的 public static 或顶层声明;在 SDK 中,仓库外仍可能有调用者,所以公开 API 和外部可见的空类型必须保留。
性能与并行处理
性能优化的核心是避免重复执行全项目工作:
- 单文件内部收敛让 Phase 1 不需要重复执行全项目遍历。
- Phase 2 将方法、字段、引用、契约和类层级放在一起构建,用一次统一扫描替代多次相互独立的全仓库索引过程。
- 当项目规模达到并行收益阈值后,CPU 密集的文件转换、项目扫描和字段引用分析会拆分给多个 worker process;小项目保持串行,避免进程启动成本。
- 增量收敛会复用未变化文件的分析数据,让后续轮次的处理量主要取决于变化文件集合。
- 安全快照只集中读取一次,并在整个流水线中复用,不会为每个 Phase 重复读取原始内容。
- 字段引用扫描只提取一次标识符并通过集合查询匹配,使扫描成本主要随源码规模增长,而不是与候选字段数量相乘。
使用
安装依赖:
git clone https://github.com/OldJii/dead-code-pruner.gitcd dead-code-pruner
pip install -r requirements.txt准备 pruner.yaml:
replacements: - pattern: "FeatureFlags.LEGACY_MODE" value: false - pattern: "LegacyFeatureController.RETIRED_DEFAULT" value: false执行:
CLI 会自动查找当前目录中的 pruner.yaml,因此完整管道不需要再传配置文件参数:
python3 -m pruner /path/to/your/project建议先在独立分支跑,跑完之后直接看 diff。
支持的项目生态
项目会根据扩展名和构建结构选择语言 Adapter 与模块边界,当前覆盖五类生态:
| 生态 | 语言 | 扩展名 |
|---|---|---|
| Android | Java, Kotlin | .java, .kt, .kts |
| JVM 服务 | Java, Kotlin | .java, .kt, .kts |
| Go 服务 | Go | .go |
| iOS | Swift | .swift |
| Flutter | Dart | .dart |
在 Android 之外使用
语法适配和自动化测试已经覆盖 Java、Kotlin、Go、Swift 和 Dart,但目前的大规模真实项目验证主要来自 Android。其他生态的框架入口、代码生成方式和动态调度规则可能不同,因此更适合从 fork 和小范围验证开始。
如果发现某种语法没有按预期清理,不要直接对大项目调宽删除规则,而是把它固化成一个最小可复现 case:
- 保留一份最小的输入源码和期望输出。
- 确认缺口来自 AST 节点识别、引用索引,还是框架入口保护。
- 让 AI Agent 协助分析语法树和现有 Adapter,再由人确认新规则的安全边界。
- 先让新 case 通过,再跑完现有回归集,最后应用到目标仓库。
先从小 diff 开始
跨语言清理的关键不是“语法能解析”,而是“项目中所有隐式入口都已经被理解”。在新生态中建议先限定目录、检查 diff,并用目标项目自己的编译和测试作为最终验收。
修改 Adapter 后的完整回归命令
python3 tests/run_tests.pypython3 tests/run_project_tests.pypython3 tests/test_language_matrix.pypython3 tests/test_project_boundary.py结尾
死代码清理很容易被低估。它不像新功能那样显眼,也不像性能优化那样有漂亮数字。但对于一个长期演进的大项目,清理掉确定不会再走的逻辑,本质上是在给之后的每一次开发、重构、review 和 AI 辅助编码减负。
dead-code-pruner 做的事情并不神秘:给它明确的常量事实,它用 AST 和项目级索引把这些事实稳定地传播出去,直到代码收敛。
这类工具最重要的不是聪明,而是确定、保守、可测试、可重复。