Files
gh-hugoduncan-library-skill…/skills/clj-kondo/QUICK_REFERENCE.md
2025-11-29 18:47:15 +08:00

7.9 KiB

clj-kondo Quick Reference

Command Line

# Basic linting
clj-kondo --lint <path>
clj-kondo --lint src test

# Output formats
clj-kondo --lint src --config '{:output {:format :json}}'
clj-kondo --lint src --config '{:output {:format :edn}}'

# Cache dependencies
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel --copy-configs

# Version
clj-kondo --version

Configuration File Location

.clj-kondo/config.edn          # Project config
~/.config/clj-kondo/config.edn  # Global config

Basic Configuration

{:linters {:unused-binding {:level :warning}
           :unused-namespace {:level :warning}
           :unresolved-symbol {:level :error}
           :invalid-arity {:level :error}
           :misplaced-docstring {:level :warning}}
 :output {:pattern "{{LEVEL}} {{filename}}:{{row}}:{{col}} {{message}}"
          :exclude-files ["generated/" "resources/public/js/"]}}

Linter Levels

:off      ; Disable
:info     ; Informational
:warning  ; Warning (default)
:error    ; Error (fails build)

Common Linter Configurations

Disable Specific Linters

{:linters {:unused-binding {:level :off}
           :unused-private-var {:level :off}}}

Exclude Namespaces

{:linters {:unused-binding {:exclude-ns [myapp.test-helpers
                                         myapp.dev]}}}

Consistent Aliases

{:linters {:consistent-alias {:level :warning
                              :aliases {clojure.string str
                                        clojure.set set
                                        clojure.java.io io}}}}

Refer :all

{:linters {:refer-all {:level :warning
                       :exclude [clojure.test]}}}

Macro as Function

{:lint-as {myapp/my-macro clojure.core/let
           myapp/defroute compojure.core/defroutes}}

Inline Suppressions

