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
127 lines
4.3 KiB
Go
127 lines
4.3 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// JavaScript / TypeScript event pub/sub detection (F15). The JS and TS
|
|
// extractors share the same tree-sitter call_expression / string /
|
|
// object node shapes, so the AST-shaped work lives here once and both
|
|
// `javascript.go` and `typescript.go` funnel their buffered member
|
|
// calls through it. Classification, transport inference, and graph
|
|
// emission are the cross-language helpers in `pubsub.go`.
|
|
//
|
|
// Two argument shapes are recognised:
|
|
//
|
|
// - positional string — EventEmitter / Socket.IO / NATS.js / ioredis
|
|
// name the channel as the first string literal
|
|
// (`socket.emit("chat:message", …)`, `nc.subscribe("orders.*")`).
|
|
// - object option — kafkajs names it as a property of the first
|
|
// argument object (`producer.send({ topic: "orders" })`,
|
|
// `consumer.subscribe({ topic: "orders" })`).
|
|
|
|
// jsPubsubObjectKeys is the set of object-literal property names that
|
|
// carry a pub/sub topic when the broker call takes an options object
|
|
// instead of a positional string (kafkajs and friends).
|
|
var jsPubsubObjectKeys = map[string]struct{}{
|
|
"topic": {},
|
|
"channel": {},
|
|
"subject": {},
|
|
"event": {},
|
|
"queue": {},
|
|
"exchange": {},
|
|
"routingkey": {},
|
|
"name": {},
|
|
}
|
|
|
|
// detectJSPubsubCall inspects a member call_expression and, when its
|
|
// method is a known pub/sub operation with a resolvable topic, returns
|
|
// the classified event. importPaths is the file's import set, used to
|
|
// disambiguate generic method names (emit / on / send / publish) and to
|
|
// infer the transport. ok is false when the call is not a pub/sub
|
|
// operation.
|
|
func detectJSPubsubCall(callExpr *sitter.Node, method string, src []byte, importPaths []string, line int) (pubsubEvent, bool) {
|
|
if callExpr == nil {
|
|
return pubsubEvent{}, false
|
|
}
|
|
if _, known := pubsubMethods[method]; !known {
|
|
return pubsubEvent{}, false
|
|
}
|
|
topic := firstJSPubsubTopicArg(callExpr, src)
|
|
if topic == "" {
|
|
return pubsubEvent{}, false
|
|
}
|
|
return classifyPubsubCall(method, topic, importPaths, line)
|
|
}
|
|
|
|
// firstJSPubsubTopicArg pulls the topic string out of a JS/TS
|
|
// call_expression — the first positional string literal, or the first
|
|
// recognised topic property of a leading options object. Returns "" for
|
|
// template strings, computed names, and non-string values, which the
|
|
// heuristic deliberately can't resolve to a stable topic node.
|
|
func firstJSPubsubTopicArg(callExpr *sitter.Node, src []byte) string {
|
|
args := callExpr.ChildByFieldName("arguments")
|
|
if args == nil {
|
|
return ""
|
|
}
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
c := args.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
switch c.Type() {
|
|
case "string":
|
|
if s := jsStringLiteralContent(c, src); s != "" {
|
|
return s
|
|
}
|
|
case "object":
|
|
if s := jsObjectTopicValue(c, src); s != "" {
|
|
return s
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// jsStringLiteralContent returns the unquoted content of a tree-sitter
|
|
// `string` node. The grammar wraps the body in a `string_fragment`
|
|
// child; an empty literal (`""`) has no fragment and yields "".
|
|
func jsStringLiteralContent(strNode *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(strNode.NamedChildCount()); i < _nc; i++ {
|
|
c := strNode.NamedChild(i)
|
|
if c != nil && c.Type() == "string_fragment" {
|
|
return strings.TrimSpace(c.Content(src))
|
|
}
|
|
}
|
|
// Fallback: trim the surrounding quote characters directly.
|
|
return strings.TrimSpace(strings.Trim(strNode.Content(src), "\"'`"))
|
|
}
|
|
|
|
// jsObjectTopicValue scans an object literal for the first `pair` whose
|
|
// key is a recognised pub/sub topic property (topic / channel /
|
|
// subject / …) and whose value is a string literal, returning that
|
|
// string. Used for kafkajs-style option-object call shapes.
|
|
func jsObjectTopicValue(objNode *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(objNode.NamedChildCount()); i < _nc; i++ {
|
|
pair := objNode.NamedChild(i)
|
|
if pair == nil || pair.Type() != "pair" {
|
|
continue
|
|
}
|
|
keyNode := pair.ChildByFieldName("key")
|
|
valNode := pair.ChildByFieldName("value")
|
|
if keyNode == nil || valNode == nil || valNode.Type() != "string" {
|
|
continue
|
|
}
|
|
key := strings.ToLower(strings.Trim(keyNode.Content(src), "\"'`"))
|
|
if _, ok := jsPubsubObjectKeys[key]; !ok {
|
|
continue
|
|
}
|
|
if s := jsStringLiteralContent(valNode, src); s != "" {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|