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

159 lines
4.9 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"
)
// NgRx effects dispatch detection. An effect declares the action(s) it reacts
// to via `createEffect(() => this.actions$.pipe(ofType(SomeAction), ...))`. The
// effect is registered with the EffectsModule, not called, so the static call
// graph never links it to the action it handles. This pass tags the effect node
// with Meta["ngrx_effect"] and stamps a placeholder EdgeCalls per ofType action,
// which the resolver's ResolveNgRxEffects binds to the action node. Shared by
// the JavaScript and TypeScript extractors.
// ngrxEffectViaTag must match resolver.ngrxEffectVia -- the languages package
// does not import resolver, so the two agree on the via tag by value.
const ngrxEffectViaTag = "ngrx-effect"
// ngrxEffectActionCap bounds the ofType actions mined from one effect.
const ngrxEffectActionCap = 16
// captureNgRxEffects finds `name$ = createEffect(() => actions$.pipe(
// ofType(Action), ...))` class properties (and `const name$ = createEffect(...)`
// functional effects), tags the effect node with Meta["ngrx_effect"], and stamps
// a placeholder EdgeCalls from it to each ofType action. Runs at the tail of
// Extract, after all nodes exist, so it can find and tag the effect node.
func captureNgRxEffects(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
if root == nil || result == nil {
return
}
ngrxEffectWalk(root, func(n *sitter.Node) {
var nameNode, init *sitter.Node
switch n.Type() {
case "public_field_definition", "field_definition", "variable_declarator":
nameNode = n.ChildByFieldName("name")
init = n.ChildByFieldName("value")
default:
return
}
if nameNode == nil || init == nil || init.Type() != "call_expression" {
return
}
if jsCalleeLastName(init.ChildByFieldName("function"), src) != "createEffect" {
return
}
name := nameNode.Content(src)
if name == "" {
return
}
var actions []string
seen := map[string]bool{}
ngrxEffectWalk(init, func(c *sitter.Node) {
if len(actions) >= ngrxEffectActionCap || c.Type() != "call_expression" {
return
}
if jsCalleeLastName(c.ChildByFieldName("function"), src) != "ofType" {
return
}
args := c.ChildByFieldName("arguments")
if args == nil {
return
}
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
act := ngrxActionName(args.NamedChild(i), src)
if act == "" || seen[act] {
continue
}
seen[act] = true
actions = append(actions, act)
}
})
if len(actions) == 0 {
return
}
effectID := ngrxEffectTag(result, filePath, name)
line := int(n.StartPoint().Row) + 1
for _, act := range actions {
result.Edges = append(result.Edges, &graph.Edge{
From: effectID,
To: "unresolved::*." + act,
Kind: graph.EdgeCalls,
FilePath: filePath,
Line: line,
Meta: map[string]any{
"via": ngrxEffectViaTag,
"ngrx_action": act,
},
})
}
})
}
// ngrxActionName returns the action name an ofType argument references: a bare
// identifier (`LoadUsers`) or the trailing property of a member access
// (`UserActions.load` -> "load").
func ngrxActionName(n *sitter.Node, src []byte) string {
if n == nil {
return ""
}
switch n.Type() {
case "identifier":
return n.Content(src)
case "member_expression":
if p := n.ChildByFieldName("property"); p != nil {
return p.Content(src)
}
}
return ""
}
// ngrxEffectTag stamps Meta["ngrx_effect"] on the effect's node and returns its
// ID. It prefers the exact-ID node (a functional effect `const name$ = ...`),
// then a same-named class property / variable, and mints a node as a last resort
// so the placeholder edge's From is always real.
func ngrxEffectTag(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 {
ngrxStampEffect(nd, name)
return nd.ID
}
if fallback == nil && nd.Name == name &&
(nd.Kind == graph.KindVariable || nd.Kind == graph.KindConstant || nd.Kind == graph.KindField || nd.Kind == graph.KindFunction) {
fallback = nd
}
}
if fallback != nil {
ngrxStampEffect(fallback, name)
return fallback.ID
}
node := &graph.Node{ID: want, Kind: graph.KindVariable, Name: name, FilePath: filePath, Meta: map[string]any{"ngrx_effect": name}}
result.Nodes = append(result.Nodes, node)
return want
}
func ngrxStampEffect(n *graph.Node, name string) {
if n.Meta == nil {
n.Meta = map[string]any{}
}
n.Meta["ngrx_effect"] = name
}
// ngrxEffectWalk visits n and all its named descendants.
func ngrxEffectWalk(n *sitter.Node, fn func(*sitter.Node)) {
if n == nil {
return
}
fn(n)
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
ngrxEffectWalk(n.NamedChild(i), fn)
}
}