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

160 lines
5.4 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"
)
// emitTSThrowsEdges walks a TypeScript function body for throw_statement
// nodes and emits one EdgeThrows per distinct exception type. The
// extractor recognises the three idiomatic shapes:
//
// throw new MyError("msg") -> "MyError"
// throw MyError -> "MyError"
// throw errs.MyError -> "MyError" (trailing identifier of the
// member-expression chain)
//
// String / numeric / template-literal throws (`throw "oops"`) are
// skipped — they don't carry a referenceable type and would just create
// `unresolved::string` noise in the graph. Re-throws (`throw caught` in
// a catch clause that captured a typed name) fall through to the
// identifier branch and resolve like any other.
//
// Origin is OriginASTInferred because TS doesn't enforce a checked-
// exception contract — the body scan is a best-effort summary of every
// type that can propagate, not a proof. The resolver upgrades to
// OriginLSPResolved when the language server confirms the target.
//
// Nested function / arrow / method bodies are skipped: their throws
// belong to those inner scopes, not the enclosing function. This
// matches the Python and Rust emitters' walk policy.
func emitTSThrowsEdges(funcNode *sitter.Node, src []byte, fromID, filePath string, result *parser.ExtractionResult) {
if funcNode == nil || fromID == "" {
return
}
body := tsFunctionBody(funcNode)
if body == nil {
return
}
seen := map[string]bool{}
tsWalkThrows(body, src, fromID, filePath, seen, result)
}
// tsWalkThrows is the recursive descent helper for emitTSThrowsEdges.
// Pulled out to keep the entry-point readable and to make the
// "don't descend into nested function bodies" rule visible at the
// single place that enforces it.
func tsWalkThrows(node *sitter.Node, src []byte, fromID, filePath string, seen map[string]bool, result *parser.ExtractionResult) {
if node == nil {
return
}
if node.Type() == "throw_statement" {
name := tsThrowTypeName(node, src)
if name != "" && !seen[name] {
seen[name] = true
result.Edges = append(result.Edges, &graph.Edge{
From: fromID,
To: "unresolved::" + name,
Kind: graph.EdgeThrows,
FilePath: filePath,
Line: int(node.StartPoint().Row) + 1,
Origin: graph.OriginASTInferred,
})
}
return
}
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
c := node.NamedChild(i)
if c == nil {
continue
}
// Skip nested scopes — their throws belong to those scopes.
switch c.Type() {
case "function_declaration", "function_expression", "arrow_function",
"method_definition", "generator_function_declaration",
"class_declaration", "class_expression":
continue
}
tsWalkThrows(c, src, fromID, filePath, seen, result)
}
}
// tsThrowTypeName returns the typed name being thrown, or "" when the
// throw expression isn't a referenceable type. Handles:
//
// throw new Foo(...) / throw new ns.Foo(...) — new_expression
// throw Foo / throw Foo.SubFoo — identifier / member_expression
//
// Anything else (string literal, template string, number, undefined,
// computed property access) returns "".
func tsThrowTypeName(throwNode *sitter.Node, src []byte) string {
if throwNode == nil {
return ""
}
for i, _nc := 0, int(throwNode.NamedChildCount()); i < _nc; i++ {
c := throwNode.NamedChild(i)
if c == nil {
continue
}
switch c.Type() {
case "new_expression":
ctor := c.ChildByFieldName("constructor")
if ctor == nil {
continue
}
return tsTrailingIdentifier(ctor, src)
case "identifier":
return c.Content(src)
case "member_expression":
// `errs.MyError.Variant` → return the trailing identifier.
// Captures the exception's leaf name regardless of
// namespace depth.
return tsTrailingIdentifier(c, src)
case "call_expression":
// `throw makeError(...)` — record the constructor-style
// identifier so the resolver can still upgrade origin once
// the call's return type is known. Skip when the callee
// isn't a bare identifier (avoids `throw a.b.c()` producing
// `c` as a throw type, which is misleading).
fn := c.ChildByFieldName("function")
if fn == nil || fn.Type() != "identifier" {
continue
}
return fn.Content(src)
}
}
return ""
}
// tsTrailingIdentifier walks a member_expression / nested_identifier
// chain returning the last (rightmost) identifier component. For
// `errs.kinds.MyError` returns "MyError"; for bare `Foo` returns
// "Foo". Returns "" for any node that isn't an identifier or a chain.
func tsTrailingIdentifier(node *sitter.Node, src []byte) string {
if node == nil {
return ""
}
switch node.Type() {
case "identifier", "type_identifier":
return node.Content(src)
case "member_expression":
// Walk the property side first (the rightmost segment of the chain).
if prop := node.ChildByFieldName("property"); prop != nil {
if name := tsTrailingIdentifier(prop, src); name != "" {
return name
}
}
case "nested_identifier":
// `ns.Inner.Leaf` — last NamedChild is the leaf.
count := int(node.NamedChildCount())
if count == 0 {
return ""
}
return tsTrailingIdentifier(node.NamedChild(count-1), src)
}
// Fall back to the verbatim content so unusual shapes still
// surface SOMETHING the resolver can dedupe against.
return node.Content(src)
}