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
264 lines
7.6 KiB
Go
264 lines
7.6 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"
|
|
)
|
|
|
|
// Object-literal command/handler registry dispatch detection. A registry
|
|
// maps keys to handler classes in an object literal
|
|
// (`this.commands = {[Cmd.ADD]: AddCommand, [Cmd.RM]: RmCommand}`) and is
|
|
// invoked by computed member + construction + method call
|
|
// (`new this.commands[cmd]().execute()`). The static call graph cannot see
|
|
// which handler runs because the key is dynamic. This pass records the
|
|
// registered classes and stamps a placeholder EdgeCalls from the
|
|
// dispatching function to each handler class; the resolver's
|
|
// ResolveObjectRegistryCalls binds each to the class's invoked method.
|
|
// Shared by the JavaScript and TypeScript extractors.
|
|
|
|
// objectRegistryViaTag must match resolver.objectRegistryVia — the
|
|
// languages package does not import resolver, so the two agree by value.
|
|
const objectRegistryViaTag = "object-registry"
|
|
|
|
// objectRegistryMethods are the handler entry-point methods a registry
|
|
// dispatch invokes on the constructed handler.
|
|
var objectRegistryMethods = map[string]bool{
|
|
"execute": true,
|
|
"run": true,
|
|
"handle": true,
|
|
}
|
|
|
|
// captureObjectRegistryDispatches finds object-literal handler registries
|
|
// and the computed-member dispatch sites that invoke them, stamping a
|
|
// placeholder EdgeCalls per registered class. Runs at the tail of Extract
|
|
// (after all nodes exist) so the dispatcher's enclosing function resolves.
|
|
// Minified bundles are skipped — their single-line, mangled shape produces
|
|
// no reliable registry.
|
|
func captureObjectRegistryDispatches(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
|
|
if root == nil || result == nil || objectRegistryLooksMinified(src) {
|
|
return
|
|
}
|
|
|
|
// 1. Collect registries: binding name → registered handler classes.
|
|
registries := map[string][]string{}
|
|
objectRegistryWalk(root, func(n *sitter.Node) {
|
|
if n.Type() != "object" {
|
|
return
|
|
}
|
|
binding, ok := objectRegistryBinding(n, src)
|
|
if !ok {
|
|
return
|
|
}
|
|
if classes := objectRegistryEntries(n, src); len(classes) > 0 {
|
|
registries[binding] = append(registries[binding], classes...)
|
|
}
|
|
})
|
|
if len(registries) == 0 {
|
|
return
|
|
}
|
|
|
|
// 2. Find dispatch sites: a computed member on a registry binding,
|
|
// invoked (constructed + method, or directly called).
|
|
funcRanges := buildFuncRanges(result)
|
|
seen := map[string]bool{}
|
|
tagged := map[string]bool{}
|
|
objectRegistryWalk(root, func(n *sitter.Node) {
|
|
if n.Type() != "subscript_expression" {
|
|
return
|
|
}
|
|
binding := objectRegistrySubscriptName(n, src)
|
|
classes := registries[binding]
|
|
if len(classes) == 0 {
|
|
return
|
|
}
|
|
method, isCall := objectRegistryDispatchMethod(n, src)
|
|
if !isCall {
|
|
return
|
|
}
|
|
line := int(n.StartPoint().Row) + 1
|
|
from := findEnclosingFunc(funcRanges, line)
|
|
if from == "" {
|
|
return
|
|
}
|
|
for _, c := range classes {
|
|
k := from + "\x00" + c + "\x00" + method
|
|
if seen[k] {
|
|
continue
|
|
}
|
|
seen[k] = true
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: from,
|
|
To: "unresolved::*." + c,
|
|
Kind: graph.EdgeCalls,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Meta: map[string]any{
|
|
"via": objectRegistryViaTag,
|
|
"registry_binding": binding,
|
|
"registry_value": c,
|
|
"registry_method": method,
|
|
},
|
|
})
|
|
}
|
|
tagged[binding] = true
|
|
})
|
|
|
|
// 3. Tag the registry var/field node so the registry is discoverable.
|
|
if len(tagged) == 0 {
|
|
return
|
|
}
|
|
for _, nd := range result.Nodes {
|
|
if nd == nil || !tagged[nd.Name] {
|
|
continue
|
|
}
|
|
switch nd.Kind {
|
|
case graph.KindVariable, graph.KindConstant, graph.KindField:
|
|
if nd.Meta == nil {
|
|
nd.Meta = map[string]any{}
|
|
}
|
|
nd.Meta["registry"] = nd.Name
|
|
}
|
|
}
|
|
}
|
|
|
|
// objectRegistryBinding returns the variable/field name an object literal
|
|
// is assigned to: `const commands = {...}`, `commands = {...}`, or
|
|
// `this.commands = {...}`.
|
|
func objectRegistryBinding(obj *sitter.Node, src []byte) (string, bool) {
|
|
p := obj.Parent()
|
|
if p == nil {
|
|
return "", false
|
|
}
|
|
switch p.Type() {
|
|
case "variable_declarator":
|
|
if n := p.ChildByFieldName("name"); n != nil && n.Type() == "identifier" {
|
|
return n.Content(src), true
|
|
}
|
|
case "assignment_expression":
|
|
left := p.ChildByFieldName("left")
|
|
if left == nil {
|
|
return "", false
|
|
}
|
|
switch left.Type() {
|
|
case "identifier":
|
|
return left.Content(src), true
|
|
case "member_expression":
|
|
if prop := left.ChildByFieldName("property"); prop != nil {
|
|
return prop.Content(src), true
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// objectRegistryEntries returns the handler-class names of a registry
|
|
// object literal — the identifier values of its top-level (depth-1) pairs,
|
|
// for both plain (`add: AddCommand`) and computed (`[Cmd.ADD]: AddCommand`)
|
|
// keys. Non-identifier values (nested objects, calls) are skipped.
|
|
func objectRegistryEntries(obj *sitter.Node, src []byte) []string {
|
|
var out []string
|
|
for i, _nc := 0, int(obj.NamedChildCount()); i < _nc; i++ {
|
|
pair := obj.NamedChild(i)
|
|
if pair == nil || pair.Type() != "pair" {
|
|
continue
|
|
}
|
|
if val := pair.ChildByFieldName("value"); val != nil && val.Type() == "identifier" {
|
|
out = append(out, val.Content(src))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// objectRegistrySubscriptName returns the last identifier of a computed
|
|
// member's object: `commands` for `commands[k]` and for `this.commands[k]`.
|
|
func objectRegistrySubscriptName(sub *sitter.Node, src []byte) string {
|
|
obj := sub.ChildByFieldName("object")
|
|
if obj == nil {
|
|
return ""
|
|
}
|
|
switch obj.Type() {
|
|
case "identifier":
|
|
return obj.Content(src)
|
|
case "member_expression":
|
|
if prop := obj.ChildByFieldName("property"); prop != nil {
|
|
return prop.Content(src)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// objectRegistryDispatchMethod walks up from a registry subscript to the
|
|
// invoking call, returning the entry-point method (execute/run/handle) and
|
|
// whether the subscript is actually invoked. It climbs through the
|
|
// constructor (`new registry[k]()`) to the `.execute()` member call, and
|
|
// also accepts a bare `registry[k]()` direct call.
|
|
func objectRegistryDispatchMethod(sub *sitter.Node, src []byte) (method string, isCall bool) {
|
|
cur := sub
|
|
for i := 0; i < 4 && cur != nil; i++ {
|
|
p := cur.Parent()
|
|
if p == nil {
|
|
return "", false
|
|
}
|
|
switch p.Type() {
|
|
case "new_expression", "parenthesized_expression":
|
|
cur = p
|
|
continue
|
|
case "member_expression":
|
|
prop := p.ChildByFieldName("property")
|
|
gp := p.Parent()
|
|
if prop != nil && gp != nil && gp.Type() == "call_expression" {
|
|
if m := prop.Content(src); objectRegistryMethods[m] {
|
|
return m, true
|
|
}
|
|
}
|
|
return "", false
|
|
case "call_expression":
|
|
// Bare `registry[k]()` / `new registry[k]()()` direct dispatch.
|
|
return "", true
|
|
default:
|
|
return "", false
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// objectRegistryLooksMinified reports whether the source looks like a
|
|
// minified bundle (one giant line or almost no newlines), where a
|
|
// best-effort registry parse would be noise.
|
|
func objectRegistryLooksMinified(src []byte) bool {
|
|
if len(src) == 0 {
|
|
return false
|
|
}
|
|
maxLine, cur, newlines := 0, 0, 0
|
|
for _, b := range src {
|
|
if b == '\n' {
|
|
if cur > maxLine {
|
|
maxLine = cur
|
|
}
|
|
cur = 0
|
|
newlines++
|
|
continue
|
|
}
|
|
cur++
|
|
}
|
|
if cur > maxLine {
|
|
maxLine = cur
|
|
}
|
|
if maxLine > 2000 {
|
|
return true
|
|
}
|
|
return newlines == 0 && len(src) > 500
|
|
}
|
|
|
|
// objectRegistryWalk visits n and all its named descendants.
|
|
func objectRegistryWalk(n *sitter.Node, fn func(*sitter.Node)) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
fn(n)
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
objectRegistryWalk(n.NamedChild(i), fn)
|
|
}
|
|
}
|