Solving version conflicts with lein-pedantic

A couple months ago I wrote do not use version ranges in project.clj. While this helps reduce confusion about what version of dependencies a project will use, it does not eliminate all sources of confusion.

Different dependency versions

(defproject sample "0.0.1"
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [com.cemerick/friend "0.0.9"]
                 [noir "1.3.0-beta9"]])

This project graph has two versions of ring/ring-core that it wants to use. com.cemerick/friend wants [ring/ring-core "1.0.2"] and noir wants [ring/ring-core "1.1.0"] The dependency resolution in lein/maven/aether will end up choosing the one closest to the project root, which in this case is [ring/ring-core "1.0.2"]. This causes a problem because noir wants to use ring.middleware.head which was added in 1.1.0.

(defproject sample "0.0.1"
  :dependencies [[org.clojure/clojure "1.4.0"]
                 [ring/ring-core "1.0.2"]
                 [noir "1.3.0-beta9"]])
                 

A similar problem can occur for top level dependencies.

Introducing lein-pedantic

To help solve these problems, I’ve written lein-pedantic. It is a plugin for leiningen that will reject dependency graphs with common errors that would be confusing for users. In addition, it tries to provide a helpful command to use to fix the issue. For example, the noir and com.cemerick.friend issue would result in:

Failing dependency resolution because:

[com.cemerick/friend "0.0.9"] -> [ring/ring-core "1.0.2"]
  is overruling
[noir "1.3.0-beta9"] -> [compojure "1.0.4"] -> [ring/ring-core "1.1.0"]
  
Please use [com.cemerick/friend "0.0.9" :exclusions [ring/ring-core]] to get
[ring/ring-core "1.1.0"] or use [noir "1.3.0-beta9" :exclusions [ring/ring-core]] to get
[ring/ring-core "1.0.2"].

It is designed to use leiningen’s hook system, and only requires being added to the :plugins vector. No extra tasks are added, it just runs anytime leiningen attempts to resolve dependencies.

It works by determining the dependency graph for each of the project’s dependencies on their own, and comparing them to the collective dependency graph. It will reject the graph if

  1. A top level dependency is overruled by another version.
  2. A transitive dependency is overruled by an older version.

Together, these help identify most of the problems “soft/recommended” dependencies cause. In addition, lein-pedantic does not change the dependency resolution rules, so the final dependencies put in project.clj will work the same in maven or another aether based system.