项目文件夹

文件
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

469 行
18 KiB
Go

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package resolver
import (
"path/filepath"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// Cross-package name-match guard.
//
// The heuristic cascade in resolveFunctionCall / resolveMethodCall ends,
// for calls it can't pin precisely, in a name-only fallback: "the first
// function/method named X in the caller's repo". When the only candidate
// of that name lives in a package the caller never imports, that
// fallback manufactures a false `calls` edge — a JS/TS factory result
// `h.handle()` binding to an unrelated `handle`, or a `ns.foo()`
// namespace call binding to a free `foo` in some other module.
//
// This guard runs once after the main resolution pass. For every edge
// the pass resolved at one of the two weakest confidence tiers
// (text_matched / ast_inferred) it asks a single question: is the
// resolved target import-reachable from the call site? Reachable means
// the target sits in the caller's own directory (same package) or in a
// directory the caller's file imports. When it is not, the edge is
// reverted to its pre-resolution `unresolved::` target so a
// higher-evidence resolver (CrossRepoResolver, or a later LSP-backed
// pass) can have a clean attempt instead of inheriting a wrong binding.
//
// Genuine same-package and imported-target edges are never touched: the
// reachability set always contains the caller's own directory, and an
// imported package contributes its directory to the set. Edges resolved
// at ast_resolved or above are out of scope — those carry structural or
// compiler-grade evidence the name-only fallback never had.
// guardCrossPackageCallEdges inspects the edges mutated by the just-
// completed resolution pass and reverts any weak-tier call/reference
// edge whose resolved target is not import-reachable from the caller.
// jobs are the reindexJob records produced by ResolveAll's worker
// phase; each carries the edge's pre-resolution target in oldTo, so a
// reverted edge is restored exactly. closure is the import-reachability
// map from buildImportClosure. Returns the number of edges reverted.
func (r *Resolver) guardCrossPackageCallEdges(jobs []reindexJob, closure map[string]map[string]struct{}) int {
if len(jobs) == 0 {
return 0
}
// Collect both mutation lists across the whole pass and apply them
// via the batched Store methods at the end. Per-edge
// SetEdgeProvenance + ReindexEdge in the body would otherwise pay
// two ACID round-trips per reverted edge against disk backends —
// catastrophic on a 30k-job pass.
var provBatch []graph.EdgeProvenanceUpdate
var reindexBatch []graph.EdgeReindex
for i := range jobs {
j := &jobs[i]
// A concurrent edit during a chunked ResolveAll yield may have evicted
// this edge since it resolved; reverting + reindexing it would
// half-resurrect it. Skip — it is no longer in the graph.
if r.validateLiveness && !edgeStillLive(r.graph, j.edge) {
continue
}
// The deferred LSP batch may have re-bound (or confirmed) this edge
// after the heuristic job was recorded, stamping it OriginLSPResolved —
// compiler-grade evidence the name-only fallback this guard polices
// never had. j.origin still holds the stale heuristic tier, so trust
// the live edge: never revert an LSP-owned binding. (The batch now
// overrides confident heuristic binds, so a recorded job's target can
// be LSP-owned; before, the batch only touched heuristic-unresolved
// edges, disjoint from these jobs, and this never fired.)
if j.edge.Origin == graph.OriginLSPResolved {
continue
}
if !isCallLikeEdge(j.kind) {
continue
}
// Only the two weakest tiers — a name-only guess — are in scope.
// DefaultOriginFor backfills the tier for edges whose Origin the
// resolver left unset (the heuristic fallbacks never stamp it).
origin := j.origin
if origin == "" {
origin = graph.DefaultOriginFor(j.kind, j.confidence, "")
}
if origin != graph.OriginTextMatched && origin != graph.OriginASTInferred {
continue
}
// The pre-resolution target must be a bare-name placeholder —
// `unresolved::Foo` (function call) or `unresolved::*.foo`
// (member call). Anything else carries evidence the name-only
// fallback never had and is out of scope: `extern::` pins an
// import path, `grpc::` / `pyrel::` / `import::` are owned by
// dedicated passes, and a non-`unresolved::` target was never a
// guess to begin with.
if !isBareNameCallTarget(j.oldTo) {
continue
}
callerFile := r.edgeCallerFile(j.edge)
callerNode := r.cachedGetNode(j.edge.From)
target := r.cachedGetNode(j.newTo)
if callerFile == "" || target == nil {
continue
}
if r.targetImportReachable(callerFile, callerNode, target, closure) {
continue
}
// A member call whose only in-repo definition of the name is this
// target is not a cross-package mis-guess — there is nowhere else the
// call could bind. A method call carries its receiver, so it needs no
// import of the method's package, and inherited / indirectly-typed
// receivers (owner.foo() → BaseType.foo two packages up) never name the
// declaring package, so the import closure structurally misses them.
// Keep the resolution.
if r.loneMemberDefnKeep(target, j.edge, j.oldTo) {
continue
}
// Not reachable — revert to the unresolved placeholder and
// re-index against the resolved target we are abandoning.
// SetEdgeProvenance("") drops the resolution provenance so
// the reverted edge's identity change is counted; the target
// revert + re-bucket follows. Both go in their respective
// batches so the whole pass commits in two chunks instead of
// 2×N per-edge transactions.
oldResolved := j.edge.To
provBatch = append(provBatch, graph.EdgeProvenanceUpdate{Edge: j.edge, NewOrigin: ""})
j.edge.To = j.oldTo
j.edge.Confidence = 0
reindexBatch = append(reindexBatch, graph.EdgeReindex{Edge: j.edge, OldTo: oldResolved})
}
if len(provBatch) > 0 {
r.graph.SetEdgeProvenanceBatch(provBatch)
}
if len(reindexBatch) > 0 {
r.graph.ReindexEdges(reindexBatch)
}
return len(reindexBatch)
}
// isBareNameCallTarget reports whether an unresolved edge target is a
// bare-name call placeholder — `unresolved::Foo` for a free-function
// call or `unresolved::*.foo` for a member call. These are the only
// shapes the name-only resolution fallback acts on. Targets that embed
// further structure (`unresolved::extern::path::sym`, `grpc::`,
// `pyrel::`, `import::`) carry evidence the fallback never had and are
// resolved by other code paths, so the guard leaves them alone.
func isBareNameCallTarget(target string) bool {
rest, ok := strings.CutPrefix(target, unresolvedPrefix)
if !ok || rest == "" {
return false
}
rest = strings.TrimPrefix(rest, "*.")
if rest == "" {
return false
}
// A remaining `::` means the placeholder is one of the structured
// forms (extern::, grpc::, pyrel::, import::), not a bare name.
return !strings.Contains(rest, "::")
}
// isCallLikeEdge reports whether an edge kind is one the guard polices.
// EdgeCalls is the obvious case; EdgeReferences is included because the
// resolver promotes a call-shaped EdgeReads to EdgeReferences once it
// learns the target is a function/method, and that promotion runs
// through the very same name-only fallback.
func isCallLikeEdge(k graph.EdgeKind) bool {
return k == graph.EdgeCalls || k == graph.EdgeReferences
}
// edgeCallerFile returns the file path of the node that owns the edge's
// From end. Empty when the caller node is unknown.
//
// Hot path: called once per cross-package-guarded edge. The pre-warmed
// per-pass cache populated in ResolveAll holds every From ID across the
// pending slice, so this call is a map lookup during a ResolveAll pass
// and a direct store call elsewhere.
func (r *Resolver) edgeCallerFile(e *graph.Edge) string {
if n := r.cachedGetNode(e.From); n != nil && n.FilePath != "" {
return n.FilePath
}
return e.FilePath
}
// targetImportReachable reports whether target sits in a package the
// caller's file can see: the caller's own directory (same package), or
// a directory present in the caller's import closure.
func (r *Resolver) targetImportReachable(callerFile string, callerNode, target *graph.Node, closure map[string]map[string]struct{}) bool {
if target.FilePath == "" {
// A target with no file (synthetic / external stub) can't be
// shown unreachable — leave the edge alone.
return true
}
callerDir := filepath.Dir(callerFile)
targetDir := filepath.Dir(target.FilePath)
if targetDir == callerDir {
return true
}
// Same source package across different directories is reachable without
// an import edge. Maven splits one package across src/main/java and
// src/test/java, and JVM same-package callers import nothing — so a
// directory-only closure reports a false "unreachable" for every
// test→production same-package call. scope_pkg is stamped only on JVM
// member nodes, so this never fires for directory-scoped ecosystems.
if sameScopePackage(callerNode, target) {
return true
}
dirs, ok := closure[callerFile]
if !ok {
// No closure entry for the caller (its file node or imports were
// not indexed). Be conservative: without evidence of isolation
// we keep the edge rather than risk dropping a real one.
return true
}
_, reachable := dirs[targetDir]
return reachable
}
// scopePkgOf returns a node's stamped source package (scope_pkg Meta),
// empty when absent. Only JVM extractors (Java / Kotlin) stamp it.
func scopePkgOf(n *graph.Node) string {
if n == nil || n.Meta == nil {
return ""
}
if p, ok := n.Meta["scope_pkg"].(string); ok {
return p
}
return ""
}
// sameScopePackage reports whether two nodes belong to the same source
// package of the same language. Empty package on either side is never a
// match, so directory-scoped ecosystems (no scope_pkg) never qualify.
func sameScopePackage(a, b *graph.Node) bool {
if a == nil || b == nil {
return false
}
pa := scopePkgOf(a)
if pa == "" {
return false
}
return pa == scopePkgOf(b) && a.Language == b.Language
}
// loneMemberDefnKeep reports whether a to-be-reverted member-call edge should
// survive the cross-package guard because its target is the sole in-repo
// definition of the method name. A name with exactly one candidate cannot be a
// cross-package mis-guess: a method call carries its receiver, so it needs no
// import of the method's package, and the import closure structurally misses
// inherited / indirectly-typed receivers (owner.foo() where owner came from a
// return value the caller's file never imports the type of). Restricted to the
// statically-typed languages (java, go) where a lone method name is unambiguous
// — TS / Python duck typing makes a same-name coincidence likelier, so the
// guard's revert stays load-bearing there — and gated on the receiver, when
// known, naming an in-repo type so an external-typed receiver (a logging
// facade's `logger.info`) still reverts rather than latching onto an unrelated
// same-named local method.
func (r *Resolver) loneMemberDefnKeep(target *graph.Node, e *graph.Edge, oldTo string) bool {
if target == nil || !loneMemberLang(target.Language) {
return false
}
bareName := graph.UnresolvedName(oldTo)
memberCall := strings.HasPrefix(bareName, "*.")
// Go has free functions that DO need their package imported, so only a
// member call (`x.foo()` — the receiver carries the type, no import of the
// method's package needed) is kept; a bare free-function call to an
// un-imported package must still revert. Java has no free functions, so its
// bare calls are static-member dispatch and keep too.
if target.Language != "java" && !memberCall {
return false
}
name := strings.TrimPrefix(bareName, "*.")
if name == "" {
return false
}
repo := r.callerRepoPrefix(e)
if rt := edgeReceiverType(e); rt != "" && !r.hasInRepoType(rt, repo) {
return false
}
n := 0
for _, c := range r.cachedFindNodesByNameInRepo(name, repo) {
if c.Language != target.Language {
continue
}
// A member call can only bind to a method; count functions too only
// for Java's static-member model (its bare calls).
if c.Kind == graph.KindMethod || (target.Language == "java" && c.Kind == graph.KindFunction) {
if n++; n > 1 {
return false
}
}
}
return n == 1
}
// loneMemberLang reports whether a lone in-repo method definition is safe to
// keep against the cross-package guard for the given language. Limited to the
// statically-typed languages where exactly one same-named member is
// structurally unambiguous; TS / Python / JS duck typing makes a same-name
// coincidence likelier, so their guard revert stays.
func loneMemberLang(lang string) bool {
switch lang {
case "java", "go", "rust", "csharp", "kotlin", "scala":
return true
}
return false
}
// hasInRepoType reports whether the repo defines a type/interface named
// typeName — the gate that keeps javaLoneMemberDefnKeep from latching a
// call on an external-typed receiver onto an unrelated in-repo method.
func (r *Resolver) hasInRepoType(typeName, repo string) bool {
for _, c := range r.cachedFindNodesByNameInRepo(typeName, repo) {
if c.Kind == graph.KindType || c.Kind == graph.KindInterface {
return true
}
}
return false
}
// buildImportClosure maps each caller file path to the set of directories
// it can reach by import. The set is seeded with the file's own directory
// and extended with the directory of every node its resolved EdgeImports
// edges point at. It is built from the post-resolution graph — by the
// time the guard runs, import edges have been resolved to real file /
// package nodes, so this closure captures JS/TS relative-file imports
// that the pre-resolution reachability index (keyed on directory-shaped
// import paths) structurally misses.
func (r *Resolver) buildImportClosure() map[string]map[string]struct{} {
return r.buildImportClosureFiltered(nil)
}
// buildImportClosureFiltered is buildImportClosure restricted to a set of repo
// prefixes: it seeds the closure only for files owned by those repos and only
// walks import edges whose caller sits in one of them. Each import edge
// contributes solely to its own caller's closure entry, so a caller in the set
// gets the same reachable-dir set it would in the whole-graph build — the guard
// queries the closure only for those callers, so its verdicts are unchanged.
// Re-export edges stay unfiltered: a caller in the set may import a barrel that
// re-exports from a repo outside it, and the transitive barrel walk must still
// reach it. A nil repos set builds the whole-graph closure.
func (r *Resolver) buildImportClosureFiltered(repos map[string]struct{}) map[string]map[string]struct{} {
inScope := func(id string) bool {
if repos == nil {
return true
}
_, ok := repos[graph.RepoPrefixOfID(id)]
return ok
}
closure := make(map[string]map[string]struct{})
add := func(file, dir string) {
if file == "" || dir == "" {
return
}
set := closure[file]
if set == nil {
set = make(map[string]struct{})
closure[file] = set
}
set[dir] = struct{}{}
}
for n := range r.graph.NodesByKind(graph.KindFile) {
if n.FilePath != "" && inScope(n.ID) {
add(n.FilePath, filepath.Dir(n.FilePath))
}
}
// Materialise the resolved import edges and batch-load their endpoints
// (caller file + target) in one GetNodesByIDs — a per-edge GetNode here
// is a query round-trip per import on a disk backend. Inlines
// edgeCallerFile's cached-node logic against the batch map.
//
// Re-export edges ride the same batch: an import that lands on a
// barrel (`import { persist } from 'zustand/middleware'` resolving to
// src/middleware.ts, which `export { persist } from
// './middleware/persist.ts'`) must make the re-exported module's
// directory reachable too — the consumer names the barrel, but the
// symbol it calls lives behind the re-export hop. Without this, the
// guard reverts every legitimate barrel-mediated call as
// "not import-reachable".
skipTarget := func(to string) bool {
return strings.HasPrefix(to, unresolvedPrefix) ||
strings.HasPrefix(to, "external::") ||
graph.IsStdlibStub(to) ||
strings.HasPrefix(to, "dep::")
}
var imports, reexports []*graph.Edge
ids := make(map[string]struct{})
collect := func(e *graph.Edge) {
if e.From != "" {
ids[e.From] = struct{}{}
}
if e.To != "" {
ids[e.To] = struct{}{}
}
}
for e := range r.graph.EdgesByKind(graph.EdgeImports) {
// Skip imports still pointing at an unresolved placeholder or an
// out-of-repo stub — neither names an in-repo directory that a
// name-only call candidate could legitimately live in.
if skipTarget(e.To) {
continue
}
// An import edge only extends its own caller's closure entry, so on a
// scoped build we need just the edges whose caller is in scope.
if !inScope(e.From) {
continue
}
imports = append(imports, e)
collect(e)
}
for e := range r.graph.EdgesByKind(graph.EdgeReExports) {
if skipTarget(e.To) {
continue
}
reexports = append(reexports, e)
collect(e)
}
if len(imports) == 0 {
return closure
}
idList := make([]string, 0, len(ids))
for id := range ids {
idList = append(idList, id)
}
nodes := r.graph.GetNodesByIDs(idList)
// Direct barrel-file → re-export-target-file map, then a memoised
// transitive walk so chained barrels (src/index.ts → src/middleware.ts
// → src/middleware/persist.ts) contribute every hop's directory.
reexpTargets := make(map[string][]string)
for _, e := range reexports {
barrel := e.FilePath
if n := nodes[e.From]; n != nil && n.FilePath != "" {
barrel = n.FilePath
}
if t := nodes[e.To]; t != nil && t.FilePath != "" && barrel != "" {
reexpTargets[barrel] = append(reexpTargets[barrel], t.FilePath)
}
}
barrelDirCache := make(map[string][]string)
var barrelDirs func(file string, seen map[string]bool) []string
barrelDirs = func(file string, seen map[string]bool) []string {
if dirs, ok := barrelDirCache[file]; ok {
return dirs
}
if seen[file] {
return nil
}
seen[file] = true
var dirs []string
for _, tf := range reexpTargets[file] {
dirs = append(dirs, filepath.Dir(tf))
dirs = append(dirs, barrelDirs(tf, seen)...)
}
barrelDirCache[file] = dirs
return dirs
}
for _, e := range imports {
callerFile := e.FilePath
if n := nodes[e.From]; n != nil && n.FilePath != "" {
callerFile = n.FilePath
}
if target := nodes[e.To]; target != nil && target.FilePath != "" {
add(callerFile, filepath.Dir(target.FilePath))
for _, d := range barrelDirs(target.FilePath, map[string]bool{}) {
add(callerFile, d)
}
}
}
return closure
}