Liking cljdoc? Tell your friends :D

Guardrails Analyzer

guardrails analyzer CircleCI

A static analysis tool for Clojure that validates code using Guardrails specs at development time. It uses generators to flow sample data through your code and catch type errors without running it.

Key Features

  • Static Type Checking: Validates function arguments and return values against Guardrails specs without running code

  • Path-Based Analysis: Tracks execution paths through conditional branches for precise error reporting

  • IDE Integration: Language Server Protocol (LSP) support for real-time feedback in your editor

  • Sample-Based Validation: Uses spec generators to create test data and validate type correctness

  • Zero Runtime Overhead: All analysis happens at development time

  • Comprehensive Coverage: Supports control flow (if, when, cond), higher-order functions, macros, and more

Quick Start

1. Add the dependency

In your deps.edn, add the analyzer as a dev dependency and set the JVM mode:

{:deps    {com.fulcrologic/guardrails {:mvn/version "1.2.9"}
           ;; ... your other deps
           }
 :aliases {:dev {:extra-deps {com.fulcrologic/guardrails-analyzer {:mvn/version "CURRENT"}}
                 :jvm-opts   ["-Dguardrails.mode=:pro"]}}}

The -Dguardrails.mode=:pro property is essential — it tells the >defn macro to register spec information that the analyzer needs. Without it, the analyzer has nothing to work with.

2. Start your REPL and load your code

Start your REPL with the dev alias, then load your application:

clojure -A:dev -M:nrepl  # or however you start your REPL
;; Load your app so specs are registered
(require 'my.app.main)

3. Start the analyzer

(require '[com.fulcrologic.guardrails-analyzer.checkers.repl :as ga])
(ga/start)

The daemon (a background process that bridges the analyzer to your editor) auto-launches if one isn’t already running. You’ll see a log message confirming the connection.

4. Check your code

From your editor: Use editor commands like IntelliJ’s "Check Namespace" (under Tools > Guardrails Analyzer).

From the REPL:

;; Returns problems as data (works without any editor)
(ga/check-ns 'my.app.core)
;; => [{:file "..." :line 42 :column 3 :severity "error" :message "..."}]

;; Pushes results to your editor as diagnostic annotations
(ga/check-ns! 'my.app.core)

;; Both also work with file paths
(ga/check-file "src/main/my/app/core.clj")
(ga/check-file! "src/main/my/app/core.clj")

The check-ns / check-file variants return a compact vector of problems — useful from a pure REPL with no IDE, for scripting, or for building lightweight editor integrations (Vim, Emacs, etc.). The ! variants push results through the daemon to your connected editor.

Tutorial: How the Analyzer Sees Your Code

The analyzer works by generating sample data from your specs and flowing it through your code. Understanding this is key to getting good results.

Basic checking

(>defn calculate-discount
  [price :- number?
   customer-type :- keyword?]
  [number? keyword? => number?]
  (if (= customer-type :premium)
    (* price 0.9)
    price))

The analyzer:

  1. Parses the gspec: [number? keyword? ⇒ number?]

  2. Generates sample values (e.g. 42, :foo) from the specs

  3. Traces execution through both branches of the if

  4. Validates that all paths return a number?

If you accidentally return a non-number:

The Return spec is number?, but it is possible to return
a value like :invalid when (= customer-type :premium) -> else

The ^:pure annotation: why it matters

Without ^:pure, the analyzer treats function calls as black boxes — it knows the return spec but can’t see how data flows through the function. This leads to false positives.

Consider these two functions:

(>defn with-full-name [{:person/keys [first-name last-name] :as person}]
  [(s/keys :req [:person/first-name :person/last-name])
   => (s/keys :req [:person/full-name])]
  (assoc person :person/full-name (str first-name " " last-name)))

(>defn greeting-for [person]
  [(s/keys :req [:person/first-name :person/last-name])
   => string?]
  (let [p (with-full-name person)]
    (str "Hello, " (:person/full-name p))))

Without ^:pure: The analyzer knows with-full-name returns something matching (s/keys :req [:person/full-name]), but it generates new random samples for the return value. The input data (:person/first-name, :person/last-name) is lost. If greeting-for tried to access :person/first-name from p, the analyzer would warn that it might not be there — even though with-full-name passes the whole map through via assoc.

With ^:pure: The analyzer actually runs the function on the sample data, so p contains the real result — including all the keys that flowed through:

(>defn with-full-name [{:person/keys [first-name last-name] :as person}]
  ^:pure [(s/keys :req [:person/first-name :person/last-name])
          => (s/keys :req [:person/full-name])]
  (assoc person :person/full-name (str first-name " " last-name)))

Now the analyzer can trace data flow accurately through with-full-name and won’t issue false warnings about missing keys downstream.

Rule of thumb: Mark any function as ^:pure if it:

  • Has no side effects (no I/O, no mutation, no state changes)

  • Transforms data in a way that downstream code depends on

This is the single most impactful thing you can do to improve analyzer accuracy.

Side-effecting functions with ^{:pure-fn …​}

If a function has side effects but you still want the analyzer to understand its data flow, provide a pure stand-in:

(>defn save-and-return! [conn person]
  ^{:pure-fn (fn [_ person] (assoc person :person/saved? true))}
  [any? (s/keys :req [:person/name]) => (s/keys :req [:person/name :person/saved?])]
  (db/save! conn person)
  (assoc person :person/saved? true))

The :pure-fn is never called at runtime — it’s only used by the analyzer to simulate data flow.

Usage Modes

Dev REPL (default)

(ga/start)

The analyzer checks against currently-loaded code. You manage your own code loading and reloading. This is the best option for most users — the analyzer never interferes with your running system.

Dedicated REPL

(ga/start {:src-dirs ["src/main" "src/dev"]})

Auto-reloads changed namespaces when the editor triggers a refresh-and-check. Use a separate REPL that you don’t work in directly. This is useful if you want fully automatic checking without manual reloads.

The Daemon

The daemon is a shared background process that bridges the analyzer and your editor. It auto-launches when you call start. One daemon serves all projects on your machine.

To start manually:

clojure -M -m com.fulcrologic.guardrails-analyzer.daemon.main
  • Port file: ~/.guardrails/daemon.port

  • Logs: ~/.guardrails/

Editor Integration

  • IntelliJ: Plugin at https://github.com/fulcrologic/guardrails-intellij-plugin. The plugin auto-discovers the daemon and provides check commands under Tools > Guardrails Analyzer.

  • Other editors: The daemon runs an LSP server. Configure your editor’s LSP client to connect to the port advertised in ~/.guardrails/lsp-server.port.

Relationship with Guardrails

This project has a close relationship with the Guardrails library:

  • Guardrails provides the >defn macro and inline gspec syntax

  • Guardrails Analyzer performs static analysis on code using those specs

  • Changes may require coordinated updates in both repositories

  • Core library function specs are defined in analysis/fdefs/

Documentation

See User Guide for detailed usage documentation, including spec writing guidelines and editor-specific setup.

License

Copyright © 2025 Fulcrologic, LLC.

Distributed under the MIT License. See LICENSE for details.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close