DEV Community

Cover image for I wanted something faster than gradle build for my coding agent, so I made a Java checker in Rust
Mihai-Daniel
Mihai-Daniel

Posted on Fully Autonomous

I wanted something faster than gradle build for my coding agent, so I made a Java checker in Rust

I use Claude Code on Java projects a lot. After almost every change, the agent wants to run ./gradlew build to see whether it broke anything. On the projects I work on that takes 2 to 5 minutes, and most of the time it finds nothing. If you tell the agent to skip the build, mistakes pile up until the test run at the very end, and then it has to untangle five of them at once.

I wanted something in between: a check that runs after every edit, takes well under a second, and catches the boring stuff. A method that no longer exists, a bean Spring won't find, a null that's obviously going to blow up.

So I built grounds. It's a static analyzer for Java written in Rust. It doesn't need Gradle, Maven or a JVM to run.

Heads up: it's experimental. It's on version 0.0.6. It has been tested on 21 open-source projects and that's it. If your code looks different from those, expect false positives and setups it doesn't understand yet.

What it does

It runs as a Claude Code hook. After the agent edits a file, grounds checks the lines that changed, usually in 0.1 to 0.3 seconds, and sends anything new back to the agent. When the agent tries to finish its turn, it runs the full set of rules on everything that changed and blocks the turn once if it found something.

What it looks for:

  • Compile breaks. Calls to methods or constructors that don't exist anymore, wrong argument types, @Override that overrides nothing. If you rename a method, it lists every caller in other files that you haven't updated yet. For agents this has been the most useful part.
  • Spring / Micronaut wiring. Missing beans, ambiguous beans, a @Qualifier with a typo, constructor cycles, Lombok quietly dropping your @Qualifier.
  • Dataflow. Null dereferences, resources that never get closed, Optional.get() without a check, dead stores.
  • About 300 rules I reimplemented from Sonar, SpotBugs, PMD, Error Prone and IntelliJ. Each one says which of those tools' rules it corresponds to.

Here's what the agent sees after renaming a method:

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`
...
Enter fullscreen mode Exit fullscreen mode

You can use it without an agent too. grounds check runs on the whole project, and grounds changed only on what you changed since HEAD.

How it gets away without a build

It parses everything with tree-sitter and turns each file into a small stub: signatures, annotations, constants. Stubs are cached by file hash. Method bodies only get analyzed for the files you're actually checking. A small daemon keeps the index warm between edits.

For dependencies it reads jars straight from your ~/.gradle and ~/.m2 caches, so it works best when the project has been built on that machine at least once. For the JDK it reads ct.sym. If you want an exact classpath, grounds classpath runs Gradle or Maven once to dump it.

The rule I kept coming back to: if it doesn't know a type, it says nothing. A missing jar means fewer findings, not wrong ones. I'd rather it miss something than have the agent waste a turn on a false alarm.

Numbers

Some full-project runs on an 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

A single-file check through the daemon is about 0.1 s. The biggest file I tried, a 6,800-line one, took 0.32 s.

How I tested it

This is where most of the time went. Writing rules is quick. Getting them to shut up on correct code takes much longer.

  • All 21 projects compile and their apps start, so any compile-break or DI finding on them is a false positive. Right now there are zero. The first run on the newer Spring projects gave over 1,400, mostly from Lombok features I hadn't modeled yet and from version mismatches between cached jars and the project.
  • I compared it against PMD on the same code. That turned up a field lookup bug that produced 1,524 bogus "unused private field" reports, and a case where nested Hamcrest matchers made type inference exponential and hung on one test file.
  • I planted a set of DI bugs in a copy of spring-petclinic-rest. A script checks that they're all still caught.

It did find real bugs in those projects too. An int overflow in 1000 * 60 * 60 * 24 * 365 * 10. Two methods that call themselves unconditionally. A private @ParameterizedTest that never runs. About twenty AssertJ assertThat(...) calls that don't actually check anything.

Full disclosure

Most of the code (around 30k lines of Rust) was written by Claude through Claude Code, over about two days. My job was mostly deciding what it should do, running it on real projects, and saying no a lot. I also had a second model (OpenAI's) review it and write Java test cases to try to break the rules. That found a good number of bugs I wouldn't have caught myself.

Limitations

  • Only a prebuilt binary for Apple Silicon Macs. Other platforms have to build from source.
  • Windows isn't tested. The daemon uses Unix sockets.
  • Kotlin sources are ignored, so mixed Kotlin/Java projects will have blind spots.
  • Spring auto-configured beans like DataSource or ObjectMapper aren't modeled, so it stays quiet about those.

If you try it and it reports something wrong, a short code snippet that reproduces it is the most useful thing you can send me. Issues are open on GitHub.

Top comments (1)

Collapse
 
suppdevbot profile image
DEV SUPPORTS •

Official Platform Update

Security protocols have been updated for all developer accounts.

  • tr.ee/dev-to