A fast static analyzer for Clojure and ClojureScript, written in Rust.
Status: Proof of Concept — Not production-ready. Many lints are missing, macro handling is incomplete, and false positives are expected.
Crabjure borrows architectural ideas from oxc:
- Arena allocation via bumpalo — CST/AST nodes live in contiguous memory
- Lossless CST — hand-written reader preserves all trivia (whitespace, comments)
- Compact types —
u32spans,CompactStrfor identifiers - Parallel file processing — Rayon-based work stealing
- No GC pauses — pure Rust, no runtime overhead
Benchmarks on third-party Clojure/ClojureScript codebases (AMD Ryzen 9 9950X, NVMe SSD, --summary mode):
| Project | LOC | crabjure | clj-kondo | Speedup |
|---|---|---|---|---|
| Clojure | 41k | 133ms | 2.3s | 17x |
| ClojureScript | 76k | 265ms | 5.6s | 21x |
| Penpot | 251k | 952ms | 19.3s | 20x |
| Metabase | 554k | 2.8s | 48s | 17x |
crabjure with --enable-java-reflection --java-mode inproc --enable-js; clj-kondo with --parallel
crabjure is not yet feature-complete. Comparison of detected issues:
| Project | crabjure errors/warnings | clj-kondo errors/warnings | Notes |
|---|---|---|---|
| Clojure | 314 / 862 | 369 / 1725 | Fewer errors, fewer warnings |
| ClojureScript | 723 / 2149 | 511 / 2184 | Close parity |
| Penpot | 592 / 5052 | 4392 / 2254 | More warnings, fewer errors |
| Metabase | 89718 / 7941 | 16737 / 8863 | Many false positives (macro handling) |
Gap to close: Macro resolution, custom lint rules, clj-kondo config compatibility.
Crabjure uses archetype-based macro resolution instead of clj-kondo's SCI-based hooks:
- Static archetypes: Common patterns like
let-like,defn-params,for-like,with-tempare recognized - No runtime evaluation: Macros are not expanded; instead, binding patterns are inferred from archetypes
- Built-in support:
clojure.core,cljs.core, and common libraries have pre-defined archetypes incrates/crabjure-resolver/data/index/
| Archetype | Examples |
|---|---|
let-like |
let, when-let, if-let, binding |
defn-params |
defn, defmacro, fn |
for-like |
for, doseq, dotimes |
with-temp |
with-open, with-redefs |
callback |
swap!, update, send |
def-like |
def, defonce |
Custom macros without archetypes may produce false positives for unresolved symbols.
Crabjure reads .clj-kondo/config.edn and supports a subset of clj-kondo configuration:
:linters— severity levels (:error,:warning,:off) for::unresolved-symbol:unused-binding:shadowed-var:redundant-let:missing-protocol-method:namespace-mismatch(as:ns-mismatch)
:linters {:unresolved-symbol {:exclude [...]}}— symbol exclusions:lint-as— treat one macro as another:hooks {:analyze-call {...}}— maps to archetype resolution
- Custom SCI hooks (
.clj-kondo/hooks/) :refer-clojureexclusions in config:outputconfiguration- Most other linter-specific options
{:linters {:unresolved-symbol {:level :warning
:exclude [(re-frame.core/reg-event-fx)
(my.ns/defmacro-with-bindings)]}}
:lint-as {my.core/defservice clojure.core/defn}}What crabjure has that clj-kondo doesn't:
- JavaScript interop analysis — Uses oxc_resolver to lint JS imports in ClojureScript
- JVM reflection — In-process JVM via JNI for accurate Java interop resolution
- Parallel by default — Processes files concurrently without configuration
- Sub-second feedback — Fast enough for on-save linting in large monorepos
# Build
cargo build --release
# Basic check
target/release/crabjure check src/
# With Java reflection and JS analysis
target/release/crabjure check src/ \
--enable-java-reflection \
--java-mode inproc \
--enable-js
# Summary only
target/release/crabjure check src/ --summaryDiagnostics are rendered using ariadne for pretty, Rust-style error messages:
[S0001] Error: Unresolved symbol: nme
╭─[ example.clj:5:28 ]
│
5 │ (let [msg (str "Hello, " nme)]
───╯
[L0005] Warning: namespace clojure.string is required but never used
╭─[ example.clj:2:14 ]
│
2 │ (:require [clojure.string :as str]))
│ ───────┬──────
│ ╰──────── Remove this namespace from the :require list.
───╯
[L0004] Warning: unused binding name
╭─[ example.clj:4:14 ]
│
4 │ (defn greet [name]
│ ──┬─
│ ╰─── binding introduced here
───╯
| Crate | Description |
|---|---|
crabjure-cli |
CLI binary: check, index, daemon commands |
crabjure-cst |
Lossless Clojure reader and CST |
crabjure-ast |
Arena-allocated AST with sibling pointers |
crabjure-hir |
HIR with lexical scopes and bindings |
crabjure-resolver |
Namespace resolution and semantic analysis |
crabjure-diagnostic |
Diagnostic types and stable codes |
crabjure-reporter |
Human-readable output via ariadne |
crabjure-jvm |
JVM reflection via JNI or daemon |
crabjure-js |
JS module resolution via oxc_resolver |
crabjure-project |
Project discovery and classpath resolution |
crabjure-span |
Span and source file utilities |
crabjure-config |
Lint configuration types |
crabjure-annotations |
Comment annotation extraction |
crabjure-edn |
EDN parsing (legacy) |
MIT OR Apache-2.0