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
160 lines
5.4 KiB
Go
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)
|
|
}
|