Files
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

340 lines
13 KiB
Go

package resolver
import (
"os"
"strings"
"github.com/zzet/gortex/internal/graph"
)
// Terminal-edge skipping. The warm-restart pending population is dominated by
// permanently external / stdlib / definition-less stubs — `*.Errorf`,
// `*.QueryRow`, `<vector>`, a bare `unresolved::Name` no repo defines — that
// are re-fed to and re-fail every scoped warm resolve, forever (see the
// warmLookupCache 200k-external-method-stub note). A FULL (unscoped) resolve
// has the global evidence to conclude such an edge can never bind; it stamps a
// durable flag so a later SCOPED pass can drop it up front. Full passes ignore
// the flag and re-examine everything, so a stamp self-heals the moment a
// matching definition appears.
const (
// metaResolveTerminal marks an unresolved edge the resolver has concluded
// is permanently unbindable. Read by the scoped pending-scan skip.
metaResolveTerminal = "resolve_terminal"
// metaResolveTerminalReason carries a short human-readable reason for the
// stamp (aligned by string value with internal/analyzer's outcome vocab).
metaResolveTerminalReason = "resolve_terminal_reason"
)
// Terminal reasons. These deliberately match internal/analyzer's resolution-
// outcome constants by STRING VALUE (the analyzer imports resolver, so resolver
// cannot import the analyzer's constants) so both surfaces speak one vocabulary.
const (
terminalReasonNoDefinition = "no_definition"
terminalReasonStubOnly = "stub_only"
terminalReasonStdlibHeader = "stdlib_header"
)
// warmupFullResolve reports whether the operator forced the warm-restart
// master resolve to re-examine every edge (ignoring the durable terminal
// flag). The daemon already translates this override into a nil resolve scope,
// so the scope-empty path covers it; the resolver honours it directly too so a
// scoped pass under the override still self-heals.
func warmupFullResolve() bool {
v := os.Getenv("GORTEX_WARMUP_FULL_RESOLVE")
return v == "1" || strings.EqualFold(v, "true")
}
// edgeTerminalFlag reports whether e carries a live resolve_terminal stamp.
func edgeTerminalFlag(e *graph.Edge) bool {
if e == nil || e.Meta == nil {
return false
}
v, ok := e.Meta[metaResolveTerminal].(bool)
return ok && v
}
// setEdgeTerminal stamps e as terminal in place (allocating Meta if needed).
func setEdgeTerminal(e *graph.Edge, reason string) {
if e == nil {
return
}
if e.Meta == nil {
e.Meta = make(map[string]any, 2)
}
e.Meta[metaResolveTerminal] = true
if reason != "" {
e.Meta[metaResolveTerminalReason] = reason
}
}
// clearEdgeTerminal drops any resolve_terminal stamp from e in place.
func clearEdgeTerminal(e *graph.Edge) {
if e == nil || e.Meta == nil {
return
}
delete(e.Meta, metaResolveTerminal)
delete(e.Meta, metaResolveTerminalReason)
}
// classifyTerminal decides whether an unresolved edge is permanently
// unbindable and, if so, the reason. It is the resolver-native twin of
// internal/analyzer.ClassifyUnresolved (which resolver cannot import): an edge
// is terminal only when NO real definition of its name exists anywhere in the
// graph (the name matches nothing, or only stub / external placeholders) or the
// target is a C/C++ standard-library angle-include. An edge with any real
// in-graph candidate — same-language (candidate_out_of_scope /
// ambiguous_multi_match) or a different language family (cross_language_only) —
// is genuinely pending and NEVER stamped, so more evidence can still bind it.
func (r *Resolver) classifyTerminal(e *graph.Edge) (reason string, terminal bool) {
if e == nil || !graph.IsUnresolvedTarget(e.To) {
return "", false
}
target := graph.UnresolvedName(e.To)
// C/C++/ObjC standard-library angle-include (<stdio.h>, <vector>, …):
// external by construction, deliberately never bound to an in-tree file of
// the same basename. A quoted / non-system include may still resolve to a
// local header, so it stays pending.
if e.Kind == graph.EdgeImports && strings.HasPrefix(target, "import::") {
if k, _ := e.Meta["include_kind"].(string); k == "system" {
if IsCppStdlibHeader(strings.TrimPrefix(target, "import::")) {
return terminalReasonStdlibHeader, true
}
}
return "", false
}
name := identifierFromTarget(target)
if name == "" {
// Module-path shapes (import:: / pyrel:: / grpc::) are owned by
// dedicated whole-graph passes; never classified terminal here.
return "", false
}
fromLang := ""
if n := r.cachedGetNode(e.From); n != nil {
fromLang = n.Language
}
var realSameLang, realOtherLang, stubs int
for _, n := range r.cachedFindNodesByName(name) {
if n == nil {
continue
}
if graph.IsStub(n.ID) {
stubs++
continue
}
if !nodeIsDefinitionKind(n.Kind) {
continue
}
if fromLang != "" && n.Language != "" && !sameLanguageFamily(fromLang, n.Language) {
realOtherLang++
continue
}
realSameLang++
}
switch {
case realSameLang >= 1 || realOtherLang >= 1:
// A real definition exists — genuinely pending, not terminal.
return "", false
case stubs >= 1:
return terminalReasonStubOnly, true
default:
return terminalReasonNoDefinition, true
}
}
// nodeIsDefinitionKind reports whether a node kind is a callable / type
// definition an unresolved call or reference could legitimately bind to. Kept
// resolver-local (the analyzer's identical helper is package-private) so this
// package takes no analyzer dependency.
func nodeIsDefinitionKind(k graph.NodeKind) bool {
switch k {
case graph.KindFunction, graph.KindMethod, graph.KindType,
graph.KindInterface, graph.KindVariable, graph.KindConstant, graph.KindField:
return true
}
return false
}
// definitionKinds is nodeIsDefinitionKind's predicate reified as a list, for
// callers (the bulk terminal classifier) that need to hand it to a Store
// capability instead of calling the predicate per node.
var definitionKinds = []graph.NodeKind{
graph.KindFunction, graph.KindMethod, graph.KindType,
graph.KindInterface, graph.KindVariable, graph.KindConstant, graph.KindField,
}
// filterTerminalSkip drops the terminally-unresolved edges a scoped pass need
// not reconsider, composing with (running AFTER) filterPendingByScope. A
// terminal edge is kept only when it is specifically anchored to a changed
// repo — its source could re-target, or its target is repo-qualified to a repo
// that just re-indexed. A terminal edge that survived the scope filter only
// because it is a bare, unqualified name is dropped: no definition exists and
// no changed repo owns it, so it would re-fail. (A changed repo that newly ADDS
// a matching name binds the edge in the subsequent whole-graph cross-repo
// resolve, which is unaffected by this flag — so the drop is a pure work
// saving.) Filters in place; the returned slice reuses pending's backing array.
func filterTerminalSkip(pending []*graph.Edge, scope map[string]struct{}) (kept []*graph.Edge, skipped int) {
out := pending[:0]
for _, e := range pending {
if e == nil {
continue
}
if !edgeTerminalFlag(e) || terminalEdgeAnchoredToScope(e, scope) {
out = append(out, e)
continue
}
skipped++
}
return out, skipped
}
// terminalEdgeAnchoredToScope reports whether a terminal edge is tied to a
// changed repo (source repo in scope, or target repo-qualified to a changed
// repo). It mirrors edgeInResolveScope MINUS the bare-unqualified rule — a bare
// terminal edge is exactly the skippable case.
func terminalEdgeAnchoredToScope(e *graph.Edge, scope map[string]struct{}) bool {
if _, ok := scope[graph.RepoPrefixOfID(e.From)]; ok {
return true
}
targetRepo := graph.UnresolvedRepoPrefix(e.To)
if targetRepo == "" {
targetRepo = graph.StubRepoPrefix(e.To)
}
if targetRepo == "" {
return false
}
_, ok := scope[targetRepo]
return ok
}
// reconcileTerminalStamps runs at the END of a FULL (unscoped) ResolveAll: it
// re-classifies every edge still unresolved after all post-passes and brings
// its durable terminal flag into agreement with the current global evidence —
// stamping the newly-terminal, un-stamping any edge that used to be terminal
// but now has a real candidate (self-healing). Edges already in the right state
// are left untouched, so a converged graph performs no writes on later full
// passes. Persisted via the batched EdgePersister capability; the in-memory
// backend's in-place Meta mutation is already durable, so its missing
// capability is a no-op. Returns the counts stamped / un-stamped.
//
// Classification itself takes the bulk path (reconcileTerminalStampsBulk)
// when the backend implements graph.NodeNameClassCounter, falling back to
// the original per-edge classifyTerminal loop otherwise (e.g. the in-memory
// backend). The two paths make IDENTICAL decisions — see
// reconcileTerminalStampsBulk's doc for why the bulk path can skip fromLang /
// language-family entirely without changing the outcome.
func (r *Resolver) reconcileTerminalStamps() (stamped, unstamped int) {
var stillPending []*graph.Edge
for e := range r.graph.EdgesWithUnresolvedTarget() {
if e != nil {
stillPending = append(stillPending, e)
}
}
var changed []*graph.Edge
if counter, ok := r.graph.(graph.NodeNameClassCounter); ok {
changed, stamped, unstamped = r.reconcileTerminalStampsBulk(stillPending, counter)
} else {
for _, e := range stillPending {
reason, terminal := r.classifyTerminal(e)
marked := edgeTerminalFlag(e)
switch {
case terminal && !marked:
setEdgeTerminal(e, reason)
changed = append(changed, e)
stamped++
case !terminal && marked:
clearEdgeTerminal(e)
changed = append(changed, e)
unstamped++
}
}
}
if len(changed) > 0 {
if p, ok := r.graph.(graph.EdgeMetaBatchPersister); ok {
p.PersistEdgeAttributesBatch(changed)
}
}
return stamped, unstamped
}
// classifyTerminalBulk replicates classifyTerminal's decision for every edge
// in pending EXCEPT EdgeImports edges, which route through the unchanged
// per-edge classifyTerminal: its C/C++ stdlib-header special case reads
// e.Meta["include_kind"] plus a 200+-entry lookup table (IsCppStdlibHeader),
// and porting that to SQL would risk the two drifting out of sync for a
// small, rarely-hit case. Every other unresolved-target edge is classified
// from one batched name lookup instead of N cachedFindNodesByName + IsStub
// calls. Pure — no mutation, no persistence — so it can be compared
// element-wise against classifyTerminal's own per-edge output.
//
// classifyTerminal computes realSameLang / realOtherLang separately but its
// own terminal/non-terminal decision only ever checks
// "realSameLang >= 1 || realOtherLang >= 1" — both branches mean exactly the
// same thing to the switch below them. So this bulk path never needs
// e.From's language or the target's language: a real (non-stub,
// definition-kind) node matching the target name by ANY language counts
// identically. Only stubs-vs-real-vs-neither matters here.
func (r *Resolver) classifyTerminalBulk(pending []*graph.Edge, counter graph.NodeNameClassCounter) (reasons []string, terminals []bool) {
names := make(map[string]struct{})
for _, e := range pending {
if e.Kind == graph.EdgeImports {
continue
}
if name := identifierFromTarget(graph.UnresolvedName(e.To)); name != "" {
names[name] = struct{}{}
}
}
nameList := make([]string, 0, len(names))
for name := range names {
nameList = append(nameList, name)
}
counts := counter.CountNodesByNameClass(nameList, definitionKinds)
reasons = make([]string, len(pending))
terminals = make([]bool, len(pending))
for i, e := range pending {
switch e.Kind {
case graph.EdgeImports:
reasons[i], terminals[i] = r.classifyTerminal(e)
default:
name := identifierFromTarget(graph.UnresolvedName(e.To))
switch c := counts[name]; {
case name == "" || c.Real >= 1:
reasons[i], terminals[i] = "", false
case c.Stub >= 1:
reasons[i], terminals[i] = terminalReasonStubOnly, true
default:
reasons[i], terminals[i] = terminalReasonNoDefinition, true
}
}
}
return reasons, terminals
}
// reconcileTerminalStampsBulk is reconcileTerminalStamps' SQL-accelerated
// path: classify every pending edge via classifyTerminalBulk, then apply the
// same stamp/unstamp diff logic as the per-edge fallback.
func (r *Resolver) reconcileTerminalStampsBulk(pending []*graph.Edge, counter graph.NodeNameClassCounter) (changed []*graph.Edge, stamped, unstamped int) {
reasons, terminals := r.classifyTerminalBulk(pending, counter)
for i, e := range pending {
reason, terminal := reasons[i], terminals[i]
marked := edgeTerminalFlag(e)
switch {
case terminal && !marked:
setEdgeTerminal(e, reason)
changed = append(changed, e)
stamped++
case !terminal && marked:
clearEdgeTerminal(e)
changed = append(changed, e)
unstamped++
}
}
return changed, stamped, unstamped
}