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
280 lines
9.0 KiB
Go
280 lines
9.0 KiB
Go
package languages
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// Express/Node inline-arrow route-handler body attribution. A route whose
|
|
// handler is an inline arrow (`app.get('/u', (req,res) => { svc.list() })`)
|
|
// has no named function for the call graph to attribute the body's calls to,
|
|
// so a trace from the route to the service it calls is lost. This pass
|
|
// materialises a synthetic handler node anchored to the route call line and
|
|
// attributes the arrow body's *application* calls to it (the framework's
|
|
// req/res/next helpers and JS built-ins are filtered out). The contracts
|
|
// layer anchors the route Contract's SymbolID to the same node, so the route
|
|
// connects through the anonymous handler to the services it invokes.
|
|
|
|
// expressRouteVerbs are the HTTP verbs (and middleware mounts) a route call
|
|
// uses as its method name.
|
|
var expressRouteVerbs = map[string]bool{
|
|
"get": true, "post": true, "put": true, "delete": true, "patch": true,
|
|
"options": true, "head": true, "all": true, "use": true,
|
|
}
|
|
|
|
// expressRouteReceivers are the common router/app receiver names.
|
|
var expressRouteReceivers = map[string]bool{
|
|
"app": true, "router": true, "fastify": true, "server": true, "api": true, "route": true,
|
|
}
|
|
|
|
// expressInlineCallCap bounds the calls attributed from one handler body.
|
|
const expressInlineCallCap = 64
|
|
|
|
// ExpressInlineHandlerNodeID is the deterministic id of the synthetic
|
|
// handler node a route's inline arrow is attributed to. The contracts layer
|
|
// reconstructs it from the route call line to anchor the route Contract.
|
|
func ExpressInlineHandlerNodeID(filePath string, line int) string {
|
|
return filePath + "::express-handler@" + strconv.Itoa(line)
|
|
}
|
|
|
|
// captureExpressInlineHandlers attributes inline route-handler body calls.
|
|
// Runs at the tail of Extract.
|
|
func captureExpressInlineHandlers(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
|
|
if root == nil || result == nil {
|
|
return
|
|
}
|
|
expressWalk(root, func(call *sitter.Node) {
|
|
if call.Type() != "call_expression" {
|
|
return
|
|
}
|
|
fn := call.ChildByFieldName("function")
|
|
if fn == nil || fn.Type() != "member_expression" {
|
|
return
|
|
}
|
|
prop := fn.ChildByFieldName("property")
|
|
recv := fn.ChildByFieldName("object")
|
|
if prop == nil || recv == nil {
|
|
return
|
|
}
|
|
if !expressRouteVerbs[prop.Content(src)] {
|
|
return
|
|
}
|
|
if !expressIsRouteReceiver(recv, src) {
|
|
return
|
|
}
|
|
args := call.ChildByFieldName("arguments")
|
|
if args == nil || !expressFirstArgIsString(args) {
|
|
return
|
|
}
|
|
// Handler-position args: every arg after the path string — inline
|
|
// arrows, named middleware idents, and XController.method handlers.
|
|
handlerArgs := expressHandlerPositionArgs(args)
|
|
if len(handlerArgs) == 0 {
|
|
return
|
|
}
|
|
line := int(call.StartPoint().Row) + 1
|
|
handlerID := ExpressInlineHandlerNodeID(filePath, line)
|
|
// Materialise the handler node so the route Contract can anchor to it
|
|
// (even when the body calls nothing the call graph tracks) and the
|
|
// body-call / named-handler edges have a real From.
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: handlerID, Kind: graph.KindFunction, Name: "route handler",
|
|
FilePath: filePath, StartLine: line, EndLine: int(call.EndPoint().Row) + 1,
|
|
Language: "javascript",
|
|
Meta: map[string]any{"express_handler": true, "signature": "(req, res) => {…}"},
|
|
})
|
|
for _, a := range handlerArgs {
|
|
switch a.Type() {
|
|
case "arrow_function", "function_expression", "function":
|
|
params := expressHandlerParamNames(a, src)
|
|
expressEmitHandlerCalls(result, a, src, handlerID, filePath, params)
|
|
case "identifier":
|
|
expressEmitHandlerRef(result, handlerID, filePath, line, "", a.Content(src))
|
|
case "member_expression":
|
|
cls := expressRootIdent(a.ChildByFieldName("object"), src)
|
|
if m := a.ChildByFieldName("property"); m != nil && cls != "" {
|
|
expressEmitHandlerRef(result, handlerID, filePath, line, cls, m.Content(src))
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// expressEmitHandlerCalls attributes the application calls in a handler body
|
|
// to handlerID, returning the count emitted.
|
|
func expressEmitHandlerCalls(result *parser.ExtractionResult, handler *sitter.Node, src []byte, handlerID, filePath string, params map[string]bool) int {
|
|
body := handler.ChildByFieldName("body")
|
|
if body == nil {
|
|
return 0
|
|
}
|
|
seen := map[string]bool{}
|
|
emitted := 0
|
|
expressWalk(body, func(c *sitter.Node) {
|
|
if emitted >= expressInlineCallCap || c.Type() != "call_expression" {
|
|
return
|
|
}
|
|
callee, ok := expressCalleeName(c.ChildByFieldName("function"), src, params)
|
|
if !ok || callee == "" || seen[callee] {
|
|
return
|
|
}
|
|
seen[callee] = true
|
|
emitted++
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: handlerID, To: "unresolved::" + callee, Kind: graph.EdgeCalls,
|
|
FilePath: filePath, Line: int(c.StartPoint().Row) + 1,
|
|
Meta: map[string]any{"express_inline_handler": true},
|
|
})
|
|
})
|
|
return emitted
|
|
}
|
|
|
|
// expressCalleeName returns the application callee name of a call, or
|
|
// ok=false when the call is a reserved framework/runtime helper or a call on
|
|
// the handler's own (req/res/next) parameters.
|
|
func expressCalleeName(fn *sitter.Node, src []byte, params map[string]bool) (string, bool) {
|
|
if fn == nil {
|
|
return "", false
|
|
}
|
|
switch fn.Type() {
|
|
case "identifier":
|
|
name := fn.Content(src)
|
|
if params[name] || jsIsReservedBareCall(name) {
|
|
return "", false
|
|
}
|
|
return name, true
|
|
case "member_expression":
|
|
root := expressRootIdent(fn.ChildByFieldName("object"), src)
|
|
if params[root] || jsIsReservedReceiver(root) {
|
|
return "", false
|
|
}
|
|
if prop := fn.ChildByFieldName("property"); prop != nil {
|
|
return prop.Content(src), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
// expressRootIdent returns the leftmost identifier of a member/call chain
|
|
// (`req` for `req.body.parse`).
|
|
func expressRootIdent(n *sitter.Node, src []byte) string {
|
|
for n != nil {
|
|
switch n.Type() {
|
|
case "identifier":
|
|
return n.Content(src)
|
|
case "member_expression":
|
|
n = n.ChildByFieldName("object")
|
|
case "call_expression":
|
|
n = n.ChildByFieldName("function")
|
|
case "subscript_expression":
|
|
n = n.ChildByFieldName("object")
|
|
default:
|
|
return strings.TrimSpace(n.Content(src))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// expressIsRouteReceiver reports whether a route call's receiver looks like an
|
|
// app/router instance.
|
|
func expressIsRouteReceiver(recv *sitter.Node, src []byte) bool {
|
|
name := expressRootIdent(recv, src)
|
|
if name == "" {
|
|
return false
|
|
}
|
|
low := strings.ToLower(name)
|
|
return expressRouteReceivers[low] || strings.Contains(low, "router") || strings.Contains(low, "app")
|
|
}
|
|
|
|
// expressFirstArgIsString reports whether the first call argument is a string
|
|
// literal (the route path).
|
|
func expressFirstArgIsString(args *sitter.Node) bool {
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
if c := args.NamedChild(i); c != nil {
|
|
return c.Type() == "string"
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// expressHandlerPositionArgs returns every argument after the route path
|
|
// string — the handler/middleware position.
|
|
func expressHandlerPositionArgs(args *sitter.Node) []*sitter.Node {
|
|
var out []*sitter.Node
|
|
sawPath := false
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
c := args.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if !sawPath {
|
|
if c.Type() == "string" {
|
|
sawPath = true
|
|
}
|
|
continue
|
|
}
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// expressEmitHandlerRef stamps a placeholder ref from the route handler
|
|
// anchor to a named middleware/handler (cls == "") or a XController.method
|
|
// handler (cls != ""), for the express resolver to bind by convention.
|
|
func expressEmitHandlerRef(result *parser.ExtractionResult, handlerID, filePath string, line int, cls, member string) {
|
|
if member == "" {
|
|
return
|
|
}
|
|
meta := map[string]any{"express_handler_ref": true}
|
|
if cls != "" {
|
|
meta["express_ref_class"] = cls
|
|
meta["express_ref_method"] = member
|
|
} else {
|
|
meta["express_ref_name"] = member
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: handlerID, To: "unresolved::" + member, Kind: graph.EdgeCalls,
|
|
FilePath: filePath, Line: line, Meta: meta,
|
|
})
|
|
}
|
|
|
|
// expressHandlerParamNames returns the parameter names of a handler function
|
|
// (`req`, `res`, `next`).
|
|
func expressHandlerParamNames(handler *sitter.Node, src []byte) map[string]bool {
|
|
out := map[string]bool{}
|
|
params := handler.ChildByFieldName("parameters")
|
|
if params == nil {
|
|
// Single-param arrow without parens: `req => ...`.
|
|
if p := handler.ChildByFieldName("parameter"); p != nil {
|
|
out[p.Content(src)] = true
|
|
}
|
|
return out
|
|
}
|
|
for i, _nc := 0, int(params.NamedChildCount()); i < _nc; i++ {
|
|
p := params.NamedChild(i)
|
|
if p == nil {
|
|
continue
|
|
}
|
|
if p.Type() == "identifier" {
|
|
out[p.Content(src)] = true
|
|
} else if id := expressRootIdent(p, src); id != "" {
|
|
out[id] = true
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// expressWalk visits n and all its named descendants.
|
|
func expressWalk(n *sitter.Node, fn func(*sitter.Node)) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
fn(n)
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
expressWalk(n.NamedChild(i), fn)
|
|
}
|
|
}
|