Lint your Clojure programs as they run.
Dynalint is a simplistic linter that wraps operations in runtime checks. It is designed to never change behaviour, except for throwing different kinds of exceptions for bad input.
Preserving performance is not a goal of Dynalint, but it will always preserve type hints.
Designed for Clojure 1.5.1, but has been modified so it can be tried with Clojure 1.6.0, too.
Dynalint's design is intrinsically prone to cyclic calling. If you get a StackOverflowException where a Dynalint namespace is obviously to blame, please stop using Dynalint for your project and file an issue.
Dynalint should only be used at dev time. You can start it manually
in a REPL, or in a Leiningen project you may modify your project.clj
file to start it automatically for you.
No matter how you start Dynalint, it is enabled by calling its lint
function. lint
adds dynamic checks to vars and their inlinings.
If you want to check a var's inlining or a macro's expansion, relevant
forms must be compiled after calling lint
.
Add this to your project's dependencies:
[com.ambrosebs/dynalint "0.1.3"]
If you want to use Dynalint at the REPL, call dynalint.lint/lint
.
(require '[dynalint.lint :as dyn])
(dyn/lint)
If you want Dynalint to be automatically enabled every time you run
lein test
or lein repl
, here is a minimal project.clj
file
example that can achieve that:
(defproject testdyna "0.1.0"
;; ... other stuff here ...
:profiles {:dev {:dependencies [[com.ambrosebs/dynalint "0.1.3"]]
:injections [(require 'dynalint.lint)
(dynalint.lint/lint)]}}
;; ... other stuff here ...
)
If you move the :injections
keyword and its value to a :test
profile, Dynalint will be enabled near the beginning of lein test
,
but not lein repl
.
Change (dynalint.lint/lint)
to (dynalint.lint/lint :start-message true)
if you want to enable a 1-line startup message to be printed to
*out*
when dynalint.lint/lint
is called, to verify that it is
being called in your project.
Other options that can be supplied to lint
:
:log-file "filename"
- Dynalint will create the directoryfilename
, delete the file, and then write to it the:warning-interval 0.1
- Specify the minimum time in seconds that0
or nil
0
nor nil
, the warnings printed by Dynalint can change:enable
- The value can be a single keyword :all
, :error
, or:warn
, or a sequence of those keywords. By default, both :error
:warn
are enabled.:disable
- The values are the same as for :enable
above.The following example enables the start message and logging to file
dynalint-log.txt
. It disables the minimum time interval between
warnings, thus showing all of them, no matter how close in time they
occur.
(defproject testdyna "0.1.0"
;; ... other stuff here ...
:profiles {:dev {:dependencies [[com.ambrosebs/dynalint "0.1.4-SNAPSHOT"]]
:injections [(require 'dynalint.lint)
(dynalint.lint/lint
:start-message true
:log-file "dynalint-log.txt"
:warning-interval nil)]}}
;; ... other stuff here ...
)
For one reason or another, many core Clojure functions have bad error messages. Here's one example.
user=> (first 1)
IllegalArgumentException Don't know how to create ISeq from: java.lang.Long clojure.lang.RT.seqFrom (RT.java:505)
user=> (clojure.repl/pst)
IllegalArgumentException Don't know how to create ISeq from: java.lang.Long
clojure.lang.RT.seqFrom (RT.java:505)
clojure.lang.RT.seq (RT.java:486)
clojure.lang.RT.first (RT.java:578)
clojure.core/first (core.clj:55)
user/eval52988 (NO_SOURCE_FILE:1)
clojure.lang.Compiler.eval (Compiler.java:6619)
clojure.lang.Compiler.eval (Compiler.java:6582)
clojure.core/eval (core.clj:2852)
clojure.main/repl/read-eval-print--6588/fn--6591 (main.clj:259)
clojure.main/repl/read-eval-print--6588 (main.clj:259)
clojure.main/repl/fn--6597 (main.clj:277)
clojure.main/repl (main.clj:277)
The error originates from passing bogus arguments to first
, yet we get an error
deep in first
's implementation. This is simply pragmatism as such error checking would
incur a performance penalty, but sometimes we just want a better error above all else.
After running (dynalint.lint/lint)
we can achieve better errors.
user=> (require '[dynalint.lint :as dyn])
nil
user=> (dyn/lint)
:ok
user=> (first 1)
ExceptionInfo ERROR (Dynalint id 1): First argument to clojure.core/first must be seqable: 1 (instance of class java.lang.Long) dynalint.lint/error (lint.clj:63)
user=> (clojure.repl/pst)
ExceptionInfo ERROR (Dynalint id 1): First argument to clojure.core/first must be seqable: 1 (instance of class java.lang.Long) {:dynalint.lint/dynalint true, :dynalint.lint/error true, :dynalint.lint/id 1}
dynalint.lint/error (lint.clj:63)
clojure.core/first (core.clj:49)
clojure.lang.AFunction$1.doInvoke (AFunction.java:29)
user/eval2098 (NO_SOURCE_FILE:1)
clojure.lang.Compiler.eval (Compiler.java:6619)
clojure.lang.Compiler.eval (Compiler.java:6582)
clojure.core/eval (core.clj:2852)
clojure.main/repl/read-eval-print--6588/fn--6591 (main.clj:259)
clojure.main/repl/read-eval-print--6588 (main.clj:259)
clojure.main/repl/fn--6597 (main.clj:277)
clojure.main/repl (main.clj:277)
clojure.tools.nrepl.middleware.interruptible-eval/evaluate/fn--660 (interruptible_eval.clj:56)
Dynalint tries to cover its tracks in the stack trace. Notice it seems dynalint.lint/error
is directly called by clojure.core/first
. This is not really the case, but the stack trace
is transformed to be as understandable as possible.
If you happen to get a stack trace not thrown
by dynalint, you will notice calls to strange looking functions like
dynalint.lint$clojure.core_SLASH_first$wrapper__8482
. This should be interpretted as dynalint's
wrapper for clojure.core/first
, and for most purposes exactly the same as a stack entry calling
clojure.core/first
.
Use dynalint.lint/configure-linting!
to customise your linting experience. Note that
dynalint.lint/lint
also takes the same arguments, but should only usually be run once.
The eventual goal is to have Dynalint modifications for all functions
and macros in Clojure, even if the only change is to give a different
warning for the wrong number of arguments (e.g. clojure.core/meta
,
clojure.core/number?
, and others).
Dynalint should give an error only when the original function/macro would throw an exception, or is known to go into an infinite loop. For all other conditions, Dynalint issues a warning.
Please open a PR or an issue and we can discuss.
Copyright © 2014-2024 Ambrose Bonnaire-Sergeant
Distributed under the Eclipse Public License, the same as Clojure.