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

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 ""
}