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*
- all-files
- code-files
- code-path
- exclude
- file-lookup
- file-suffix
- file-type
- get-path
- in-context
- lookup-ns
- lookup-path
- matching-root
- parse-ns-name
- project
- project-file
- project-lookup
- project-map
- project-name
- relative-root-path
- relativize-lookup
- source-ns
- sym-name
- test-ns
- test-root
- test-suffix
all-files ^
[] [paths] [paths opts] [paths opts project]
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")
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"})
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"
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}
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")
v 3.0
(defn file-suffix
([] (file-suffix common/*type*))
([type]
(-> (common/type-lookup type) :extension)))
link
(file-suffix) => ".clj" (file-suffix :cljs) => ".cljs"
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
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"
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
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
v 3.0
(defn lookup-path
([ns]
(get @common/*lookup* ns)))
link
(str (lookup-path (env/ns-sym))) => string?
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]
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
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?})
v 3.0
(defn project-file
([]
(->> [lein/*project-file*
shadow/*shadow-file*]
(filter fs/exists?)
(first))))
link
(project-file) => "project.clj"
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?
v 3.0
(defn project-name
([] (:name (project)))
([path]
(:name (project path))))
link
(project-name) => symbol?
NONE
(defn- relative-root-path
([path project]
(when (and path (:root project))
(str (fs/relativize (:root project) path)))))
link
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
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
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
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]
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"
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?
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")