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

206 lines
7.4 KiB
Go

package languages
import (
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// Emitter-literal fallback channel (F15, extends jsts_pubsub.go).
//
// The import-gated pub/sub path (detectJSPubsubCall) only recognises a
// bare `recv.on('event', …)` / `recv.emit('event')` when the file imports
// a recognised event library (`events` / `eventemitter3` / socket.io).
// A custom emitter base class, an inline `new EventEmitter()`, or an
// anonymous-handler `.on()` in a file with no such import produces no
// topic node, so the emit↔listen pair is lost.
//
// This file backstops that path: when detectJSPubsubCall declines a call,
// the JS/TS extractors try detectJSEmitterLiteralCall, which materialises
// an emitter-literal KindEvent topic node keyed by the receiver scope and
// the event-name string literal. Those nodes flow through the existing
// ResolveEventChannelCalls resolver (which now recognises the
// event::emitter:: prefix and applies a tighter per-literal fan-out cap),
// so no new resolver is needed.
//
// jsEmitterTransport must match resolver.emitterLiteralTransport — the two
// packages agree on the "emitter" label by value (the languages package
// does not import resolver).
const jsEmitterTransport = "emitter"
// emitterSubscribeMethods is the EventEmitter subscribe family. These are
// the weak EventEmitter entries of pubsubMethods, recognised here without
// the import gate so a custom emitter still pairs.
var emitterSubscribeMethods = map[string]struct{}{
"on": {},
"once": {},
"addListener": {},
"prependListener": {},
"prependOnceListener": {},
}
// emitterPublishMethod is the EventEmitter publish call.
const emitterPublishMethod = "emit"
// jsEmitterEvent is one resolved emitter-literal call site the
// import-gated pub/sub path did not classify.
type jsEmitterEvent struct {
publish bool // emit -> true; on/once/addListener -> false
recv string // receiver text — the topic-node scope
topic string // the event-name string literal
method string // the matched call method name
line int // 1-based line of the call expression
handler string // named handler identifier on a subscribe site ("" = inline/anonymous)
}
// detectJSEmitterLiteralCall recognises a bare EventEmitter call
// (recv.on('event', handler) / recv.emit('event')) the import-gated
// pub/sub path (detectJSPubsubCall) did not classify. Callers invoke it
// only when detectJSPubsubCall returned ok=false, so the two paths never
// double-emit. ok is false for a non-emitter method, an empty receiver,
// or a missing event-name literal.
func detectJSEmitterLiteralCall(callExpr *sitter.Node, method, receiver string, src []byte, line int) (jsEmitterEvent, bool) {
if callExpr == nil {
return jsEmitterEvent{}, false
}
recv := emitterReceiverScope(receiver)
if recv == "" {
return jsEmitterEvent{}, false
}
_, isSub := emitterSubscribeMethods[method]
isPub := method == emitterPublishMethod
if !isSub && !isPub {
return jsEmitterEvent{}, false
}
topic := emitterEventLiteral(callExpr, src)
if topic == "" {
return jsEmitterEvent{}, false
}
ev := jsEmitterEvent{publish: isPub, recv: recv, topic: topic, method: method, line: line}
if isSub {
ev.handler = emitterHandlerName(callExpr, src)
}
return ev, true
}
// emitterReceiverScope normalises a member-call receiver text into a
// single clean node-ID segment. An empty receiver (a bare function call,
// not a member call) yields "" so it is skipped — there is no scope to
// correlate the literal against.
func emitterReceiverScope(receiver string) string {
r := strings.TrimSpace(receiver)
if r == "" {
return ""
}
r = strings.Join(strings.Fields(r), " ")
// The :: delimiter is reserved for the node-ID structure.
r = strings.ReplaceAll(r, "::", ".")
return r
}
// emitterEventLiteral returns the event-name string literal — the first
// positional argument when it is a plain string. A non-string first
// argument (template literal, computed name) yields "" because there is
// no stable literal to key the topic node on.
func emitterEventLiteral(callExpr *sitter.Node, src []byte) string {
args := callExpr.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() == 0 {
return ""
}
first := args.NamedChild(0)
if first == nil || first.Type() != "string" {
return ""
}
return jsStringLiteralContent(first, src)
}
// emitterHandlerName returns the handler identifier of a subscribe call's
// second positional argument when it is a bare identifier
// (`emitter.on('e', onReady)`). An inline function / arrow / member
// expression yields "" — there is no named node to attribute the listen
// edge to, so the caller falls back to the enclosing function.
func emitterHandlerName(callExpr *sitter.Node, src []byte) string {
args := callExpr.ChildByFieldName("arguments")
if args == nil || args.NamedChildCount() < 2 {
return ""
}
h := args.NamedChild(1)
if h == nil || h.Type() != "identifier" {
return ""
}
return strings.TrimSpace(h.Content(src))
}
// emitterEventNodeID is the canonical ID for an emitter-literal topic
// node. It is scoped by the receiver binding so recv.emit('e') and
// recv.on('e') across different files pair on the same node, while a
// different receiver's 'e' stays a distinct node — the analog of the
// tight per-literal cap, receiver-scoped rather than file-scoped so
// cross-file pairing still holds.
func emitterEventNodeID(recv, topic string) string {
return "event::emitter::" + recv + "::" + topic
}
// emitJSEmitterLiteralEvents materialises one KindEvent topic node per
// distinct (recv, topic) emitter-literal pair, an EdgeEmits from each emit
// site's enclosing function, and an EdgeListensOn whose From is the named
// handler (so the resolver's synthesized call edge lands on the handler,
// not the .on call's enclosing function), falling back to the enclosing
// function for an inline/anonymous handler. callerLookup maps a 1-based
// line to its enclosing function ID; sites with no enclosing function are
// skipped.
func emitJSEmitterLiteralEvents(events []jsEmitterEvent, callerLookup func(line int) string, filePath, language string, result *parser.ExtractionResult) {
if len(events) == 0 {
return
}
seen := make(map[string]struct{}, len(events))
for _, e := range events {
var from string
kind := graph.EdgeEmits
if e.publish {
from = callerLookup(e.line)
} else {
kind = graph.EdgeListensOn
if e.handler != "" {
from = filePath + "::" + e.handler
} else {
from = callerLookup(e.line)
}
}
if from == "" {
continue
}
nodeID := emitterEventNodeID(e.recv, e.topic)
if _, ok := seen[nodeID]; !ok {
seen[nodeID] = struct{}{}
result.Nodes = append(result.Nodes, &graph.Node{
ID: nodeID,
Kind: graph.KindEvent,
Name: e.topic,
FilePath: filePath, // first sighting; not authoritative
Language: language,
Meta: map[string]any{
"event_kind": "emitter",
"transport": jsEmitterTransport,
"name": e.topic,
"receiver": e.recv,
},
})
}
result.Edges = append(result.Edges, &graph.Edge{
From: from,
To: nodeID,
Kind: kind,
FilePath: filePath,
Line: e.line,
Origin: graph.OriginASTInferred,
Meta: map[string]any{
"method": e.method,
"transport": jsEmitterTransport,
},
})
}
}