Add package-map support for cross-repo SCIP navigation
SCIP Index / index (push) Successful in 1m35s
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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)]
|
||||||
|
|||||||
Reference in New Issue
Block a user