Deprecation warnings in Jaunt

This week I’m working on getting Jaunt’s 1.9.0 release nearer the door, on which note I’m happy to properly demo one the features of this first release: deprecation warnings.

A bare REPL will do for these demos.

$ cd $(mktemp -d)
$ wget https://clojars.org/repo/org/jaunt-lang/jaunt/1.9.0-RC4/jaunt-1.9.0-RC4.jar
$ java -jar jaunt-1.9-RC4.jar

Jaunt #2 (not my best ticket ever in terms of hygiene) was the first work I did on what became Jaunt and was really pretty critical for getting my teeth into the project and validating that there were tractable incremental improvements to be made over Clojure.

This change introduced four new switches to the Jaunt compiler, the most interesting of which is :warn-on-deprecated which enables or disables compiler warnings in support the use of ^:deprecated metadata on functions and namespaces. :warn-on-deprecated is on (true) by default, although it can be disabled globally with the JVM system property clojure.compiler.warn-on-deprecated=false, or by set!ing clojure.core/*compiler-options* say to have (set! *compiler-options* (dissoc *compiler-options* :warn-on-deprecated)) in a namespace where you want these checks to be disabled.

The semantics of deprecation are simple:

Vars and namespaces may become deprecated in any SemVer minor version or major version, but may only legally be deleted only on a major version.

Deprecation warnings are only emitted when a deprecated namespace or definition is accessed from a context which is not deprecated. Deprecated contexts are namespaces which are deprecated, and the bodies of deprecated definitions.

So for instance, in the following code:

(defn ^:deprecated bad-idea [x]
  (println "Oh noez!")
  (throw (new Exception "Bad code is bad")))
;; => #'user/bad-idea

(defn ^:deprecated also-bad-idea [x]
  (bad-idea x))
;; => #'user/also-bad-idea

(defn almost-better-idea [x]
  (bad-idea x))
;; Warning: using deprecated var: #'user/bad-idea ...
;; => #'user/also-bad-idea

The definition of bad-idea itself is innocuous. Simply evaluating deprecated definitions is fine. Warning that also-bad-idea uses bad-idea would be superfluous, because that fact is innocuous so long as neither is used. Suppressing warnings in this case allows Jaunt to load arbitrarily much deprecated code without drowning a user with false positive warnings.

Users should only have to care about deprecation at the edge between current code and deprecated code. Thus compiling almost-better-idea will emit a warning that it makes use of bad-idea, since almost-better-idea is not itself deprecated. Likewise at the repl invoking either deprecated function would also generate a warning.

As to namespaces, similar principles apply. Consider the two namespaces and trace:

(ns ^:deprecated com.proj.code)
;; => nil

(def some-const 3)
;; => #'com.proj.code/some-const

;;------------------------------------------------------------------------
(ns com.proj.new-code
  (:require [com.proj.code :as old :refer [some-const]]))
;; Warning: aliasing deprecated ns: com.proj.code  ...
;; Warning: referring deprecated var: #'com.proj.code/some-const ...
;; => nil

(def g 4)
;; => #'com.proj.new-code/g

(def h (+ old/some-const g))
;; Warning: using deprecated var: #'com.proj.code/some-const ...
;; => #'com.proj.new-code/h

As the whole com.proj.code namespace is deprecated, merely referencing it and requiring that it be loaded is cause for a warning. Should that namespace ever be removed, the require would break regardless of whether anything from the required namespace is used or not.

If symbols which are deprecated are referred into a namespace which is not deprecated, each one of those will generate a warning as well for the same reason. However Jaunt tries to be smart about this, suppressing such warnings if a (require ' [... :refer :all]) or a (use ...) directive has been issued which constitutes an indefinite referral.

The symbol com.proj.code/some-const, is deprecated by dint of being defined in a deprecated namespace. Consequently a warning is emitted when it is used outside of a deprecated context, here in the definition of com.proj.new-code/h.

One last demo, disabling warnings!

(ns com.proj.no-warnings)
;; => nil

(set! *compiler-options* 
  (dissoc *compiler-options* :warn-on-deprecated))
;; elided...

(def ^:deprecated a 3)
;; => #'com.proj.no-warnings/a

(def b (+ a 4))
;; => #'com.proj.no-warnings/b

There’s more than just this coming down the line in Jaunt, so if you’re interested check out the 1.9 CHANGELOG, watch the repo or stay tuned here for more demos. Issues, ideas and especially pull requests are welcome :D

^d