Home

Awesome

lein-typed

Type checking for Clojure with Clojure core.typed.

Installation

A core.typed dependency must be specified.

Use this for user-level plugins:

Put [lein-typed "0.4.6"] into the :plugins vector of your :user profile, or if you are on Leiningen 1.x do lein plugin install lein-typed 0.4.6.

Use this for project-level plugins:

Put [lein-typed "0.4.6"] into the :plugins vector of your project.clj.

Quickstart

For type checking, see lein typed check below.

For automatically generating types and specs, see lein typed infer-type and lein typed infer-spec.

Clojure Checking

To type check many namespaces, add namespaces to your project.clj like

:core.typed {:check [my.ns1 my.ns2]}

Then run

$ lein typed check 
...

To type check speific namespaces, provide one or more namespace symbols to check:

$ lein typed check typed-demo.core
Initializing core.typed ...
"Elapsed time: 5705.247176 msecs"
core.typed initialized.
Start collecting typed-demo.core
Finished collecting typed-demo.core
Collected 1 namespaces in 6039.963178 msecs
Start checking typed-demo.core
Checked typed-demo.core in 94.891391 msecs
Checked 1 namespaces (approx. 7 lines) in 6138.087383 msecs

ClojureScript Checking

To type check many ClojureScript namespaces, add namespaces to your project.clj like

:core.typed {:check-cljs [my.ns1 my.ns2]}

Then run

$ lein typed check-cljs
...

Requires core.typed 0.2.36 or later.

Type coverage

lein typed coverage is used the same as check, except information on type annotation coverage is given. Namespaces are not checked.

Requires core.typed version 0.2.3 or later.

$ lein typed coverage
Initializing core.typed ...
"Elapsed time: 4505.279024 msecs"
core.typed initialized.
Start collecting typed-demo.core
Finished collecting typed-demo.core
Collected 1 namespaces in 4619.570363 msecs
Checked 0 namespaces (approx. 0 lines) in 4621.945525 msecs
Start collecting typed-demo.nil
Finished collecting typed-demo.nil
Collected 1 namespaces in 20.48361 msecs
Checked 0 namespaces (approx. 0 lines) in 20.645363 msecs
Found 2 annotated vars out of 2 vars
100% var annotation coverage

Type and spec inference

lein typed infer-spec nsym and lein typed infer-type nsym infer clojure.spec specs and core.typed types respectively for namespace nsym. Keyword options are described below. A hybrid command lein typed infer-all nsym outputs both types and specs, but an output directory must be provided via :infer-opts "{:out-dir \"your-dir\"}" (two subfolders will be created, types and specs for the generated types and specs respectively).

Requires core.typed 0.4.0 or later, with Clojure 1.9.0-alpha17 or later.

Options:
:test-timeout-msRestricts each deftest to a specific time quanta before being aborted, in milliseconds (an integer). If the test suite is excessively slow during instrumentation, try lowering this option.
Default: No timeout
:test-selectorsA string containing a vector of arguments normally passed to lein test to narrow tests.
:infer-optsA string containing a map of options to be passed to clojure.core.typed/{runtime,spec}-infer after the :ns argument.
eg. :infer-opts "{:debug :all, :out-dir \"out\"}"
Default: No options.

This command first instruments the provided namespace before then running your test suite. It then summarizes runtime observations based on your test suite in either specs or types.

Note that these specs or types will reuse exist namespace require's. We recommend requiring and aliasing clojure.spec.alpha or clojure.core.typed as needed.

eg. (:require [[clojure.spec.alpha :as s]])

Example

Let's infer the specs for math.combinatorics. First we clone the repo.

git clone git@github.com:clojure/math.combinatorics.git
cd math.combinatorics

This is a Maven project, but we can easily convert it to Leiningen. Create a project.clj file in the project directory and paste:

(defproject org.clojure/math.combinatorics "0.0.1-SNAPSHOT"
  :source-paths ["src/main/clojure"]
  :test-paths ["src/test/clojure"]
  :dependencies [[org.clojure/clojure "1.9.0-alpha17"]
                 ;; insert latest clojure + core.typed versions
                 [org.clojure/core.typed "0.4.0"]
                 [org.clojure/test.check "0.9.0"]]
  ;; insert latest lein-typed version
  :plugins [[lein-typed "0.4.0"]])

Now let's run the tests and infer some specs. The math.combinatorics tests take a very long time with instrumentation, so we restrict each test to 3 seconds each.

So, execute the following in your terminal.

lein typed infer-spec clojure.math.combinatorics :test-timeout-ms 3000

Specs will be generated in the clojure.math.combinatorics source file between two special Start and End comments at the beginning of the file like this:

;; Start: Generated by clojure.core.typed - DO NOT EDIT
(s/def
  ::alias__17018
  (s/or
    :G__17805
    (partial instance? clojure.lang.IAtom)
    :char?
    char?
    :keyword?
    keyword?
    :int?
    int?))
(s/fdef
  all-different?
  :args
  (s/cat :s (s/coll-of ::alias__17018))
  :ret
  boolean?)
(s/fdef
  bounded-distributions
  :args
  (s/cat :m (s/coll-of int?) :t int?)
  :ret
  (s/coll-of (s/coll-of (s/tuple int? int? int?))))
...
...
;; End: Generated by clojure.core.typed - DO NOT EDIT

To generate core.typed types, run this command instead:

lein typed infer-type clojure.math.combinatorics :test-timeout-ms 3000

Notice that in addition to top-level annotation, inline annotations are also provided.

;; Start: Generated by clojure.core.typed - DO NOT EDIT
(declare)
(t/ann
  all-different?
  [(t/Coll (t/U t/Int (t/Atom1 t/Int) ':a ':b Character)) :-> Boolean])
(t/ann
  bounded-distributions
  [(t/Vec t/Int) t/Int :-> (t/Coll (t/Vec '[t/Int t/Int t/Int]))])
...
...
;; End: Generated by clojure.core.typed - DO NOT EDIT

...
(defn- multi-comb
  "Handles the case when you want the combinations of a list with duplicate items."
  [l t]
  (let [f (frequencies l),
        v (vec (distinct l)),
        domain (range (count v))
        m (vec (for ^{::t/ann t/Int} [^{::t/ann t/Int} i domain] (f (v i))))
        qs (bounded-distributions m t)]
    (for ^{::t/ann (t/Coll (t/U t/Int Character))} [^{::t/ann (t/Vec '[t/Int t/Int t/Int])} q qs]
      (apply concat
             (for ^{::t/ann (t/Coll (t/U t/Int Character))} [^{::t/ann '[t/Int t/Int t/Int]} [index this-bucket _] q]
               (repeat this-bucket (v index)))))))

Enjoy!

License

Copyright © 2017 Ambrose Bonnaire-Sergeant

Distributed under the Eclipse Public License, the same as Clojure.