Skip to content

Repository files navigation

Crabjure

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.

Why Fast?

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 typesu32 spans, CompactStr for identifiers
  • Parallel file processing — Rayon-based work stealing
  • No GC pauses — pure Rust, no runtime overhead

Performance

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

Diagnostic Coverage

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.

Macro Resolution

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-temp are 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 in crates/crabjure-resolver/data/index/

Supported Archetypes

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.

clj-kondo Compatibility

Crabjure reads .clj-kondo/config.edn and supports a subset of clj-kondo configuration:

Supported

  • :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

Not Supported

  • Custom SCI hooks (.clj-kondo/hooks/)
  • :refer-clojure exclusions in config
  • :output configuration
  • Most other linter-specific options

Example Config

{: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}}

Features

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

Usage

# 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/ --summary

Example Output

Diagnostics 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
───╯

Crates

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)

License

MIT OR Apache-2.0

About

No description, website, or topics provided.

Resources

Stars

28 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages