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

245 lines
7.6 KiB
Go

package languages
import (
"strconv"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// emitTSLocalBindings walks a TypeScript / JavaScript function body
// and materialises a KindLocal node for every introduced binding
// (`let x = …`, `const x = …`, `var x = …`, destructured shorthand,
// for-in/for-of induction vars, catch clause bindings, ...). Each
// binding gets:
//
// - ID `<ownerID>#local:<name>@+<offsetFromOwnerStartLine>`
// (function-relative offset like the Go walker, so an edit
// above the function leaves the IDs stable),
// - Name = the identifier,
// - FilePath / StartLine = the binding's source position,
// - EdgeMemberOf back to the enclosing function so the resolver's
// scope-aware bare-name binding (#81) can find it by walking
// the function's inbound EdgeMemberOf of KindLocal.
//
// TS doesn't (yet) have a dataflow walker analogous to
// emitGoDataflow, so no value_flow / arg_of / returns_to edges
// target these locals today. Their value is semantic parity with
// Go: every introduced binding is a first-class graph node with
// stable identity, ready for the dataflow / scope-resolution
// passes downstream. KindLocal is excluded from BM25 search via
// shouldIndexForSearch so the materialisation doesn't pollute name
// lookups with per-function `err` / `data` / `i` rows.
//
// Mirrors emitGoDataflow's bindLocal helper for the
// node-emission side; the walk shape is TypeScript-specific
// (different AST node types).
func emitTSLocalBindings(ownerID string, ownerStartLine int, body *sitter.Node, src []byte, filePath string, result *parser.ExtractionResult) {
if body == nil || ownerID == "" {
return
}
w := &tsBindingWalker{
ownerID: ownerID,
ownerStartLine: ownerStartLine,
filePath: filePath,
src: src,
result: result,
emitted: map[string]struct{}{},
}
w.walk(body)
}
type tsBindingWalker struct {
ownerID string
ownerStartLine int
filePath string
src []byte
result *parser.ExtractionResult
emitted map[string]struct{}
}
func (w *tsBindingWalker) walk(n *sitter.Node) {
if n == nil {
return
}
switch n.Type() {
case "function_declaration", "method_definition", "function", "arrow_function", "generator_function", "generator_function_declaration", "function_expression":
// Don't descend into nested functions — their bindings
// belong to the inner function's scope. The TS extractor's
// own pass handles each inner function separately.
return
case "lexical_declaration", "variable_declaration":
w.handleVarDecl(n)
// Fall through to children for any nested expressions
// (e.g. an initializer that contains a destructure pattern
// is already captured by handleVarDecl; no extra walk).
return
case "for_in_statement", "for_of_statement":
w.handleForInOf(n)
// Continue into the body to pick up nested declarations.
if body := n.ChildByFieldName("body"); body != nil {
w.walk(body)
}
return
case "catch_clause":
w.handleCatchClause(n)
if body := n.ChildByFieldName("body"); body != nil {
w.walk(body)
}
return
}
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
w.walk(n.NamedChild(i))
}
}
// handleVarDecl visits `let`, `const`, `var` declarations and emits
// a KindLocal node per declarator. Each declarator's `name` field
// is either an identifier (simplest case) or a destructure pattern
// (object_pattern / array_pattern) — for patterns we descend and
// emit one node per shorthand identifier.
func (w *tsBindingWalker) handleVarDecl(decl *sitter.Node) {
for i, _nc := 0, int(decl.NamedChildCount()); i < _nc; i++ {
c := decl.NamedChild(i)
if c == nil || c.Type() != "variable_declarator" {
continue
}
name := c.ChildByFieldName("name")
if name == nil {
continue
}
w.emitFromPattern(name, int(decl.StartPoint().Row)+1)
}
}
// handleForInOf visits `for (const x of items)` / `for (let k in obj)`
// and materialises the induction var(s) declared on the LHS.
func (w *tsBindingWalker) handleForInOf(n *sitter.Node) {
left := n.ChildByFieldName("left")
if left == nil {
return
}
line := int(n.StartPoint().Row) + 1
switch left.Type() {
case "lexical_declaration", "variable_declaration":
w.handleVarDecl(left)
case "identifier":
w.bindLocal(left.Content(w.src), line)
default:
w.emitFromPattern(left, line)
}
}
// handleCatchClause materialises the catch parameter (`catch (err)
// { ... }`). TS supports both an identifier and a destructure
// pattern as the catch binding.
func (w *tsBindingWalker) handleCatchClause(n *sitter.Node) {
param := n.ChildByFieldName("parameter")
if param == nil {
return
}
w.emitFromPattern(param, int(n.StartPoint().Row)+1)
}
// emitFromPattern recursively visits a binding pattern (identifier
// at the leaf; object_pattern / array_pattern in the middle) and
// emits a KindLocal node for every leaf identifier. Shorthand
// (`{ a, b }`) and renamed (`{ a: aliased }`) both produce
// identifier leaves the walker handles uniformly.
func (w *tsBindingWalker) emitFromPattern(node *sitter.Node, line int) {
if node == nil {
return
}
switch node.Type() {
case "identifier", "shorthand_property_identifier_pattern":
w.bindLocal(node.Content(w.src), line)
case "object_pattern", "array_pattern":
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
c := node.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "pair_pattern":
// `{ a: aliased }` — the bound name lives on the
// `value` field.
if v := c.ChildByFieldName("value"); v != nil {
w.emitFromPattern(v, line)
}
case "rest_pattern":
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
w.emitFromPattern(c.NamedChild(j), line)
}
default:
w.emitFromPattern(c, line)
}
}
case "assignment_pattern":
// `let x = 1` inside a destructure — the bound name is on
// the `left` field; the right is the default.
if l := node.ChildByFieldName("left"); l != nil {
w.emitFromPattern(l, line)
}
case "rest_pattern":
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
w.emitFromPattern(node.NamedChild(i), line)
}
}
}
// bindLocal emits the KindLocal node + owner edge. Idempotent on
// the binding ID so a name visited through more than one walk path
// produces exactly one node row.
func (w *tsBindingWalker) bindLocal(name string, line int) {
if name == "" || name == "_" {
return
}
offset := line
if w.ownerStartLine > 0 {
offset = line - w.ownerStartLine + 1
}
id := w.ownerID + "#local:" + name + "@+" + strconv.Itoa(offset)
if _, ok := w.emitted[id]; ok {
return
}
w.emitted[id] = struct{}{}
// Language tag mirrors the file's source language; the
// extractor's caller passes the file path so we recover it
// from the suffix. Defaults to typescript when ambiguous.
lang := "typescript"
switch {
case hasSuffix(w.filePath, ".tsx"):
lang = "tsx"
case hasSuffix(w.filePath, ".jsx"):
lang = "javascript"
case hasSuffix(w.filePath, ".js"), hasSuffix(w.filePath, ".mjs"), hasSuffix(w.filePath, ".cjs"):
lang = "javascript"
}
w.result.Nodes = append(w.result.Nodes, &graph.Node{
ID: id,
Kind: graph.KindLocal,
Name: name,
FilePath: w.filePath,
StartLine: line,
EndLine: line,
Language: lang,
})
w.result.Edges = append(w.result.Edges, &graph.Edge{
From: id,
To: w.ownerID,
Kind: graph.EdgeMemberOf,
FilePath: w.filePath,
Line: line,
Origin: graph.OriginASTResolved,
})
}
func hasSuffix(s, suf string) bool {
if len(s) < len(suf) {
return false
}
return s[len(s)-len(suf):] == suf
}