Files
zzet--gortex/internal/resolver/external_call_attribution.go
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

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)
}