死代码检测工具对比:Knip、Vulture、Go deadcode,以及我们自己的工具在哪里误报

Raghav Chamadiya13 分钟

死代码检测工具 · 无用代码检测 · 未使用的导出 · Go deadcode · Knip · Vulture

本页目录

选死代码检测工具,先按语言:JS/TS 用 Knip,Python 用 Vulture,Go 用官方的 deadcode 和 staticcheck 的 U1000 检查,ts-prune 已经归档不再维护。多语言的大仓库、需要结合依赖图和提交历史判断能不能删的,可以加上 repowise。但无论用哪个,列表都要人审一遍再删。

工具语言无用导出无用文件无用依赖主要误报来源
KnipJS/TS是是是入口文件没配全
ts-pruneTS是否否已于 2023-12 归档,作者建议迁移到 Knip
VulturePython是(函数、类、变量)部分否装饰器、框架约定,需要白名单
Go deadcodeGo不可达函数否否不理解 //go:linkname
staticcheck U1000Go未使用的标识符否否反射、代码生成
IntelliJ / GoLand 检查多语言是有限否只看当前工程,难以覆盖整个仓库
repowise16 种语言是是部分(整包无人引用的“僵尸包”)框架注册、动态加载、同包调用

Scroll the table sideways to see every column.

表里最后一列的误报来源,下文会用四个真实仓库逐一核对,其中也包括我们自己工具的误报。

死代码有三种

无法到达的文件:没有任何代码 import 它,运行时也不会加载它。重构之后留下的工具文件、早就下线的功能模块,都属于这一类。

无用的导出:一个函数、类、类型被导出了,但没有其他文件用到它。这是最容易下手清理的一类。

僵尸依赖:package.json、requirements.txt、go.mod、pom.xml 里还写着,但代码早就不用了;或者仓库里整个包、整个目录都没人引用。它拖慢构建、增加安全扫描的噪音,升级时还会带来不必要的麻烦。

这三类死代码需要不同的检测方法,一个工具在某一类上很准,在另一类上可能完全看不到。

各工具的位置

Knip:JS/TS 的默认选择

Knip 能同时找出无用文件、无用导出和无用依赖,它会建立项目的模块依赖图,也认识很多框架和构建工具的配置约定。用 Knip 最要紧的是配好入口文件:入口配少了,用到的代码会被报成无用;入口配多了,真正的死代码又会被藏起来。官方文档对这一点讲得很直接。

ts-prune:已归档

ts-prune 只做一件事:列出 TypeScript 里没被用到的导出。它的仓库在 2023 年 12 月 17 日归档,作者在 README 里建议改用 Knip。老项目里还在用的话问题不大,新项目没必要再引入。

Vulture:Python 的起点

Vulture 扫描 Python 代码里没用到的类、函数和变量,轻量,容易放进 CI。它的问题是 Python 的动态性:装饰器注册的函数、框架按名字调用的方法,它都看不出来,所以需要维护白名单。它的结果适合当作待审清单,不要直接拿来删代码。

Go:官方 deadcode 加 staticcheck

Go 团队在 2023 年 12 月发布了 deadcode(golang.org/x/tools/cmd/deadcode)。它从 main 函数出发,用 RTA(快速类型分析)建调用图,凡是到达不了的函数都报出来。官方博客的说法是,它报出来的函数,即使通过动态机制也调用不到;已知的例外是 //go:linkname。加上 -test 参数会把测试程序也作为入口。

注意它的前提:只有 main 包才是分析的起点。如果你的仓库是一个被别人引用的库,没有 main,它就不适用。日常开发里,staticcheck 的 U1000 检查(未使用的函数、类型、字段)更常用,两者可以搭配。

IDE 检查

IntelliJ IDEA、GoLand 的“未使用声明”检查适合边写边看,改一个文件时能马上提示。但它只看当前打开的工程,对多模块、多语言的仓库覆盖不全,也不知道哪些类是通过配置文件或反射加载的。

repowise

