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

179 lines
5.5 KiB
Go

package languages
import (
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// Redux Toolkit createAsyncThunk dispatch-chain detection. A thunk's
// payload-creator body dispatches other actions/thunks
// (`dispatch(setLoading())`); those indirect calls are invisible to the
// static call graph because the thunk is registered, not directly called.
// This pass tags the thunk node with Meta["redux_thunk"] and stamps a
// placeholder EdgeCalls per inner dispatch, which the resolver's
// ResolveReduxThunkCalls binds to the action/thunk node. Shared by the
// JavaScript and TypeScript extractors.
// reduxThunkFactories are the createAsyncThunk-family callees whose return
// value is a thunk, matched on the callee's last identifier.
var reduxThunkFactories = map[string]bool{
"createAsyncThunk": true,
"createThunk": true,
}
// reduxThunkViaTag must match resolver.reduxThunkVia — the languages
// package does not import resolver, so the two agree on the via tag by
// value.
const reduxThunkViaTag = "redux-thunk"
// reduxThunkDispatchCap bounds the inner dispatches mined from one thunk
// body so a pathological payload creator does not fan out unbounded.
const reduxThunkDispatchCap = 24
// captureReduxThunkDispatches finds `const X = createAsyncThunk(type,
// (arg, {dispatch}) => { dispatch(Y()); ... })` declarators, tags X's
// node with Meta["redux_thunk"], and stamps a placeholder EdgeCalls from X
// to each dispatched action Y. Runs at the tail of Extract, after all
// nodes exist, so it can find and tag the thunk's own node.
func captureReduxThunkDispatches(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
if root == nil || result == nil {
return
}
reduxThunkWalk(root, func(n *sitter.Node) {
if n.Type() != "variable_declarator" {
return
}
nameNode := n.ChildByFieldName("name")
if nameNode == nil || nameNode.Type() != "identifier" {
return
}
init := n.ChildByFieldName("value")
if init == nil || init.Type() != "call_expression" {
return
}
if !reduxThunkFactories[jsCalleeLastName(init.ChildByFieldName("function"), src)] {
return
}
creator := reduxThunkPayloadCreator(init)
if creator == nil {
return
}
thunkID := reduxThunkTag(result, filePath, nameNode.Content(src))
emitted := 0
reduxThunkWalk(creator, func(c *sitter.Node) {
if emitted >= reduxThunkDispatchCap || c.Type() != "call_expression" {
return
}
if jsCalleeLastName(c.ChildByFieldName("function"), src) != "dispatch" {
return
}
inner := reduxThunkFirstCallArg(c)
if inner == nil {
return
}
action := jsCalleeLastName(inner.ChildByFieldName("function"), src)
if action == "" {
return
}
result.Edges = append(result.Edges, &graph.Edge{
From: thunkID,
To: "unresolved::*." + action,
Kind: graph.EdgeCalls,
FilePath: filePath,
Line: int(c.StartPoint().Row) + 1,
Meta: map[string]any{
"via": reduxThunkViaTag,
"thunk_dispatch": action,
},
})
emitted++
})
})
}
// reduxThunkTag stamps Meta["redux_thunk"] on the thunk const's node and
// returns its ID. It prefers the exact-ID node (filePath::name), falls
// back to a same-named variable/constant/function node when the ID was
// disambiguated, and mints a function node as a last resort so the
// placeholder edge's From is always a real node.
func reduxThunkTag(result *parser.ExtractionResult, filePath, name string) string {
want := filePath + "::" + name
var fallback *graph.Node
for _, nd := range result.Nodes {
if nd == nil {
continue
}
if nd.ID == want {
reduxStampThunk(nd, name)
return nd.ID
}
if fallback == nil && nd.Name == name &&
(nd.Kind == graph.KindVariable || nd.Kind == graph.KindConstant || nd.Kind == graph.KindFunction) {
fallback = nd
}
}
if fallback != nil {
reduxStampThunk(fallback, name)
return fallback.ID
}
node := &graph.Node{ID: want, Kind: graph.KindFunction, Name: name, FilePath: filePath, Meta: map[string]any{"redux_thunk": name}}
result.Nodes = append(result.Nodes, node)
return want
}
func reduxStampThunk(n *graph.Node, name string) {
if n.Meta == nil {
n.Meta = map[string]any{}
}
n.Meta["redux_thunk"] = name
}
// reduxThunkPayloadCreator returns the first function argument of a
// createAsyncThunk call — the payload creator that holds the dispatch
// chain (the string type prefix is argument 0).
func reduxThunkPayloadCreator(call *sitter.Node) *sitter.Node {
args := call.ChildByFieldName("arguments")
if args == nil {
return nil
}
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "arrow_function", "function_expression", "function", "function_declaration":
return c
}
}
return nil
}
// reduxThunkFirstCallArg returns the first argument of a call when it is
// itself a call expression — the dispatched action creator invocation in
// `dispatch(setLoading())`.
func reduxThunkFirstCallArg(call *sitter.Node) *sitter.Node {
args := call.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() == 0 {
return nil
}
first := args.NamedChild(0)
if first != nil && first.Type() == "call_expression" {
return first
}
return nil
}
// reduxThunkWalk visits n and all its named descendants.
func reduxThunkWalk(n *sitter.Node, fn func(*sitter.Node)) {
if n == nil {
return
}
fn(n)
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
reduxThunkWalk(n.NamedChild(i), fn)
}
}