a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
253 lines
9.0 KiB
Go
253 lines
9.0 KiB
Go
package resolver
|
|
|
|
import (
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// attributeGoExternalCalls materialises a KindFunction node for every
|
|
// unique `stdlib::<importPath>::<symbol>` / `dep::<importPath>::<symbol>`
|
|
// / `external::<importPath>::<symbol>` edge target, plus a KindModule
|
|
// parent for each owning import path. Without this pass the targets
|
|
// are stubs in storage backends that enforce rel-table FK (the on-disk backend)
|
|
// and invisible nodes in the in-memory backend, so a query like
|
|
// `find_usages(stdlib::encoding/json::Marshal)`
|
|
// can't surface "every function in this codebase that calls
|
|
// json.Marshal" — the destination doesn't exist as a graph node.
|
|
//
|
|
// Mirrors the Python / Dart attributeNonGoModuleImports pass for Go.
|
|
// Runs after resolveExtern (which classifies extern targets into the
|
|
// three prefix buckets) so we materialise the post-classification
|
|
// state rather than the pre-classification `unresolved::extern::*`
|
|
// shape.
|
|
//
|
|
// ID conventions:
|
|
// - Module node: `module::go:<importPath>` — shared across every
|
|
// repo that imports the same path. Carries
|
|
// Meta["ecosystem"]="go" and Meta["import_path"]=<path>.
|
|
// Meta["role"]="stdlib" for stdlib paths.
|
|
// - Symbol node: the original `stdlib::*` / `dep::*` /
|
|
// `external::*` ID stays the symbol's ID so existing edges land
|
|
// on it without rewriting. Carries Meta["external"]=true and
|
|
// Meta["module_path"]=<importPath>.
|
|
// - EdgeMemberOf: symbol → module so `get_callers` on the module
|
|
// surfaces every symbol used from that package.
|
|
//
|
|
// All AddNode / AddEdge calls are idempotent on ID, so a second run
|
|
// of this pass (incremental ResolveFile re-invocation) is a no-op.
|
|
// extKey identifies a unique external target across the attribution
|
|
// passes; modKey identifies its owning module. Package-level so the
|
|
// whole-graph and single-file collectors feed one materialiser.
|
|
//
|
|
// repoPrefix is part of the key because stdlib stubs are per-repo (see
|
|
// internal/graph/stub.go) — two repos on different Go SDK versions emit
|
|
// semantically distinct `<repoA>::stdlib::fmt::Errorf` and
|
|
// `<repoB>::stdlib::fmt::Errorf` stubs that MUST round-trip through this
|
|
// attribution as distinct nodes, not collide into one.
|
|
type extKey struct {
|
|
repoPrefix, prefix, importPath, symbol string
|
|
}
|
|
|
|
type modKey struct{ repoPrefix, importPath string }
|
|
|
|
// goExternalAttribKinds is every edge kind an extern-prefixed target can
|
|
// show up on — the same set attributeGoBuiltins scans.
|
|
var goExternalAttribKinds = []graph.EdgeKind{
|
|
graph.EdgeCalls,
|
|
graph.EdgeReferences,
|
|
graph.EdgeReads,
|
|
graph.EdgeArgOf,
|
|
graph.EdgeValueFlow,
|
|
graph.EdgeReturnsTo,
|
|
graph.EdgeTypedAs,
|
|
graph.EdgeReturns,
|
|
graph.EdgeInstantiates,
|
|
graph.EdgeCaptures,
|
|
graph.EdgeThrows,
|
|
}
|
|
|
|
func (r *Resolver) attributeGoExternalCalls() {
|
|
// Go-only pass: skip the external-prefix edge scan when the graph has
|
|
// no Go nodes.
|
|
if !r.graphHasLanguage("go") {
|
|
return
|
|
}
|
|
seen := map[extKey]struct{}{}
|
|
for _, k := range goExternalAttribKinds {
|
|
for e := range r.graph.EdgesByKind(k) {
|
|
collectGoExternalTarget(e, seen)
|
|
}
|
|
}
|
|
r.materializeGoExternalSeen(seen)
|
|
}
|
|
|
|
// attributeGoExternalCallsForFile is the single-file scope of
|
|
// attributeGoExternalCalls: an extern-prefixed target is referenced from
|
|
// inside the edited file, so only that file's outgoing edges can
|
|
// introduce a new one. Produces the same materialisation as the
|
|
// whole-graph sweep for a per-save resolve.
|
|
func (r *Resolver) attributeGoExternalCallsForFile(filePath string) {
|
|
if !r.graphHasLanguage("go") {
|
|
return
|
|
}
|
|
seen := map[extKey]struct{}{}
|
|
for _, e := range r.fileOutEdges(filePath) {
|
|
collectGoExternalTarget(e, seen)
|
|
}
|
|
r.materializeGoExternalSeen(seen)
|
|
}
|
|
|
|
// collectGoExternalTarget records e's external target (if any) into seen,
|
|
// deduping by the per-repo (prefix, path, symbol) tuple.
|
|
func collectGoExternalTarget(e *graph.Edge, seen map[extKey]struct{}) {
|
|
if e == nil || e.To == "" {
|
|
return
|
|
}
|
|
prefix, importPath, symbol := splitGoExternalTarget(e.To)
|
|
if prefix == "" {
|
|
return
|
|
}
|
|
seen[extKey{graph.StubRepoPrefix(e.To), prefix, importPath, symbol}] = struct{}{}
|
|
}
|
|
|
|
// materializeGoExternalSeen turns the collected external targets into
|
|
// KindModule + KindFunction nodes and their EdgeMemberOf links. All
|
|
// AddNode / AddEdge calls are idempotent on ID, so a second run (e.g. a
|
|
// re-resolve of the same file) is a no-op.
|
|
func (r *Resolver) materializeGoExternalSeen(seen map[extKey]struct{}) {
|
|
if len(seen) == 0 {
|
|
return
|
|
}
|
|
|
|
// Materialise the parent KindModule for each unique import path,
|
|
// then the per-symbol KindFunction. Module-side dedupe is via
|
|
// the `modules` map; the per-symbol nodes are unique by (prefix,
|
|
// path, symbol) by construction.
|
|
// Module IDs are also per-repo now — a module node carries the
|
|
// same SDK-version sensitivity its symbols do. Key includes the
|
|
// repo prefix so two repos importing the same path get distinct
|
|
// module nodes.
|
|
modules := map[modKey]string{}
|
|
for k := range seen {
|
|
modKey := modKey{repoPrefix: k.repoPrefix, importPath: k.importPath}
|
|
moduleID, ok := modules[modKey]
|
|
if !ok {
|
|
// Ecosystem + path are ONE stub segment joined by a single
|
|
// colon (`go:<importPath>`), matching the npm convention
|
|
// (`module::npm:<pkg>`) and every module-id consumer
|
|
// (tools_analyze_external_calls). Passing them as two
|
|
// StubID parts would emit `module::go::<path>` (double
|
|
// colon) — the form that broke the attribution tests.
|
|
moduleID = graph.StubID(k.repoPrefix, graph.StubKindModule, "go:"+k.importPath)
|
|
modules[modKey] = moduleID
|
|
role := "external"
|
|
switch k.prefix {
|
|
case "stdlib::":
|
|
role = "stdlib"
|
|
case "dep::":
|
|
role = "dep"
|
|
}
|
|
r.graph.AddNode(&graph.Node{
|
|
ID: moduleID,
|
|
Kind: graph.KindModule,
|
|
Name: lastImportSegment(k.importPath),
|
|
Language: "go",
|
|
Meta: map[string]any{
|
|
"ecosystem": "go",
|
|
"role": role,
|
|
"import_path": k.importPath,
|
|
},
|
|
})
|
|
}
|
|
var symbolID string
|
|
switch k.prefix {
|
|
case "stdlib::":
|
|
symbolID = graph.StubID(k.repoPrefix, graph.StubKindStdlib, k.importPath, k.symbol)
|
|
default:
|
|
// dep:: / external:: keep their legacy unprefixed form for
|
|
// now — they aren't covered by the stub-prefix migration
|
|
// (different module paths already provide repo-level
|
|
// distinction; same version pinning is enforced by go.mod
|
|
// per-repo).
|
|
symbolID = k.prefix + k.importPath + "::" + k.symbol
|
|
}
|
|
r.graph.AddNode(&graph.Node{
|
|
ID: symbolID,
|
|
Kind: graph.KindFunction,
|
|
Name: k.symbol,
|
|
Language: "go",
|
|
Meta: map[string]any{
|
|
"external": true,
|
|
"module_path": k.importPath,
|
|
"module_role": map[string]string{
|
|
"stdlib::": "stdlib",
|
|
"dep::": "dep",
|
|
"external::": "external",
|
|
}[k.prefix],
|
|
},
|
|
})
|
|
// EdgeMemberOf: symbol → module. AddEdge is idempotent on the
|
|
// edge-key tuple so a re-run doesn't duplicate.
|
|
r.graph.AddEdge(&graph.Edge{
|
|
From: symbolID,
|
|
To: moduleID,
|
|
Kind: graph.EdgeMemberOf,
|
|
Origin: graph.OriginASTResolved,
|
|
})
|
|
}
|
|
}
|
|
|
|
// splitGoExternalTarget recognises the three external-target prefixes
|
|
// the resolver emits after resolveExtern. Returns the prefix
|
|
// (`stdlib::` / `dep::` / `external::`), the import path, and the
|
|
// symbol name. Returns ("", "", "") for any other shape so the pass
|
|
// can skip it cleanly.
|
|
//
|
|
// The stdlib case is matched via graph.IsStdlibStub so both the
|
|
// legacy `stdlib::fmt::Errorf` shape and the per-repo-prefixed
|
|
// `<repo>::stdlib::fmt::Errorf` shape (see internal/graph/stub.go)
|
|
// route the same way. The returned bucket label stays `stdlib::` for
|
|
// downstream `k.prefix == "stdlib::"` comparisons.
|
|
func splitGoExternalTarget(target string) (prefix, importPath, symbol string) {
|
|
var body string
|
|
switch {
|
|
case graph.IsStdlibStub(target):
|
|
prefix = "stdlib::"
|
|
body = graph.StubRest(target)
|
|
case strings.HasPrefix(target, "dep::"):
|
|
prefix = "dep::"
|
|
body = strings.TrimPrefix(target, prefix)
|
|
case strings.HasPrefix(target, "external::"):
|
|
prefix = "external::"
|
|
body = strings.TrimPrefix(target, prefix)
|
|
default:
|
|
return "", "", ""
|
|
}
|
|
// The body shape produced by resolveExtern is
|
|
// `<importPath>::<symbol>`. Split on the LAST `::` because import
|
|
// paths can include slashes but not `::`, so the rightmost
|
|
// separator is always between path and symbol.
|
|
sep := strings.LastIndex(body, "::")
|
|
if sep < 0 {
|
|
// `external::os` style (just the package, no symbol —
|
|
// the resolveImport path). Treat the whole body as the path
|
|
// and leave symbol empty so we still materialise the module
|
|
// node but skip the symbol.
|
|
return prefix, body, ""
|
|
}
|
|
return prefix, body[:sep], body[sep+2:]
|
|
}
|
|
|
|
// lastImportSegment returns the rightmost path component, used as
|
|
// the human-readable Name on the KindModule node. For
|
|
// `github.com/stretchr/testify/assert` the segment is `assert`; for
|
|
// `encoding/json` it's `json`; for `fmt` it's `fmt`.
|
|
func lastImportSegment(importPath string) string {
|
|
if importPath == "" {
|
|
return ""
|
|
}
|
|
return path.Base(importPath)
|
|
}
|