repowise 在文件和符号级的依赖图上找死代码:无用文件、无用导出,以及整包没人引用的僵尸包。每条结果都带一个置信度、一句原因和几条证据,再附上最近 90 天的提交次数。它的优势是跨语言、能结合热点和负责人信息;它的弱点和所有基于依赖图的工具一样,下面详细说。

四个真实仓库,四种误报

我们从 repowise 对四个开源仓库的死代码结果里挑出几条,去源码里核对。先说结论:置信度高的结果也可能是错的。

1. OpenViking:FastAPI 路由被当成无用导出

volcengine/openviking 是火山引擎开源的上下文数据库,给 AI agent 用,Python 为主,2,522 个文件。repowise 报了 434 条:289 个无用导出、143 个无法到达的文件、2 个僵尸包。其中 176 条的置信度是 1.0。

但 1.0 里有一批是错的。比如 openviking/storage/vectordb/service/api_fastapi.py 里的 create_collection 和 search_by_vector,被报成“公开符号,没有任何引用”。去源码一看:

python
@collection_router.post("/CreateVikingdbCollection", response_model=ApiResponse)
async def create_collection(request: CollectionCreateRequest, req: Request):

这是 FastAPI 的路由函数,由装饰器注册,框架在收到 HTTP 请求时调用它。整个代码库里确实没有任何文件 import 它,因为本来就不需要。同样,SearchRequest、AddResourceRequest 这些 Pydantic 请求模型是 FastAPI 的请求体,也被报成了无用。

这是我们检测器的错误,而且给了最高置信度。原因是依赖图里只有 import 边,没有“框架调用”这条边。Flask 的 @app.route、Spring 的 @RequestMapping、Celery 的 @task 也是同样的道理。

2. open-code-review:Go 同包调用

alibaba/open-code-review 是阿里开源的代码评审工具,Go 为主,144 个文件,41 条结果。internal/agent/agent.go 里的 CommentWorkerPool 被报成置信度 1.0 的无用导出。

源码里,同一个文件第 96 行就把它作为结构体字段用了,第 195 行还有 NewCommentWorkerPool 构造函数。Go 的同一个包内互相调用不需要 import,所以只看“谁 import 了这个文件”的检测器,会把同包使用全部漏掉。这在 Go 项目里会产生大量误报,尤其是 internal/ 下面的包。

同一个仓库里,bin/ocr.js 被报成无法到达的文件。它是 npm 的 bin 入口,用户在命令行里执行,不会被任何代码 import。这一条 repowise 给的置信度是 0.4,也没有标成可以安全删除,算是判断对了一半。

3. deepseek-harness:270 条,一条都不建议删

deepseek-ai/deepseek-harness 有 12,578 个文件。repowise 报了 270 条:169 个无法到达的文件、101 个无用导出。

但这 270 条的置信度全是 0.4,没有一条标成可以安全删除,可删行数是 0。原因写在证据里:这个仓库大量使用动态 import 和运行时解析的依赖。比如 tsdown.config.ts 被报“没有文件 import 它”,证据里同时写着“包使用了动态 import”、“配置文件,存在运行时加载风险”。配置文件本来就是由构建工具读取的,不会被 import。

这是我认为检测器应该有的行为:看不清楚的时候,降低置信度,明确说不能删。一个以插件机制为核心的仓库(它自己的介绍就是“Everything is a Plugin”),静态依赖图本来就看不全。

4. Gitea:一条误报,一条真的

go-gitea/gitea 有 292 条结果。我们核对了两条置信度 1.0 的:

  • models/actions/runner.go 里的 CountRunnersWithoutBelongingOwner:误报。它在 services/doctor/dbconsistency.go 第 153 行被当作函数值传进去:Counter: actions_model.CountRunnersWithoutBelongingOwner。gitea doctor 命令会调用它。
  • models/auth/access_token_scope.go 里的 ContainsCategory:除了定义处,整个仓库里搜不到其他引用,看起来确实没用。

