Eastwood Clojure Linter

repository·master·Indexed 22 days ago

https://github.com/jonase/eastwood

A high-accuracy Clojure linter for JVM Clojure (>= 1.7.0) that leverages the Clojure compiler's evaluation and macroexpansion capabilities to spot bugs. Optimized for CI environments, it can be run as a Leiningen plugin, via deps.edn, or within a REPL. It includes a variety of linters such as :bad-arglists, :reflection, and :wrong-arity, and supports custom configuration files to selectively disable warnings.

Tokens
10.3K
Snippets
32
Records
54
Agent score
28%

What's inside Eastwood

  1. Use the :reflection linter to systematically avoid reflection

    master

    The :reflection linter helps you identify and address reflection warnings from the Clojure compiler. Addressing these improves performance, maintainability, and compatibility with newer JDKs and GraalVM.

    Key Behaviors:

    • Scope: By default, it only emits warnings if reflection occurs within your project's source or test paths.
    • Overrides: It ignores any (set! warn-on-reflection ...) settings in a namespace, as Eastwood analyzes each top-level form with a binding that overrides the surrounding choice.
    • Macros: If a third-party macro expands to reflective access within your source path, it will be reported, as this creates reflective code in your codebase.
  2. Understand Eastwood's analysis approach and limitations

    master

    Accuracy and Requirements

    Eastwood uses tools.analyzer and prefers evaluation and macroexpansion over other approaches to ensure high accuracy (comparable to the Clojure compiler).

    Critical Requirement: Eastwood can only lint a file if Clojure can successfully compile it. If a file has syntax errors, Eastwood will not be able to finish linting it. It is recommended to run lein test or lein check before running Eastwood to ensure your code is compilable.

    Supported Runtimes

    • Supported: JVM Clojure (>= 1.7.0).
    • Not Supported: ClojureScript or Clojure/CLR. For ClojureScript support, consider using .cljc files.
  3. Use the :wrong-pre-post linter

    master

    The :wrong-pre-post linter ensures that Clojure preconditions (:pre) and postconditions (:post) are defined correctly.

    Common mistakes detected:

    • Missing vectors: Pre/postconditions must be a vector of expressions. If you provide a single expression instead of a vector (e.g., {:pre (>= x 0)} instead of {:pre [(>= x 0)]}), Clojure treats the expression as multiple independent assertions, which often results in no errors being thrown.
    • Always-true conditions: The linter checks if any conditions in the vector are values that are always logically true or false (e.g., passing a function Var like non-neg? instead of a call like [non-neg? x]).
  4. Avoid `:wrong-tag` in `extend-type` and `extend-protocol`

    master

    The extend-type and extend-protocol macros propagate the provided class name as a type tag to the first argument of all functions. If you use a runtime-evaluated expression (like (Class/forName "[D")) as the type, the macro expands to an invalid type tag that Clojure silently ignores, causing reflection. Eastwood will issue a :wrong-tag warning.

    Solution: Use the extend function instead of the convenience macros. This allows you to use a valid type tag (like ^doubles) on the first argument manually, which avoids reflection and satisfies Eastwood.

    ;; Avoid this (causes :wrong-tag and reflection):
    (extend-protocol PGetElem
      (Class/forName "[D")
        (get-elem [m idx] (aget m idx)))
    
    ;; Use this instead (avoids reflection and warnings):
    (extend (Class/forName "[D")
     PGetElem
     {:get-elem
      (fn ([^doubles m idx]
        (aget m idx)))})
  5. Avoid shadowing global Vars with `:local-shadows-var`

    master

    The :local-shadows-var linter warns when a local name (function argument, let binding, or record field) has the same name as a global Var and is called as a function. This can lead to accidental bugs where you intend to call a global function but instead call a local value.

    Linter behavior:

    • It warns if the name is used in the first position of a form (a function call).
    • It warns if Eastwood cannot prove the bound value is actually a function.
    • It warns if a Clojure record field is called as a function when a Var with the same name is visible.

    How to resolve: To explicitly use the global Var when a local name shadows it, qualify the name with its namespace or a namespace alias (e.g., using :as in a require form).

    ;; This will cause a warning because Eastwood cannot prove 'replace' is a function
    (let [replace (comp str biginteger)]
      (println (replace 5)))
  6. Use the :non-dynamic-earmuffs linter for variable naming

    master

    The :non-dynamic-earmuffs linter enforces a naming convention for dynamic variables. Variables marked ^:dynamic should use 'earmuff' notation (surrounded by asterisks), and non-dynamic variables should not.

    Valid Examples:

    • (def foo 42) (Non-dynamic, no earmuffs)
    • (def ^:dynamic *foo* 42) (Dynamic, with earmuffs)

    Invalid Examples:

    • (def ^:dynamic foo 42) (Dynamic, but missing earmuffs)
    • (def *foo* 42) (Earmuffed, but missing ^:dynamic tag)
    ;; OK
    (def foo 42)
    (def ^:dynamic *foo* 42)
    
    ;; NOK
    (def ^:dynamic foo 42)
    (def *foo* 42)
  7. Namespace and File Name Consistency Check

    master

    Eastwood performs a mandatory check to ensure that Clojure file names match their declared namespaces. This check is performed before any other linting.

    Rules:

    • Dots in a namespace (e.g., foo.bar.baz) must correspond to path separators (foo/bar/baz.clj).
    • Dashes in a namespace (e.g., foo.bar.baz-tests) must correspond to underscores in the file name (foo/bar/baz_tests.clj).

    If a mismatch is found in files within :source-paths or :test-paths, Eastwood will print an error and abort all further linting. This prevents issues where require fails or tests are skipped due to incorrect file/namespace mapping.

  8. Use the :wrong-ns-form linter

    master

    The :wrong-ns-form linter detects syntax errors or non-standard options in ns forms. It specifically warns about:

    • Multiple ns forms in a single file.
    • References that do not begin with documented keywords: :require, :use, :import, :refer-clojure, :load, or :gen-class.
    • Use of undocumented flag keywords (only :reload, :reload-all, and :verbose are supported).
    • Using flag keywords during normal development (they are typically for interactive use).
    • :require or :use followed by a single-item list (e.g., (:require (eastwood.util))), which is a prefix list with no libspecs.
    • :require libspecs containing option keys other than :as and :refer (unless :refer is also present, in which case :exclude and :rename are allowed).
    • :use libspecs containing option keys other than :as, :refer, :exclude, :rename, or :only.
    • Option keys followed by values of the wrong type (e.g., :refer not followed by a list of symbols or :all).

    Note: References enclosed in square brackets (vectors) are permitted if they are part of a prefix list, as tools.namespace and Clojure recognize them.

  9. Avoid reflection with `:unused-meta-on-macro`

    master

    The :unused-meta-on-macro linter warns when metadata (like type hints) is applied to a macro invocation that will be discarded by Clojure upon expansion. This often leads to confusing reflection warnings from the Clojure compiler.

    Common scenarios where metadata is lost:

    • Constructor calls: (ClassName. args)
    • Most other macro expansions.

    Scenarios where metadata is preserved:

    • (new ClassName args)
    • Calls beginning with a . (e.g., (. x close))
    • Class method calls, field access, and instance method/field access (these preserve :tag type hints).

    Workaround: To avoid reflection without triggering the linter, bind the macro invocation result to a symbol using let and type hint that symbol instead.

    ;; Instead of: (.close ^Writer (my-macro (StringWriter.)))
    ;; Use:
    (let [^Writer w (my-macro (StringWriter.))]
      (.close w))
  10. Use the :unlimited-use linter to manage namespace imports

    master

    The :unlimited-use linter warns against using use for namespaces like clojure.string because it can lead to symbol shadowing and makes it difficult to track the origin of symbols.

    Best Practices:

    • Use require with :refer to explicitly list symbols.
    • Use :as to create an alias (e.g., :as str) to prefix symbols.

    Exceptions:

    • The linter ignores 'limited' use statements that use :only or :refer.
    • The linter never warns about clojure.test in test files, as unlimited use is common and generally considered harmless there.
    (ns my.namespace
      (:require [clojure.string :as str :refer [replace join]]))
  11. How Eastwood determines the options map

    master

    The final options map used by Eastwood is constructed differently depending on how it is invoked.

    From Leiningen CLI

    The options map is built in three stages:

    1. Leiningen Profile Merging: Leiningen merges :eastwood keys from various sources (top-level defproject, :system profile, :user profile, and :dev profile). Collections like vectors and sets are combined, and maps are merged recursively.
    2. Path Injection: Eastwood merges a map containing :source-paths and :test-paths calculated by Leiningen.
    3. Command Line Overrides: Any options provided directly via the CLI override previous values.

    From a REPL

    When calling eastwood.lint/eastwood or lint, the provided options map is augmented with defaults for:

    • :cwd: The current working directory.
    • :linters: All linters enabled by default.
    • :namespaces: Defaults to [:source-paths :test-paths].
    • :source-paths: If not provided, defaults to all directories on the Java classpath.
    • :callback: A default function that prints messages to *out* (or a file specified by :out).
  12. Use the :constant-test linter

    master

    The :constant-test linter warns when a test condition is a compile-time constant that will always evaluate to true or false.

    Examples of flagged code:

    (if false 1 2)
    (if-not [nil] 1 2)
    (when-first [x [1 2]] (println "Goodbye"))