Skip to content

Instantly share code, notes, and snippets.

@MrEbbinghaus
Last active November 16, 2023 23:17
Show Gist options
  • Select an option

  • Save MrEbbinghaus/a890f93906b499fba72d1ff508791e2d to your computer and use it in GitHub Desktop.

Select an option

Save MrEbbinghaus/a890f93906b499fba72d1ff508791e2d to your computer and use it in GitHub Desktop.
Generate a graph of all symbols in your project from clj-kondos analyis output
(ns graph
(:require
[clj-kondo.core :as kondo]
[clojure.string :as str]
[dorothy.core :as dot]))
(def kondo-output
(kondo/run! {:lint ["/Users/bjebb/Development/decide3/src/main/decide"]
:config {:output {:analysis true}
:lint-as
'{com.fulcrologic.guardrails.core/>def clojure.core/def
com.fulcrologic.guardrails.core/>defn clojure.core/defn
com.fulcrologic.guardrails.core/>defn- clojure.core/defn-
com.wsscode.pathom.connect/defmutation clojure.core/defn
com.wsscode.pathom.connect/defresolver clojure.core/defn}}}))
(defn init-graph [var-definitions]
(let [vars (map (juxt :ns :name) var-definitions)]
(reduce (fn [graph var] (assoc graph var #{})) {} vars)))
(defn start-with-some? [s substr-set]
(some #(str/starts-with? s %) substr-set))
(defn depend [m [dependant dependency]]
(cond
(= dependency dependant) m
(contains? m dependant) (update m dependant conj dependency)
:else (assoc m dependant #{dependency})))
(def built-in (complement :to))
(def top-level (complement :from-var))
(defn dep-graph
([analysis] (dep-graph analysis {:ignore-prefix #{"clojure" "cljs" "com.fulcrologic" "mui." "@mui" "com.wsscode.pathom.connect"}
:remove-langs #{:cljs}}))
([analysis {:keys [ignore-prefix remove-langs]}]
(let [{:keys [var-usages var-definitions]} analysis
ignored-ns-prefix #(start-with-some? % ignore-prefix)
_ (printf "Variable definitions: %d\n" (count var-definitions))
_ (printf "Number of variable usages: %d\n" (count var-usages))
var-definitions
(->> var-definitions
(remove #(some-> % :file (str/ends-with? ".cljs")))
(remove (complement :ns))
(remove #(remove-langs (:lang %)))
(remove (comp ignored-ns-prefix :ns)))
var-def-set (set (map (juxt :ns :name) var-definitions))
var-usages
(cond->> var-usages
:always (filter :name) ; no ns declerations
:always (remove built-in)
:always (remove #(some-> % :file (str/ends-with? ".cljs")))
:always (remove #(#{:clj-kondo/unknown-namespace} (:to %)))
remove-langs (remove #(remove-langs (:lang %)))
ignore-prefix (remove (comp ignored-ns-prefix :to)))
empty-deps-graph (init-graph var-definitions)]
(printf "Number of elements to squash: %d\n" (count var-usages))
(->> var-usages
(filter #(contains? var-def-set ((juxt :to :name) %)))
(map (juxt (juxt :from :from-var) (juxt :to :name)))
(reduce depend empty-deps-graph)))))
(defn escape [s]
(str/escape (str s) {\" ""}))
(defn fqvec->str [fqvec]
(escape (str/join "/" (remove nil? fqvec))))
;; => #'graph/fqvec->str
(defn edges [[var depends-on]]
(let [dep->edge #(vector (fqvec->str var) (fqvec->str %))]
(mapv dep->edge depends-on)))
(defn hash-color
"Return a #12DE32 hex color for a given string."
[s]
(let [color-number (-> s hash (bit-and 0xFFFFFF))]
(format "#%06x" color-number)))
(defn namespace->subgraph [[namespace vars]]
(mapcat edges vars)
(dot/subgraph {:id (str namespace)
:color (hash-color (str namespace))}
(mapcat edges vars)))
(defn var->node [var]
[(fqvec->str var) {:label (fqvec->str var)
:shape "rectangle"
:fontcolor "black"
:group (escape (first var))
:color (hash-color (escape (first var)))}])
(defn dep-graph->dot [dep-graph]
(let [nodes (keys dep-graph)
grouped-by-ns (group-by ffirst dep-graph)]
(dot/dot
(dot/digraph
{}
(concat
(map var->node nodes)
(map namespace->subgraph grouped-by-ns))))))
(defn -main []
(let [{:keys [analysis]} kondo-output]
(->> analysis
dep-graph
dep-graph->dot
(spit "var-graph.dot"))))
(comment
(time (-main))
)
{:paths ["."]
:deps {org.clojure/tools.namespace {:mvn/version "1.1.0"}
clj-kondo/clj-kondo {:mvn/version "2021.10.19"}
dorothy/dorothy {:mvn/version "0.0.7"}}}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment