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
180 lines
5.6 KiB
Go
180 lines
5.6 KiB
Go
package resolver
|
|
|
|
import (
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// storeFactoryVia is the Meta["via"] tag the JS/TS extractors stamp on a
|
|
// store-factory call placeholder the same-file pass could not bind (the store
|
|
// lives in another file).
|
|
const storeFactoryVia = "store-factory"
|
|
|
|
// ResolveStoreFactoryCalls binds Zustand/Redux/Pinia/MobX indirect action
|
|
// calls — `useStore.getState().fetchUser()` and `const {fetchUser} =
|
|
// useStore.getState(); fetchUser()` — to the precise action node, across files.
|
|
//
|
|
// The extractors stamp each store-action node with Meta["store_factory"]=<binding>
|
|
// + Meta["store_member"]=<member>, and stamp the cross-file call placeholder
|
|
// with Meta["via"]="store-factory" + store_binding + store_action. This pass
|
|
// joins them by (binding, member). It is strictly more precise than the
|
|
// competitor's bare-name matching: two stores that each define `reset` do not
|
|
// collide, because the placeholder carries the specific store binding and the
|
|
// pass prefers the candidate in the caller's file (then a unique binding match,
|
|
// then a singleton). Returns the number of call edges landed on an action.
|
|
func ResolveStoreFactoryCalls(g graph.Store) int {
|
|
if g == nil {
|
|
return 0
|
|
}
|
|
// index: binding → member → action nodes (multiple files may reuse the
|
|
// same binding name, hence a slice).
|
|
index := map[string]map[string][]*graph.Node{}
|
|
for _, n := range nodesByKindsOrAll(g, graph.KindFunction) {
|
|
if n == nil || n.Meta == nil {
|
|
continue
|
|
}
|
|
binding, _ := n.Meta["store_factory"].(string)
|
|
if binding == "" {
|
|
continue
|
|
}
|
|
member, _ := n.Meta["store_member"].(string)
|
|
if member == "" {
|
|
member = n.Name
|
|
}
|
|
if index[binding] == nil {
|
|
index[binding] = map[string][]*graph.Node{}
|
|
}
|
|
index[binding][member] = append(index[binding][member], n)
|
|
}
|
|
if len(index) == 0 {
|
|
return 0
|
|
}
|
|
|
|
resolved := 0
|
|
var reindex []graph.EdgeReindex
|
|
for e := range g.EdgesByKind(graph.EdgeCalls) {
|
|
if e == nil || e.Meta == nil {
|
|
continue
|
|
}
|
|
if v, _ := e.Meta["via"].(string); v != storeFactoryVia {
|
|
continue
|
|
}
|
|
binding, _ := e.Meta["store_binding"].(string)
|
|
action, _ := e.Meta["store_action"].(string)
|
|
if binding == "" || action == "" {
|
|
continue
|
|
}
|
|
cands := sameBoundaryCandidates(g, e.From, index[binding][action])
|
|
target := pickStoreAction(g, e, cands)
|
|
|
|
want := "unresolved::*." + action
|
|
if target != nil {
|
|
want = target.ID
|
|
}
|
|
if e.To == want {
|
|
if target != nil {
|
|
resolved++
|
|
}
|
|
continue
|
|
}
|
|
oldTo := e.To
|
|
e.To = want
|
|
if target != nil {
|
|
e.Origin = graph.OriginASTInferred
|
|
e.Confidence = 0.75
|
|
e.ConfidenceLabel = graph.ConfidenceLabelFor(graph.EdgeCalls, 0.75)
|
|
StampSynthesized(e, SynthStoreFactory)
|
|
resolved++
|
|
} else {
|
|
e.Origin = graph.OriginASTInferred
|
|
e.Confidence = 0
|
|
e.ConfidenceLabel = ""
|
|
UnstampSynthesized(e)
|
|
}
|
|
reindex = append(reindex, graph.EdgeReindex{Edge: e, OldTo: oldTo})
|
|
}
|
|
if len(reindex) > 0 {
|
|
g.ReindexEdges(reindex)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
// pickStoreAction disambiguates action candidates for a call: prefer the
|
|
// candidate defined in the same file as the `useXStore` getter the call's
|
|
// store binding resolves to, then the candidate in the caller's file, then a
|
|
// singleton. Returns nil when the choice is ambiguous (never guesses).
|
|
func pickStoreAction(g graph.Store, call *graph.Edge, cands []*graph.Node) *graph.Node {
|
|
switch len(cands) {
|
|
case 0:
|
|
return nil
|
|
case 1:
|
|
return cands[0]
|
|
}
|
|
// Multiple stores reuse this binding name across files. The strongest
|
|
// signal is the store DEFINITION file: the call's `store_binding` names a
|
|
// `useXStore` getter, which lives in exactly one file. Prefer the candidate
|
|
// action defined there — it is the store the binding actually refers to,
|
|
// regardless of where the call sits.
|
|
binding, _ := call.Meta["store_binding"].(string)
|
|
if getterFile := storeGetterFile(g, binding); getterFile != "" {
|
|
var inGetterFile *graph.Node
|
|
for _, c := range cands {
|
|
if c.FilePath == getterFile {
|
|
if inGetterFile != nil {
|
|
inGetterFile = nil // two candidates in the getter's file: no win
|
|
break
|
|
}
|
|
inGetterFile = c
|
|
}
|
|
}
|
|
if inGetterFile != nil {
|
|
return inGetterFile
|
|
}
|
|
}
|
|
// No definition-file signal. Fall back to the caller's file: prefer the
|
|
// candidate co-located with the call; otherwise it's ambiguous.
|
|
callerFile := ""
|
|
if cn := g.GetNode(call.From); cn != nil {
|
|
callerFile = cn.FilePath
|
|
}
|
|
var sameFile *graph.Node
|
|
for _, c := range cands {
|
|
if c.FilePath == callerFile && callerFile != "" {
|
|
if sameFile != nil {
|
|
return nil // two same-file candidates: ambiguous
|
|
}
|
|
sameFile = c
|
|
}
|
|
}
|
|
return sameFile
|
|
}
|
|
|
|
// storeGetterFile returns the file that defines the `useXStore` getter named by
|
|
// binding — the variable/function/const a `defineStore` / `create` call is
|
|
// assigned to. The getter is the store DEFINITION anchor, so its file pins which
|
|
// store a binding refers to even when the same binding name is reused across
|
|
// files. Returns "" when no getter, or more than one, carries the name (no
|
|
// unambiguous definition site).
|
|
func storeGetterFile(g graph.Store, binding string) string {
|
|
if g == nil || binding == "" {
|
|
return ""
|
|
}
|
|
file := ""
|
|
for _, n := range nodesByKindsOrAll(g, graph.KindVariable, graph.KindFunction, graph.KindConstant) {
|
|
if n == nil || n.Name != binding || n.FilePath == "" {
|
|
continue
|
|
}
|
|
// A getter node anchors a store: it is the assignment target of the
|
|
// factory call, never itself a store-factory action.
|
|
if n.Meta != nil {
|
|
if _, isAction := n.Meta["store_factory"]; isAction {
|
|
continue
|
|
}
|
|
}
|
|
if file != "" && file != n.FilePath {
|
|
return "" // binding defined in two files: ambiguous
|
|
}
|
|
file = n.FilePath
|
|
}
|
|
return file
|
|
}
|