dev.to #ai短讯
为了让编码 Agent 更快,我用 Rust 写了个 Java 检查器替代 Gradle
作者在使用 Claude Code 处理 Java 项目时,发现每次修改后运行 ./gradlew build 耗时 2-5 分钟且多数无报错,导致错误堆积。为此,作者开发了一个基于 Rust 的轻量级 Java 检查工具,旨在实现秒级反馈,在编辑后快速捕获基础错误,平衡了构建速度与即时反馈的需求。
我经常在 Java 项目中使用 Claude Code。几乎每次修改后,代理都会想要运行 ./gradlew build 来查看是否破坏了任何内容。在我负责的项目中,这需要 2 到 5 分钟,而且大多数时候它什么也没发现。如果你告诉代理跳过构建,错误会堆积起来,直到最后的测试运行时才暴露出来,然后它必须一次性解开五个错误。
我想要一种折衷方案:一种在每次编辑后运行的检查,耗时远低于一秒,并能捕获那些枯燥的问题。比如不再存在的方法、Spring 找不到的 Bean、或者明显会导致崩溃的空指针。
所以我构建了 grounds。这是一个用 Rust 编写的 Java 静态分析器。它不需要 Gradle、Maven 或 JVM 即可运行。
注意:它是实验性的。目前版本为 0.0.6。它已在 21 个开源项目中经过测试,仅此而已。如果你的代码与这些项目不同,请预期会出现误报以及它尚不理解的环境配置。
它能做什么
它作为 Claude Code 的钩子运行。当代理编辑文件后,grounds 会检查发生变化的行,通常在 0.1 到 0.3 秒内完成,并将任何新发现的问题发送回代理。当代理试图结束其回合时,它会对所有更改的内容运行完整的规则集,如果发现问题,则会阻止该回合继续。
- 编译错误。调用已不存在的方法或构造函数、参数类型错误、覆盖空方法的 @Override。如果你重命名了一个方法,它会列出其他文件中尚未更新的每个调用者。对于代理来说,这是最有用的部分。
- Spring / Micronaut 依赖注入。缺失的 Bean、歧义的 Bean、拼写错误的 @Qualifier、构造函数循环、Lombok 悄悄丢弃你的 @Qualifier。
- 数据流。空指针解引用、从未关闭的资源、未检查就使用的 Optional.get()、死存储。
- 我从 Sonar、SpotBugs、PMD、Error Prone 和 IntelliJ 重新实现了大约 300 条规则。每条规则都注明了对应哪个工具的规则。
以下是代理在重命名方法后看到的内容:
grounds: API changes in src/main/java/.../Owner.java broke 15 call site(s) in 6 other file(s) not yet updated:
src/main/java/.../PetController.java:103:9: `Owner` has no method `addPet`
...你也可以在没有代理的情况下使用它。grounds check 会在整个项目上运行,而 grounds changed 仅针对你自 HEAD 以来更改的内容进行检查。
它是如何在不进行构建的情况下工作的
它使用 tree-sitter 解析所有内容,并将每个文件转换为一个小存根:签名、注解、常量。存根按文件哈希缓存。只有在你实际检查的文件中才会分析方法体。一个小守护进程在编辑之间保持索引活跃。
对于依赖项,它直接从你的 ~/.gradle 和 ~/.m2 缓存中读取 jar 包,因此当项目在该机器上至少构建过一次时效果最佳。对于 JDK,它读取 ct.sym。如果你想要精确的类路径,grounds classpath 会运行一次 Gradle 或 Maven 来转储它。
我反复回到的一条规则是:如果它不知道某个类型,它就什么都不说。缺少 jar 包意味着发现更少,而不是错误的发现。我宁愿它漏掉一些东西,也不愿让代理在一个误报上浪费一个回合。
数据
在 M4 Pro 上的一些全项目运行结果:
| project | files | time |
|---|---|---|
| spring-petclinic | 50 | 0.24 s |
| micronaut-core | 6,371 | 3.4 s |
| thingsboard | 6,884 | 4.9 s |
| camunda | 15,743 | 10.2 s |
通过守护进程的单文件检查大约需要 0.1 秒。我尝试过的最大文件是一个 6,800 行的文件,耗时 0.32 秒。
我是如何测试它的
大部分时间都花在这里了。编写规则很快。让它们对正确的代码保持沉默则需要更长的时间。
- 全部 21 个项目均能编译成功且应用可以启动,因此针对它们发现的任何编译错误或依赖注入(DI)问题均为误报。目前误报数为零。在较新的 Spring 项目上首次运行时,发现了超过 1,400 个问题,主要源于我尚未建模的 Lombok 特性以及缓存 jar 包与项目版本之间的不匹配。
- 我将它与 PMD 在同一代码库上进行了对比。这揭示了一个字段查找 bug,导致了 1,524 条虚假的“未使用的私有字段”报告;还发现了一种情况,即嵌套的 Hamcrest 匹配器导致类型推断呈指数级增长,并在一个测试文件上挂起。
- 我在 spring-petclinic-rest 的一个副本中植入了一组 DI 错误。一个脚本检查确认这些错误仍能被捕获。
它确实在这些项目中也发现了一些真实的 bug。例如:1000 * 60 * 60 * 24 * 365 * 10 中的整数溢出;两个无条件调用自身的递归方法;一个从未运行的私有 @ParameterizedTest;以及大约二十个实际上并未进行任何检查的 AssertJ assertThat(...) 调用。
完整披露
大部分代码(约 3 万行 Rust)是由 Claude 通过 Claude Code 编写的,耗时约两天。我的主要工作是决定让它做什么、在真实项目上运行它,并频繁地否决它的方案。我还让另一个模型(OpenAI 的)审查代码,并编写 Java 测试用例试图破坏规则。这发现了许多我自己不会注意到的 bug。
局限性
- 仅提供适用于 Apple Silicon Mac 的预构建二进制文件。其他平台必须从源代码构建。
- 未测试 Windows 平台。守护进程使用 Unix 套接字。
- Kotlin 源文件被忽略,因此混合 Kotlin/Java 的项目会有盲区。
- 未对 Spring 自动配置的 Bean(如 DataSource 或 ObjectMapper)进行建模,因此对这些内容保持静默。
如果你尝试使用它并发现其报告有误,最能帮助我的是提供一个能复现该问题的简短代码片段。GitHub 上已开放 Issue 提交通道。
译文已达到本站中文翻译的字数上限,剩余内容请查看原文。