;; Suppress for entire namespace
(ns myapp.core
  {:clj-kondo/config '{:linters {:unused-binding {:level :off}}}})

;; Suppress specific linters for form
#_{:clj-kondo/ignore [:unused-binding :unresolved-symbol]}
(let [x 1] (undefined-fn))

;; Suppress all linters for form
#_{:clj-kondo/ignore true}
(problematic-code)

;; Suppress with underscore prefix
(let [_unused-var 42] ;; No warning
  ...)

Hook Types

analyze-call Hook

Analyze function/macro calls:

;; config.edn
{:hooks {:analyze-call {mylib/deprecated-fn hooks.my/warn-deprecated}}}

;; hooks/my.clj
(ns hooks.my
  (:require [clj-kondo.hooks-api :as api]))

(defn warn-deprecated [{:keys [node]}]
  {:findings [{:message "This function is deprecated"
               :type :deprecated-api
               :row (api/row node)
               :col (api/col node)
               :level :warning}]})

macroexpand Hook

Transform macro for analysis:

;; config.edn
{:hooks {:macroexpand {mylib/defentity hooks.my/expand-defentity}}}

;; hooks/my.clj
(defn expand-defentity [{:keys [node]}]
  (let [[_ name & body] (:children node)]
    {:node (api/list-node
            [(api/token-node 'def)
             name
             (api/map-node body)])}))

Hook API - Node Functions

;; Query
(api/tag node)           ; :list, :vector, :map, :token, etc.
(api/sexpr node)         ; Convert to s-expression
(:children node)         ; Get child nodes
(api/string node)        ; String representation

;; Position
(api/row node)
(api/col node)
(api/end-row node)
(api/end-col node)

;; Predicates
(api/token-node? node)
(api/keyword-node? node)
(api/string-node? node)
(api/list-node? node)
(api/vector-node? node)
(api/map-node? node)

Hook API - Node Constructors

;; Atoms
(api/token-node 'symbol)
(api/keyword-node :keyword)
(api/string-node "string")
(api/number-node 42)

;; Collections
(api/list-node [node1 node2 node3])
(api/vector-node [node1 node2])
(api/map-node [k1 v1 k2 v2])
(api/set-node [node1 node2])

Hook Return Values

;; Return findings
{:findings [{:message "Error message"
             :type :my-custom-type
             :row (api/row node)
             :col (api/col node)
             :level :warning}]}

;; Return transformed node
{:node new-node}

;; Return both
{:node new-node
 :findings [{:message "..." ...}]}

Common Hook Patterns

Deprecation Warning

(defn deprecation [{:keys [node]}]
  {:findings [{:message "Use new-api instead of old-api"
               :type :deprecated
               :row (api/row node)
               :col (api/col node)
               :level :warning}]})

Argument Count Validation

(defn validate-arity [{:keys [node]}]
  (let [args (rest (:children node))]
    (when (< (count args) 2)
      {:findings [{:message "Expected at least 2 arguments"
                   :type :invalid-arity
                   :row (api/row node)
                   :col (api/col node)
                   :level :error}]})))

Argument Type Validation

(defn validate-type [{:keys [node]}]
  (let [first-arg (second (:children node))]
    (when-not (api/keyword-node? first-arg)
      {:findings [{:message "First argument must be a keyword"
                   :type :invalid-argument-type
                   :row (api/row node)
                   :col (api/col node)
                   :level :error}]})))

Required Map Keys

(defn validate-keys [{:keys [node]}]
  (let [map-node (second (:children node))]
    (when (api/map-node? map-node)
      (let [keys (->> (:children map-node)
                      (take-nth 2)
                      (map api/sexpr)
                      (set))
            required #{:host :port}
            missing (clojure.set/difference required keys)]
        (when (seq missing)
          {:findings [{:message (str "Missing keys: " missing)
                       :type :missing-keys
                       :row (api/row node)
                       :col (api/col node)
                       :level :error}]})))))

Macro Expansion for DSL

(defn expand-defroute [{:keys [node]}]
  (let [[_ method path handler] (:children node)]
    {:node (api/list-node
            [(api/token-node 'def)
             (api/token-node (gensym "route"))
             (api/map-node [method path handler])])}))

IDE Integration

VS Code (Calva)

Built-in support, works automatically.

Emacs

(use-package flycheck-clj-kondo :ensure t)

Vim/Neovim (ALE)

let g:ale_linters = {'clojure': ['clj-kondo']}

IntelliJ/Cursive

Native integration via Preferences → Editor → Inspections.

CI/CD Patterns

GitHub Actions

- name: Install clj-kondo
  run: |
    curl -sLO https://raw.githubusercontent.com/clj-kondo/clj-kondo/master/script/install-clj-kondo
    chmod +x install-clj-kondo
    ./install-clj-kondo
- name: Run clj-kondo
  run: clj-kondo --lint src test

GitLab CI

lint:
  image: cljkondo/clj-kondo:latest
  script:
    - clj-kondo --lint src test

Pre-commit Hook

#!/bin/bash
clj-kondo --lint src test

Troubleshooting

False Positive - Macro Generated Symbol

{:lint-as {myapp/my-macro clojure.core/let}}

Exclude Generated Files

{:output {:exclude-files ["generated/" "target/" "node_modules/"]}}

Hook Not Triggering

  1. Check hook registration in config.edn
  2. Verify namespace path matches
  3. Test with minimal example
  4. Run with --debug flag

Performance Issues

# Cache dependencies once
clj-kondo --lint "$(clojure -Spath)" --dependencies --parallel

# Exclude large directories
{:output {:exclude-files ["resources/public/"]}}

Tips

  • Start with zero config - defaults are good
  • Use --copy-configs to get library-specific rules
  • Write hooks for domain-specific linting
  • Test hooks with minimal examples
  • Document why you suppress warnings
  • Run in CI to catch issues early
  • Cache dependency analysis for speed