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
334 lines
9.4 KiB
Go
334 lines
9.4 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// Vuex string-keyed dispatch/commit binding. A Vuex store declares actions
|
|
// and mutations inside a `new Vuex.Store({ modules, actions, mutations })`
|
|
// config, namespaced by module path, and they are invoked by string key
|
|
// (`this.$store.dispatch('user/login')`, `commit('user/SET_TOKEN')`). This
|
|
// pass tags each action/mutation node with its namespace + name + kind and
|
|
// stamps a placeholder EdgeCalls per dispatch/commit site, which the
|
|
// resolver's ResolveVuexDispatchCalls binds with namespace disambiguation.
|
|
// Shared by the JS and TS extractors (and .vue SFCs via Vue delegation).
|
|
|
|
// vuexDispatchViaTag must match resolver.vuexDispatchVia — the languages
|
|
// package does not import resolver, so the two agree by value.
|
|
const vuexDispatchViaTag = "vuex-dispatch"
|
|
|
|
// captureVuexDispatch tags Vuex action/mutation nodes and stamps the
|
|
// dispatch/commit placeholders. Runs at the tail of Extract so the action
|
|
// function nodes already exist.
|
|
func captureVuexDispatch(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
|
|
if root == nil || result == nil {
|
|
return
|
|
}
|
|
// Index function/method nodes by start line, so a Vuex action found in
|
|
// the config AST can be matched back to its node.
|
|
nodesByLine := map[int][]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
if n != nil && (n.Kind == graph.KindFunction || n.Kind == graph.KindMethod) {
|
|
nodesByLine[n.StartLine] = append(nodesByLine[n.StartLine], n)
|
|
}
|
|
}
|
|
|
|
tagged := false
|
|
vuexWalk(root, func(n *sitter.Node) {
|
|
obj := vuexConfigObject(n, src)
|
|
if obj == nil {
|
|
return
|
|
}
|
|
processVuexConfig(obj, "", src, nodesByLine)
|
|
tagged = true
|
|
})
|
|
if !tagged {
|
|
return
|
|
}
|
|
|
|
// Dispatch / commit call sites.
|
|
funcRanges := buildFuncRanges(result)
|
|
seen := map[string]bool{}
|
|
vuexWalk(root, func(call *sitter.Node) {
|
|
if call.Type() != "call_expression" {
|
|
return
|
|
}
|
|
callee := jsCalleeLastName(call.ChildByFieldName("function"), src)
|
|
switch callee {
|
|
case "dispatch", "commit":
|
|
vuexEmitDispatch(result, call, callee, src, funcRanges, filePath, seen)
|
|
case "mapActions", "mapMutations":
|
|
vuexEmitMapHelper(result, call, callee, src, funcRanges, filePath, seen)
|
|
}
|
|
})
|
|
}
|
|
|
|
// vuexConfigObject returns the Vuex store config object of a
|
|
// `new Vuex.Store({...})` / `new Store({...})` / `createStore({...})`
|
|
// expression, gated on the object carrying a Vuex-specific `mutations` or
|
|
// `modules` key (so a Redux `createStore(reducer)` is not mistaken for one).
|
|
func vuexConfigObject(n *sitter.Node, src []byte) *sitter.Node {
|
|
var callee string
|
|
var args *sitter.Node
|
|
switch n.Type() {
|
|
case "new_expression":
|
|
callee = jsCalleeLastName(n.ChildByFieldName("constructor"), src)
|
|
args = n.ChildByFieldName("arguments")
|
|
case "call_expression":
|
|
callee = jsCalleeLastName(n.ChildByFieldName("function"), src)
|
|
args = n.ChildByFieldName("arguments")
|
|
default:
|
|
return nil
|
|
}
|
|
if callee != "Store" && callee != "createStore" {
|
|
return nil
|
|
}
|
|
if args == nil {
|
|
return nil
|
|
}
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
obj := args.NamedChild(i)
|
|
if obj == nil || obj.Type() != "object" {
|
|
continue
|
|
}
|
|
if vuexHasKey(obj, "mutations", src) || vuexHasKey(obj, "modules", src) {
|
|
return obj
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// processVuexConfig walks a store/module config object, tagging its actions
|
|
// and mutations with the running namespace prefix and recursing into nested
|
|
// modules (whose key joins the prefix only when `namespaced: true`).
|
|
func processVuexConfig(obj *sitter.Node, prefix string, src []byte, nodesByLine map[int][]*graph.Node) {
|
|
namespace := strings.TrimSuffix(prefix, "/")
|
|
for i, _nc := 0, int(obj.NamedChildCount()); i < _nc; i++ {
|
|
pair := obj.NamedChild(i)
|
|
if pair == nil || pair.Type() != "pair" {
|
|
continue
|
|
}
|
|
key := rtkPairKey(pair, src)
|
|
val := pair.ChildByFieldName("value")
|
|
if val == nil {
|
|
continue
|
|
}
|
|
switch key {
|
|
case "actions", "mutations":
|
|
if val.Type() == "object" {
|
|
kind := "action"
|
|
if key == "mutations" {
|
|
kind = "mutation"
|
|
}
|
|
vuexTagMembers(val, namespace, kind, src, nodesByLine)
|
|
}
|
|
case "modules":
|
|
if val.Type() == "object" {
|
|
for j, _nc := 0, int(val.NamedChildCount()); j < _nc; j++ {
|
|
mod := val.NamedChild(j)
|
|
if mod == nil || mod.Type() != "pair" {
|
|
continue
|
|
}
|
|
mobj := mod.ChildByFieldName("value")
|
|
if mobj == nil || mobj.Type() != "object" {
|
|
continue
|
|
}
|
|
sub := prefix
|
|
if vuexIsNamespaced(mobj, src) {
|
|
sub = prefix + rtkPairKey(mod, src) + "/"
|
|
}
|
|
processVuexConfig(mobj, sub, src, nodesByLine)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// vuexTagMembers stamps the action/mutation functions of an object literal
|
|
// with their Vuex namespace, name, and kind.
|
|
func vuexTagMembers(obj *sitter.Node, namespace, kind string, src []byte, nodesByLine map[int][]*graph.Node) {
|
|
for i, _nc := 0, int(obj.NamedChildCount()); i < _nc; i++ {
|
|
c := obj.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
var member string
|
|
switch c.Type() {
|
|
case "method_definition":
|
|
if name := c.ChildByFieldName("name"); name != nil {
|
|
member = name.Content(src)
|
|
}
|
|
case "pair":
|
|
val := c.ChildByFieldName("value")
|
|
if val == nil || !vuexIsFnLike(val.Type()) {
|
|
continue
|
|
}
|
|
member = rtkPairKey(c, src)
|
|
default:
|
|
continue
|
|
}
|
|
if member == "" {
|
|
continue
|
|
}
|
|
line := int(c.StartPoint().Row) + 1
|
|
for _, n := range nodesByLine[line] {
|
|
if n.Name != member && !strings.HasSuffix(n.Name, "."+member) {
|
|
continue
|
|
}
|
|
if n.Meta == nil {
|
|
n.Meta = map[string]any{}
|
|
}
|
|
n.Meta["vuex_action"] = member
|
|
n.Meta["vuex_namespace"] = namespace
|
|
n.Meta["vuex_kind"] = kind
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// vuexEmitDispatch stamps the placeholder for a dispatch('ns/name') or
|
|
// commit('ns/NAME') call with a string-literal key.
|
|
func vuexEmitDispatch(result *parser.ExtractionResult, call *sitter.Node, callee string, src []byte, funcRanges []funcRange, filePath string, seen map[string]bool) {
|
|
key := vuexFirstStringArg(call, src)
|
|
if key == "" {
|
|
return
|
|
}
|
|
kind := "action"
|
|
if callee == "commit" {
|
|
kind = "mutation"
|
|
}
|
|
name := key
|
|
if i := strings.LastIndex(key, "/"); i >= 0 {
|
|
name = key[i+1:]
|
|
}
|
|
line := int(call.StartPoint().Row) + 1
|
|
from := findEnclosingFunc(funcRanges, line)
|
|
vuexAppendPlaceholder(result, from, name, key, kind, filePath, line, seen)
|
|
}
|
|
|
|
// vuexEmitMapHelper stamps placeholders for a mapActions/mapMutations
|
|
// helper: an optional namespace string followed by an array of names.
|
|
func vuexEmitMapHelper(result *parser.ExtractionResult, call *sitter.Node, callee string, src []byte, funcRanges []funcRange, filePath string, seen map[string]bool) {
|
|
kind := "action"
|
|
if callee == "mapMutations" {
|
|
kind = "mutation"
|
|
}
|
|
args := call.ChildByFieldName("arguments")
|
|
if args == nil {
|
|
return
|
|
}
|
|
namespace := ""
|
|
var names []string
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
a := args.NamedChild(i)
|
|
if a == nil {
|
|
continue
|
|
}
|
|
switch a.Type() {
|
|
case "string":
|
|
namespace = jsStringLiteralContent(a, src)
|
|
case "array":
|
|
for j, _nc := 0, int(a.NamedChildCount()); j < _nc; j++ {
|
|
if el := a.NamedChild(j); el != nil && el.Type() == "string" {
|
|
if s := jsStringLiteralContent(el, src); s != "" {
|
|
names = append(names, s)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(names) == 0 {
|
|
return
|
|
}
|
|
line := int(call.StartPoint().Row) + 1
|
|
from := findEnclosingFunc(funcRanges, line)
|
|
for _, nm := range names {
|
|
key := nm
|
|
if namespace != "" {
|
|
key = namespace + "/" + nm
|
|
}
|
|
vuexAppendPlaceholder(result, from, nm, key, kind, filePath, line, seen)
|
|
}
|
|
}
|
|
|
|
func vuexAppendPlaceholder(result *parser.ExtractionResult, from, name, key, kind, filePath string, line int, seen map[string]bool) {
|
|
if from == "" || name == "" {
|
|
return
|
|
}
|
|
k := from + "\x00" + key + "\x00" + kind
|
|
if seen[k] {
|
|
return
|
|
}
|
|
seen[k] = true
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: from,
|
|
To: "unresolved::*." + name,
|
|
Kind: graph.EdgeCalls,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Meta: map[string]any{
|
|
"via": vuexDispatchViaTag,
|
|
"vuex_key": key,
|
|
"vuex_kind": kind,
|
|
},
|
|
})
|
|
}
|
|
|
|
func vuexHasKey(obj *sitter.Node, key string, src []byte) bool {
|
|
for i, _nc := 0, int(obj.NamedChildCount()); i < _nc; i++ {
|
|
pair := obj.NamedChild(i)
|
|
if pair != nil && pair.Type() == "pair" && rtkPairKey(pair, src) == key {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func vuexIsNamespaced(obj *sitter.Node, src []byte) bool {
|
|
for i, _nc := 0, int(obj.NamedChildCount()); i < _nc; i++ {
|
|
pair := obj.NamedChild(i)
|
|
if pair == nil || pair.Type() != "pair" || rtkPairKey(pair, src) != "namespaced" {
|
|
continue
|
|
}
|
|
if val := pair.ChildByFieldName("value"); val != nil {
|
|
return strings.TrimSpace(val.Content(src)) == "true"
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func vuexIsFnLike(t string) bool {
|
|
switch t {
|
|
case "arrow_function", "function_expression", "function":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func vuexFirstStringArg(call *sitter.Node, src []byte) string {
|
|
args := call.ChildByFieldName("arguments")
|
|
if args == nil || args.NamedChildCount() == 0 {
|
|
return ""
|
|
}
|
|
first := args.NamedChild(0)
|
|
if first != nil && first.Type() == "string" {
|
|
return jsStringLiteralContent(first, src)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// vuexWalk visits n and all its named descendants.
|
|
func vuexWalk(n *sitter.Node, fn func(*sitter.Node)) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
fn(n)
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
vuexWalk(n.NamedChild(i), fn)
|
|
}
|
|
}
|