Add package-map support for cross-repo SCIP navigation
SCIP Index / index (push) Successful in 1m35s

- Add --package-map CLI option to specify namespace-to-package mappings
- Update symbol format to include package coordinates for cross-repo nav
- Skip external_symbols for packages that will be indexed separately
- This enables Sourcegraph to resolve cross-repo references and show
  documentation from the external package's index

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 23:09:57 -05:00
parent 5ed5e61ab3
commit 768b8e8d29
3 changed files with 170 additions and 38 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
run: clojure -T:build compile-java run: clojure -T:build compile-java
- name: Generate SCIP index - name: Generate SCIP index
run: clojure -M:run -p . -o index.scip run: clojure -M:run -p . -o index.scip -m package-map.edn
env: env:
CLOJURE_LSP_PATH: /usr/local/bin/clojure-lsp CLOJURE_LSP_PATH: /usr/local/bin/clojure-lsp
+4
View File
@@ -0,0 +1,4 @@
;; Package map for cross-repository SCIP navigation
;; Maps namespace prefixes to package@version coordinates
{"scip-clojure" "io.github.ajet/scip-clojure@main"}
+165 -37
View File
@@ -16,8 +16,60 @@
:default "."] :default "."]
["-o" "--output FILE" "Output SCIP index file" ["-o" "--output FILE" "Output SCIP index file"
:default "index.scip"] :default "index.scip"]
["-m" "--package-map FILE" "Package map JSON file for cross-repo navigation"
:default nil]
["-h" "--help" "Show help"]]) ["-h" "--help" "Show help"]])
;; --- Package map support for cross-repo navigation ---
(defn load-package-map
"Load package map from EDN file.
Format: {\"ns-prefix\" \"package@version\", ...}
Example: {\"tui\" \"io.github.ajet/clojure-tui@main\"}
The ns-prefix is matched against namespace names. A prefix of 'tui'
matches 'tui.core', 'tui.render', etc.
Also supports JSON format for convenience."
[path]
(when path
(let [content (slurp path)
;; Try to parse as EDN first, fall back to simple JSON conversion
parsed (try
(edn/read-string content)
(catch Exception _
;; Simple JSON to EDN conversion for basic object format
(edn/read-string
(-> content
(str/replace #"\":\s*\"" "\" \"")
(str/replace #",\s*\"" " \"")
(str/replace #",\s*\}" "}")))))]
;; Ensure all keys and values are strings
(into {}
(map (fn [[k v]] [(str k) (str v)]))
parsed))))
(defn parse-package-spec
"Parse 'package@version' into [package version].
If no @ present, uses empty string for version."
[spec]
(let [parts (str/split spec #"@" 2)]
[(first parts) (or (second parts) "")]))
(defn find-package-for-ns
"Find the package spec for a namespace using the package map.
Returns [package version] or nil if not found.
Matches by longest prefix first."
[ns-sym package-map]
(when package-map
(let [ns-str (str ns-sym)
;; Sort by prefix length descending for longest match
sorted-prefixes (->> (keys package-map)
(sort-by count)
reverse)]
(when-let [matching-prefix (first (filter #(str/starts-with? ns-str %) sorted-prefixes))]
(parse-package-spec (get package-map matching-prefix))))))
(defn escape-identifier (defn escape-identifier
"Escape an identifier for SCIP symbol format. "Escape an identifier for SCIP symbol format.
Identifiers containing characters other than [_+-$a-zA-Z0-9] must be Identifiers containing characters other than [_+-$a-zA-Z0-9] must be
@@ -28,28 +80,55 @@
s s
(str "`" (str/replace s "`" "``") "`")))) (str "`" (str/replace s "`" "``") "`"))))
;; Dynamic var to hold package map during indexing
(def ^:dynamic *package-map* nil)
(defn make-symbol (defn make-symbol
"Create a SCIP symbol identifier from namespace and name. "Create a SCIP symbol identifier from namespace and name.
Format: scip-clojure clojure <namespace> . <name>. With package map: scip-clojure clojure <package> <version> <namespace>/<name>.
Uses namespace as package name for cross-repo navigation." Without package map: scip-clojure clojure <namespace> . <name>.
The package format enables cross-repo navigation by using Maven-style
coordinates that Sourcegraph can resolve to repositories."
[ns-sym var-name] [ns-sym var-name]
(format "scip-clojure clojure %s . %s." (if-let [[pkg version] (find-package-for-ns ns-sym *package-map*)]
(escape-identifier ns-sym) ;; With package map: use package coordinates
(escape-identifier var-name))) ;; Format: scheme manager package version descriptor
;; Descriptor uses namespace/ prefix for namespacing within package
(format "scip-clojure clojure %s %s %s/%s."
(escape-identifier pkg)
(escape-identifier (or version "."))
(escape-identifier ns-sym)
(escape-identifier var-name))
;; Without package map: fall back to namespace-only format
(format "scip-clojure clojure %s . %s."
(escape-identifier ns-sym)
(escape-identifier var-name))))
(defn make-ns-symbol (defn make-ns-symbol
"Create a SCIP symbol identifier for a namespace." "Create a SCIP symbol identifier for a namespace."
[ns-sym] [ns-sym]
(format "scip-clojure clojure %s . " (if-let [[pkg version] (find-package-for-ns ns-sym *package-map*)]
(escape-identifier ns-sym))) (format "scip-clojure clojure %s %s %s/"
(escape-identifier pkg)
(escape-identifier (or version "."))
(escape-identifier ns-sym))
(format "scip-clojure clojure %s . "
(escape-identifier ns-sym))))
(defn make-alias-symbol (defn make-alias-symbol
"Create a SCIP symbol identifier for a namespace alias. "Create a SCIP symbol identifier for a namespace alias.
The alias is scoped to the namespace where it's defined." The alias is scoped to the namespace where it's defined."
[from-ns alias-name] [from-ns alias-name]
(format "scip-clojure clojure %s . %s." (if-let [[pkg version] (find-package-for-ns from-ns *package-map*)]
(escape-identifier from-ns) (format "scip-clojure clojure %s %s %s/%s."
(escape-identifier alias-name))) (escape-identifier pkg)
(escape-identifier (or version "."))
(escape-identifier from-ns)
(escape-identifier alias-name))
(format "scip-clojure clojure %s . %s."
(escape-identifier from-ns)
(escape-identifier alias-name))))
(defn position->range (defn position->range
"Convert row/col positions to SCIP range format. "Convert row/col positions to SCIP range format.
@@ -161,27 +240,43 @@
(.setSymbol builder (make-ns-symbol name)) (.setSymbol builder (make-ns-symbol name))
(.build builder))) (.build builder)))
(defn ns-alias->occurrence (defn ns-alias-definition->occurrence
"Convert a namespace alias definition (e.g., :as ansi) to SCIP Occurrence. "Convert a namespace-usage with :as alias to SCIP Occurrence for alias definition.
Creates a Definition for the alias symbol, scoped to the defining namespace." Creates a Definition for the alias symbol, scoped to the defining namespace.
[{:keys [from alias] :as ns-alias}] E.g., [tui.ansi :as ansi] creates definition at 'ansi' position.
(let [builder (Scip$Occurrence/newBuilder)] Uses alias-row/alias-col positions from namespace-usages."
(.addAllRange builder (position->range ns-alias)) [{:keys [from alias alias-row alias-col alias-end-col] :as ns-usage}]
(.setSymbol builder (make-alias-symbol from alias)) (when (and alias alias-row alias-col)
(.setSymbolRoles builder (int Scip$SymbolRole/Definition_VALUE)) (let [builder (Scip$Occurrence/newBuilder)
(.build builder))) start-line (int (dec alias-row))
start-char (int (dec alias-col))
end-char (int (dec (or alias-end-col (+ alias-col (count (str alias))))))]
(.addAllRange builder [(int start-line) (int start-char) (int end-char)])
(.setSymbol builder (make-alias-symbol from alias))
(.setSymbolRoles builder (int Scip$SymbolRole/Definition_VALUE))
(.build builder))))
(defn ns-alias-definition->symbol-info
"Create SCIP SymbolInformation for a namespace alias."
[{:keys [from name alias]}]
(when alias
(let [builder (Scip$SymbolInformation/newBuilder)]
(.setSymbol builder (make-alias-symbol from alias))
(.addDocumentation builder (str "```clojure\nAlias for " name "\n```"))
(.build builder))))
(defn alias-usage->occurrence (defn alias-usage->occurrence
"Convert a var-usage with an alias to SCIP Occurrence for the alias part. "Convert a var-usage with an alias to SCIP Occurrence for the alias part.
E.g., for (ansi/style ...), creates an occurrence for 'ansi' referencing the alias." E.g., for (ansi/style ...), creates an occurrence for 'ansi' referencing the alias.
[{:keys [from alias row col name-col]}] The alias position is calculated from col (start of expression) using alias length."
[{:keys [from alias row col]}]
(when alias (when alias
(let [builder (Scip$Occurrence/newBuilder) (let [builder (Scip$Occurrence/newBuilder)
;; alias spans from col to name-col - 2 (excluding the /) alias-str (str alias)
alias-end-col (- name-col 1)
start-line (int (dec row)) start-line (int (dec row))
start-char (int (dec col)) start-char (int (dec col))
end-char (int (dec alias-end-col))] ;; alias ends at col + length(alias), before the /
end-char (int (+ start-char (count alias-str)))]
(.addAllRange builder [(int start-line) (int start-char) (int end-char)]) (.addAllRange builder [(int start-line) (int start-char) (int end-char)])
(.setSymbol builder (make-alias-symbol from alias)) (.setSymbol builder (make-alias-symbol from alias))
(.build builder)))) (.build builder))))
@@ -252,14 +347,26 @@
(.addDocumentation builder doc)) (.addDocumentation builder doc))
(.build builder))) (.build builder)))
(defn ns-in-package-map?
"Check if a namespace is covered by the package-map.
If so, it will be indexed separately and we shouldn't include it
in external_symbols (per SCIP spec: 'Leave this field empty if you
assume the external package will get indexed separately')."
[ns-sym]
(when *package-map*
(some #(str/starts-with? (str ns-sym) %) (keys *package-map*))))
(defn collect-external-symbols (defn collect-external-symbols
"Collect all unique external symbol references from analysis. "Collect all unique external symbol references from analysis.
Returns symbol info for all external symbols, with docs when available. Returns symbol info for external symbols that won't be indexed separately.
Essential for cross-repo navigation to dependencies." Skips symbols from packages in the package-map since those will have
their own SCIP indexes with full documentation."
[analysis internal-namespaces] [analysis internal-namespaces]
(->> (vals analysis) (->> (vals analysis)
(mapcat :var-usages) (mapcat :var-usages)
(filter #(external-ns? (:to %) internal-namespaces)) (filter #(external-ns? (:to %) internal-namespaces))
;; Skip symbols from packages that will be indexed separately
(remove #(ns-in-package-map? (:to %)))
(map (fn [{:keys [to name]}] [to name])) (map (fn [{:keys [to name]}] [to name]))
(filter (fn [[ns-sym _]] ns-sym)) ; Filter out nil namespaces (filter (fn [[ns-sym _]] ns-sym)) ; Filter out nil namespaces
(distinct) (distinct)
@@ -279,7 +386,7 @@
[uri file-analysis project-root] [uri file-analysis project-root]
(let [builder (Scip$Document/newBuilder) (let [builder (Scip$Document/newBuilder)
relative-path (uri->relative-path uri project-root) relative-path (uri->relative-path uri project-root)
{:keys [var-definitions var-usages namespace-definitions namespace-usages namespace-alias]} file-analysis {:keys [var-definitions var-usages namespace-definitions namespace-usages]} file-analysis
;; Build refer-map: for vars imported via :refer, map var-name -> target-namespace ;; Build refer-map: for vars imported via :refer, map var-name -> target-namespace
;; These entries have :refer true and :to populated ;; These entries have :refer true and :to populated
@@ -305,8 +412,10 @@
;; Namespace usages ;; Namespace usages
ns-usage-occurrences (map ns-usage->occurrence (or namespace-usages [])) ns-usage-occurrences (map ns-usage->occurrence (or namespace-usages []))
;; Namespace alias definitions (e.g., :as ansi) ;; Namespace alias definitions from namespace-usages entries with :alias
ns-alias-occurrences (map ns-alias->occurrence (or namespace-alias [])) ;; E.g., [tui.ansi :as ansi] creates definition for 'ansi'
ns-alias-def-occurrences (keep ns-alias-definition->occurrence (or namespace-usages []))
ns-alias-symbols (keep ns-alias-definition->symbol-info (or namespace-usages []))
;; Alias usages from var-usages (e.g., ansi in ansi/style) ;; Alias usages from var-usages (e.g., ansi in ansi/style)
alias-usage-occs (keep alias-usage->occurrence (or var-usages []))] alias-usage-occs (keep alias-usage->occurrence (or var-usages []))]
@@ -318,11 +427,11 @@
;; Add all occurrences ;; Add all occurrences
(doseq [occ (concat def-occurrences usage-occurrences (doseq [occ (concat def-occurrences usage-occurrences
ns-def-occurrences ns-usage-occurrences ns-def-occurrences ns-usage-occurrences
ns-alias-occurrences alias-usage-occs)] ns-alias-def-occurrences alias-usage-occs)]
(.addOccurrences builder occ)) (.addOccurrences builder occ))
;; Add symbol information ;; Add symbol information
(doseq [sym (concat def-symbols ns-def-symbols)] (doseq [sym (concat def-symbols ns-def-symbols ns-alias-symbols)]
(.addSymbols builder sym)) (.addSymbols builder sym))
(.build builder))) (.build builder)))
@@ -401,7 +510,16 @@
(println "Usage: scip-clojure [options]") (println "Usage: scip-clojure [options]")
(println) (println)
(println "Options:") (println "Options:")
(println summary)) (println summary)
(println)
(println "Package Map Format (JSON):")
(println " {\"ns-prefix\": \"package@version\", ...}")
(println)
(println "Example package-map.json for cross-repo navigation:")
(println " {")
(println " \"tui\": \"io.github.ajet/clojure-tui@main\",")
(println " \"lazygitclj\": \"io.github.ajet/lazygitclj@main\"")
(println " }"))
errors errors
(do (do
@@ -413,13 +531,23 @@
(let [project-root (-> (:project-root options) (let [project-root (-> (:project-root options)
io/file io/file
.getCanonicalPath) .getCanonicalPath)
output-file (:output options)] output-file (:output options)
package-map-file (:package-map options)
package-map (when package-map-file
(println "Loading package map from" package-map-file)
(load-package-map package-map-file))]
(println "Generating SCIP index for" project-root) (println "Generating SCIP index for" project-root)
(when package-map
(println "Package mappings:")
(doseq [[prefix spec] package-map]
(println " " prefix "->" spec)))
(try (try
(let [dump (run-clojure-lsp-dump project-root) ;; Bind package map for symbol generation
index (dump->scip-index dump)] (binding [*package-map* package-map]
(write-scip-index index output-file) (let [dump (run-clojure-lsp-dump project-root)
(println "Done!")) index (dump->scip-index dump)]
(write-scip-index index output-file)
(println "Done!")))
(catch Exception e (catch Exception e
(println "Error:" (.getMessage e)) (println "Error:" (.getMessage e))
(when-let [data (ex-data e)] (when-let [data (ex-data e)]