两条都是置信度 1.0,结果一对一错,所以我一直说列表要先审再删。

误报从哪里来

四个仓库里的误报,原因可以归成三类:

  1. 框架负责调用:路由、请求模型、定时任务、插件钩子。代码里没有 import,运行时却一定会被调用。
  2. 语言的可见性规则:Go 同包调用不需要 import;Java 同包、同类内的调用也一样。只看文件级 import 的工具会漏掉它们。
  3. 按约定或配置加载:配置文件、CLI 入口、插件目录、反射、Java 的 SPI。这些在静态依赖图里是孤岛。

所以不同工具的准确度,很大程度上取决于它有没有建模这些“看不见的边”。Go 的 deadcode 准,是因为它从 main 出发做真正的调用图分析,代价是只能分析可执行程序。Knip 准,是因为它认识大量 JS 框架的约定,代价是需要你把入口配对。

一个稳妥的清理流程

  1. 一次只清一类:先清无用导出,或者先清无用文件,不要一起上。
  2. 从把握最大的开始:先删私有工具函数、早就下线的功能开关、没人依赖的模块。
  3. 先排查入口:路由、定时任务、CLI 子命令、插件注册表、反射、只在测试里用的代码。
  4. 看历史和负责人:最近 90 天还有人改的文件,先问问再删。repowise 的结果里直接带了这个字段。
  5. 小步提交:每个 PR 只删一个目录或一个包,CI 挂了也容易回滚。
  6. 把检测放进 CI:已知的例外写进白名单,新增的结果在评审时看。

试一试

在 repowise.dev 贴一个 GitHub 仓库地址,就能看到它的死代码结果,每一条都带置信度、原因和证据,可以拿去和源码核对。

写作说明

四个仓库的数据来自 repowise 已索引的公开快照:OpenViking 2026-06-14(提交 66622aa),open-code-review 2026-06-07(提交 3885f49),deepseek-harness 2026-09-28,Gitea 2026-07-17。文中引用的每一条“误报”或“真的没用”,都对照源码人工核对过:OpenViking 和 open-code-review 核对的是对应提交,Gitea 核对的是 2026-10-06 的 main 分支;没有核对的结果,我们没有下结论。第三方工具的说明来自其官方文档和 Go 官方博客。仓库最新代码可能已经变化,结果以页面上的快照日期为准。

常见问题

Java 项目用什么工具找死代码?

IntelliJ IDEA 的“未使用声明”检查是最常用的,适合单个工程。整个仓库层面,需要能理解 Spring 注解、SPI、反射的工具,否则会有大量误报。基于依赖图的工具(包括 repowise)在 Java 里要特别注意框架注册的类。

Go 项目怎么找没用的代码?

可执行程序用官方的 `deadcode`,它从 `main` 出发做调用图分析,结果可靠。日常开发用 staticcheck 的 U1000 检查。如果是被别人引用的库,没有 `main`,deadcode 就不适用了。

死代码检测工具能自动删除吗?

Knip 支持自动修复,可以直接删除无用文件。但我建议所有自动删除都放在评审之后。本文的例子里,置信度 1.0 的结果也会出错。

Python 死代码检测为什么误报这么多?

因为 Python 大量依赖装饰器和框架约定。FastAPI、Flask 的路由函数,Celery 的任务,pytest 的 fixture,都不会被 import,却会被调用。Vulture 用白名单解决,基于依赖图的工具需要识别这些装饰器。

死代码多说明代码质量差吗?

不一定。数量多少和仓库的规模、是否是给别人调用的库、是否大量用插件机制关系很大。一个公共 SDK 的导出本来就是给外部用的,仓库内部没人引用很正常。

monorepo 怎么找死代码?

先分清哪些包是给外部用的(导出是公开 API),哪些只在仓库内部用。前端部分用 Knip 的 workspace 配置,后端按语言用各自的工具,跨语言的结果可以用 repowise 汇总,再结合提交历史判断。

索引你的仓库, 免费