code.project

Project metadata and file lookup.

code.project normalizes project files, source paths, lookup tables, and dependency metadata so tools can work across source, test, and language directories.

1    Motivation

Most higher-level tools need to know where namespaces live. code.project builds that lookup from project metadata and abstracts over Leiningen and Shadow project shapes.

2    Internal usage

code.framework, code.manage, code.doc, and build tooling all use project lookups to resolve namespaces to files. This is also what lets documentation pages reference APIs from src, src-lang, and tests consistently.

3    Walkthrough

3.1    Reading project metadata

project loads the current project map, and project-name returns the project symbol. These are the foundation for every path lookup and namespace scan.

load the current project

(project/project)
=> (contains {:name symbol?
              :dependencies vector?})

get the project name

(project/project-name)
=> symbol?

3.2    Resolving namespaces to files

file-lookup builds a map of namespace to file path, file-type distinguishes source from test files, and source-ns/test-ns convert between the two conventions.

look up namespace files

(-> (project/file-lookup (project/project))
    (get 'code.project))
=> #(.endsWith ^String % "/src/code/project.clj")

distinguish source and test files

(project/file-type "project.clj")
=> :source

(project/file-type "test/code/project_test.clj")
=> :test

convert between source and test namespaces

(project/source-ns 'a-test)
=> 'a

(project/test-ns 'a)
=> 'a-test

3.3    End-to-end: scan all test files

all-files walks configured project roots and returns every Clojure file it finds, keyed by namespace.

collect test files in the project

(-> (project/all-files ["test"])
    (get 'code.project-test))
=> #(.endsWith ^String % "/test/code/project_test.clj")

4    API



*include* ^

NONE
(def ^:dynamic *include* [".clj$"])
link

all-files ^

[] [paths] [paths opts] [paths opts project]
Added 3.0

returns all the clojure files in a directory

v 3.0
(defn all-files
  ([] (all-files ["."]))
  ([paths] (all-files paths {}))
  ([paths opts]
   (all-files paths opts (project)))
  ([paths opts project]
   (let [filt (-> {:include *include*}
                  (merge opts)
                  (update-in [:exclude]
                             conj
                             fs/link?))
         result (->> paths
                     (map #(fs/path (:root project) %))
                     (mapcat #(fs/select % filt))
                     (pmap lookup-ns)
                     (into {}))]
     (dissoc result nil))))
link
(count (all-files ["test"])) => number? (-> (all-files ["test"]) (get 'code.project-test)) => #(.endsWith ^String % "/test/code/project_test.clj")

code-files ^

[]
Added 3.0

returns only the code files for the current project

v 3.0
(defn code-files
  ([]
   (let [project (project)]
     (relativize-lookup (file-lookup project) project))))
link
(code-files) => (contains {'code.project any 'js.react-native.physical-addon "src-lang/js/react_native/physical_addon.clj" 'xt.db.helpers.seed-user-test "test-lang/xt/db/helpers/seed_user_test.clj"})

code-path ^

[ns relative]
Added 3.0

returns the path of the code

v 3.0
(defn code-path
  ([ns relative]
   (let [project (project)
         path (or (get @common/*lookup* ns)
                  (get (file-lookup project) ns)
                  (throw (ex-info "Namespace does not exist" {:ns ns})))]
     (if relative
       (or (relative-root-path path project)
           (str (fs/relativize (fs/path ".") path)))
       path))))
link
(str (code-path (env/ns-sym) true)) => "test/code/project_test.clj"

exclude ^

[lookup exclusions]
Added 3.0

helper function for excluding certain namespaces

v 3.0
(defn exclude
  ([lookup exclusions]
   (reduce-kv (fn [out ns v]
                (let [nss (str ns)
                      exclude? (->> exclusions
                                    (map (fn [ex]
                                           (.startsWith nss ex)))
                                    (some true?))]
                  (if exclude?
                    out
                    (assoc out ns v))))
              {}

              lookup)))
link
(exclude '{lucid.legacy.analyzer :a lucid.legacy :a lib.aether :b} ["lucid.legacy"]) => '{lib.aether :b}

file-lookup ^

[] [project]
Added 3.0

creates a lookup of namespaces and files in the project

v 3.0
(defn file-lookup
  ([] (file-lookup (project)))
  ([project]
   (all-files (concat (:source-paths project)
                      (:test-paths project))
              {}
              project)))
link
(-> (file-lookup (project)) (get 'code.project)) => #(.endsWith ^String % "/src/code/project.clj")

file-suffix ^

[] [type]
Added 3.0

returns the file suffix for a given type

v 3.0
(defn file-suffix
  ([] (file-suffix common/*type*))
  ([type]
   (-> (common/type-lookup type) :extension)))
link
(file-suffix) => ".clj" (file-suffix :cljs) => ".cljs"

file-type ^

[path]
Added 3.0

returns the type of file according to the suffix

v 3.0
(defn file-type
  ([path]
   (cond (.endsWith (str path)
                    (str (munge (test-suffix))
                         (file-suffix)))
         :test

         :else
         :source)))
link
(file-type "project.clj") => :source (file-type "test/code/project_test.clj") => :test

get-path ^

[ns & [project]]
Added 4.1

uses configured project roots when resolving namespaces

v 4.1
(defn get-path
  ([ns & [project]]
   (let [project-map (or project (code.project/project))
         roots       (concat
                      (or (:source-paths project-map) ["src"])
                      (or (:test-paths project-map)   ["test"]))]
     (or (get @common/*lookup* ns)
         (some (fn [root]
                 (let [candidate (str root "/" (fs/ns->file ns) ".clj")]
                   (when (fs/exists? candidate)
                     (second (lookup-ns candidate)))))
               roots)))))
link
(get-path 'js.react-native.physical-addon) => "src-lang/js/react_native/physical_addon.clj" (get-path 'xt.db.helpers.seed-user-test) => "test-lang/xt/db/helpers/seed_user_test.clj"

in-context ^

[[func & args]]
Added 3.0

creates a local context for executing code functions

v 3.0
(defmacro in-context
  ([[func & args]]
    (let [project `(project)
          lookup  `(all-files (concat (:source-paths ~'project)
                                      (:test-paths ~'project))
                              {}
                              ~'project)
          current `(.getName *ns*)
          params  `{}]
      (case (count args)
        0  `(let [~'project ~project]
              (~func ~current ~params ~lookup ~'project))
       1   (if (map? (first args))
             `(let [~'project ~project]
                (~func ~current ~(first args) ~lookup ~'project))
             `(let [~'project ~project]
                (~func ~(first args) ~params ~lookup ~'project)))
       2   `(let [~'project ~project]
              (~func ~@args ~lookup ~'project))))))
link
(in-context ((fn [current params _ project] current))) => 'code.project-test

lookup-ns ^

[path]
Added 3.0

fast lookup for all-files function

v 3.0
(invoke/definvoke lookup-ns
  [:recent {:key str
            :compare fs/last-modified}]
  ([path]
   (when-let [ns (fs/file-namespace path)]
     (swap! common/*lookup* assoc ns path)
     [ns (str path)])))
link
(first (lookup-ns (get-path (env/ns-sym)))) => 'code.project-test

lookup-path ^

[ns]
Added 3.0

looks up the path given the `ns`

v 3.0
(defn lookup-path
  ([ns]
    (get @common/*lookup* ns)))
link
(str (lookup-path (env/ns-sym))) => string?

matching-root ^

[path roots project]

NONE
(defn- matching-root
  ([path roots project]
   (let [^String relative (relative-root-path path project)]
     (when relative
       (->> roots
            (filter (fn [root]
                      (let [^String root (str root)]
                        (or (= relative root)
                            (.startsWith relative (str root "/"))))))
            (sort-by count >)
            first)))))
link

parse-ns-name ^

[forms] [forms file-path]
Added 4.1

parses namespace declarations from forms with an optional file fallback

v 4.1
(defn parse-ns-name
  ([forms]
   (parse-ns-name forms nil))
  ([forms file-path]
   (or (some (fn [form]
               (when (and (seq? form)
                          (= 'ns (first form))
                          (symbol? (second form)))
                 (second form)))
             forms)
       (some-> file-path
               fs/file-namespace)
       (some-> file-path
               str
               (str/replace #"^.*(?:src|test)/" "")
               (str/replace #".clj[csx]?$" "")
               (str/replace #"/" ".")
               (str/replace #"_" "-")
               symbol))))
link
(parse-ns-name '[(comment x) (ns sample.core) (def x 1)]) => 'sample.core (parse-ns-name '[(comment x)] "src/code/project.clj") => 'code.project

project ^

[] [path]
Added 3.0

returns project options as a map

v 3.0
(defn project
  ([] (project (project-file)))
  ([path]
   (cond (nil? path)
         (throw (ex-info "Cannot find project" {:path nil}))

         (io/input-stream? path)
         (lein/project path)

         :else
         (project-map (fs/path path)))))
link
(project) => (contains {:name symbol? :dependencies vector?})

project-file ^

[]
Added 3.0

returns the current project file

v 3.0
(defn project-file
  ([]
   (->> [lein/*project-file*
         shadow/*shadow-file*]
        (filter fs/exists?)
        (first))))
link
(project-file) => "project.clj"

project-lookup ^

NONE
(def project-lookup)
link

project-map ^

[path]
Added 3.0

returns the project map

v 3.0
(invoke/definvoke project-map
  [:recent {:key str
            :compare fs/last-modified}]
  ([path]
   ((project-lookup (str (fs/file-name path)) path))))
link
(project-map (fs/path "project.clj")) => map?

project-name ^

[] [path]
Added 3.0

returns the name, read from the project map

v 3.0
(defn project-name
  ([] (:name (project)))
  ([path]
   (:name (project path))))
link
(project-name) => symbol?

relative-root-path ^

[path project]

NONE
(defn- relative-root-path
  ([path project]
   (when (and path (:root project))
     (str (fs/relativize (:root project) path)))))
link

relativize-lookup ^

[lookup project]

NONE
(defn- relativize-lookup
  ([lookup project]
   (reduce-kv (fn [out ns path]
                (assoc out ns
                       (or (relative-root-path path project)
                           (str path))))
              {}
              lookup)))
link

source-ns ^

[ns]
Added 3.0

returns the source namespace

v 3.0
(defn source-ns
  ([ns]
   (let [sns (str (sym-name ns))
         suffix (test-suffix)
         sns (if (.endsWith (str sns) suffix)
               (subs sns 0 (- (count sns) (count suffix)))
               sns)]
     (symbol sns))))
link
(source-ns 'a) => 'a (source-ns 'a-test) => 'a

sym-name ^

[x]
Added 3.0

returns the symbol of the namespace

v 3.0
(defn sym-name
  ([x]
   (cond (instance? clojure.lang.Namespace x)
         (.getName ^clojure.lang.Namespace x)

         (symbol? x)
         x

         :else
         (throw (ex-info "Only symbols and namespaces are supported" {:type (type x)
                                                                      :value x})))))
link
(sym-name *ns*) => 'code.project-test (sym-name 'a) => 'a

test-ns ^

[ns]
Added 3.0

returns the test namespace

v 3.0
(defn test-ns
  ([ns]
   (let [sns (str (sym-name ns))
         suffix (test-suffix)
         sns (if (.endsWith (str sns) suffix)
               sns
               (str sns suffix))]
     (symbol sns))))
link
(test-ns 'a) => 'a-test (test-ns 'a-test) => 'a-test

test-root ^

[path project]
Added 4.1

returns the preferred test root for a path within the project

v 4.1
(defn test-root
  ([path project]
   (let [test-paths   (or (:test-paths project) ["test"])
         current-test (matching-root path test-paths project)
         source-root  (matching-root path (:source-paths project) project)
         source-test  (when source-root
                        (let [candidate (str/replace source-root #"^src" "test")]
                          (when ((set test-paths) candidate)
                            candidate)))]
     (or current-test
         source-test
         (first test-paths)))))
link
(test-root "test/code/project_test.clj" {:root "." :source-paths ["src" "src-lang"] :test-paths ["test" "test-lang"]}) => "test" (test-root "src/code/project.clj" {:root "." :source-paths ["src" "src-lang"] :test-paths ["test" "test-lang"]}) => "test" (test-root "src-lang/js/react_native/physical_addon.clj" {:root "." :source-paths ["src" "src-lang"] :test-paths ["test" "test-lang"]}) => "test-lang" (test-root "foo/bar.clj" {:root "." :source-paths ["src"] :test-paths ["test" "test-lang"]}) => "test"

test-suffix ^

[] [s]
Added 3.0

returns the test suffix

v 3.0
(defn test-suffix
  ([] common/*test-suffix*)
  ([s] (alter-var-root #'common/*test-suffix*
                       (constantly s))))
link
(test-suffix) => "-test"


*lookup* ^

NONE
(defonce ^:dynamic *lookup* (atom {}))
link

*memory* ^

NONE
(defonce ^:dynamic *memory* (atom {}))
link

*test-suffix* ^

NONE
(def ^:dynamic *test-suffix*)
link

*type* ^

NONE
(def ^:dynamic *type* :clj)
link

+defaults+ ^

NONE
(def +defaults+)
link

artifact ^

[full]
Added 3.0

returns the artifact map given a symbol

v 3.0
(defn artifact
  ([full]
   (let [group    (or (namespace full)
                      (str full))
         artifact (name full)]
     {:name full
      :artifact artifact
      :group group})))
link
(artifact 'hara/hara) => '{:name hara/hara, :artifact "hara", :group "hara"}

type-lookup ^

NONE
(def type-lookup)
link


*project-file* ^

NONE
(def ^:dynamic *project-file*)
link

project ^

[] [project-file]
Added 3.0

returns the root project map

v 3.0
(defn project
  ([] (project *project-file*))
  ([project-file]
   (let [proj-form (-> project-file
                       slurp
                       (#(str "[" % "]"))
                       read-string
                       (->> (filter (fn [form]
                                      (and (list? form)
                                           (= (first form) 'defproject)))))
                       first)
         root  (-> project-file fs/path fs/parent str)
         [_ full version] (take 3 proj-form)
         entry (common/artifact full)
         proj  (-> (apply hash-map (drop 3 proj-form))
                   (merge entry
                          {:version version
                           :root root})
                   (->> (merge common/+defaults+)))]
     proj)))
link
(project) => map?

project-name ^

[] [project-file]
Added 3.0

returns the project name

v 3.0
(defn project-name
  ([] (project-name *project-file*))
  ([project-file]
   (:name (project project-file))))
link
(project-name) => symbol?


*shadow-file* ^

NONE
(def ^:dynamic *shadow-file*)
link

project ^

[] [shadow-file]
Added 3.0

opens a shadow.edn file as the project

v 3.0
(defn project
  ([] (project *shadow-file*))
  ([shadow-file]
   (if-let [proj (-> shadow-file
                     slurp
                     read-string)]
     (let [root    (-> shadow-file fs/path fs/parent str)
           proj (-> proj
                    (assoc :root root)
                    (merge (common/artifact (:name proj)))
                    (->> (merge common/+defaults+)))]
       proj))))
link
(project "../yin/shadow-cljs.edn")