Files
deusdata--codebase-memory-mcp/internal/cbm/extract_channels.c
T
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

1089 lines
42 KiB
C

/*
* extract_channels.c — Pub/sub channel participation extractor.
*
* Detects event-driven communication patterns across multiple languages:
* JS/TS/TSX: Socket.IO, EventEmitter, raw WebSocket, Kafka, RabbitMQ
* Python: python-socketio, Django Channels, FastAPI WebSocket, kafka-python
* Go: gorilla/nhooyr websocket (WriteMessage/ReadMessage)
* Java: JSR 356 WebSocket, Spring STOMP/WebSocket
* C#: SignalR (Hub.SendAsync / Hub.On)
* Ruby: ActionCable (broadcast / stream_from)
* Elixir: Phoenix.PubSub, Phoenix.Channel
* Rust: tokio-tungstenite (sink.send / stream.next)
*
* Transport is stored on the record ("socketio", "websocket", "kafka", etc.)
* so later detectors can share the same schema without changing edge types.
*
* String-constant resolution: when the channel name argument is a plain
* identifier, we perform a single-pass local scan of the module body to
* resolve `const EVENT = "foo"` style bindings. Template literals and
* config-driven names stay unresolved (acceptable — those require real
* data-flow analysis).
*/
#include "cbm.h"
#include "arena.h"
#include "helpers.h"
#include "foundation/constants.h"
#include "extract_node_stack.h"
#include "tree_sitter/api.h"
#include <stdint.h>
#include <string.h>
enum {
CHAN_CONST_CAP = 256, /* max tracked identifiers per file */
CHAN_IDENT_MAX = 128, /* max identifier length tracked */
CHAN_STACK_CAP = 4096, /* traversal stack depth per walk */
CHAN_DIR_UNKNOWN = -1, /* unrecognized method → no channel */
};
typedef struct {
const char *name; /* borrowed — points into arena */
const char *value; /* borrowed — points into arena */
} chan_const_t;
typedef struct {
chan_const_t items[CHAN_CONST_CAP];
int count;
} chan_const_table_t;
/* ── String literal helpers ──────────────────────────────────────── */
static const char *unquote_string(CBMArena *a, const char *s) {
if (!s) {
return NULL;
}
size_t len = strlen(s);
if (len < CBM_QUOTE_PAIR) {
return NULL;
}
char first = s[0];
char last = s[len - CBM_QUOTE_OFFSET];
if ((first == '"' && last == '"') || (first == '\'' && last == '\'') ||
(first == '`' && last == '`')) {
return cbm_arena_strndup(a, s + CBM_QUOTE_OFFSET, len - CBM_QUOTE_PAIR);
}
return NULL;
}
/* Extract a literal channel name from an argument node. Returns NULL if the
* argument is not a plain string literal (caller can then try identifier
* resolution via the constant table). */
static const char *literal_from_arg(CBMExtractCtx *ctx, TSNode arg) {
const char *kind = ts_node_type(arg);
if (strcmp(kind, "string") != 0 && strcmp(kind, "string_literal") != 0 &&
strcmp(kind, "interpreted_string_literal") != 0 &&
strcmp(kind, "raw_string_literal") != 0 && strcmp(kind, "string_content") != 0) {
return NULL;
}
char *text = cbm_node_text(ctx->arena, arg, ctx->source);
return unquote_string(ctx->arena, text);
}
/* Extract string literal from first named child (for nodes wrapping string content). */
static const char *literal_from_first_child(CBMExtractCtx *ctx, TSNode node) {
uint32_t nc = ts_node_named_child_count(node);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_named_child(node, i);
const char *val = literal_from_arg(ctx, child);
if (val) {
return val;
}
}
return NULL;
}
/* ── Constant resolution table ──────────────────────────────────── */
/* Walk the whole tree once and collect `const IDENT = "value"` bindings so
* later passes can resolve bare-identifier channel arguments. Only scalar
* string literals are tracked — template literals and expressions are left
* unresolved. This is a flat lookup; scope boundaries are ignored (a single
* const table per file is sufficient for the common Socket.IO pattern). */
static void scan_string_consts_js(CBMExtractCtx *ctx, chan_const_table_t *tbl) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0 && tbl->count < CHAN_CONST_CAP) {
TSNode node = ts_nstack_pop(&stack);
const char *kind = ts_node_type(node);
if (strcmp(kind, "variable_declarator") == 0) {
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
TSNode value_node = ts_node_child_by_field_name(node, TS_FIELD("value"));
if (!ts_node_is_null(name_node) && !ts_node_is_null(value_node)) {
const char *nk = ts_node_type(name_node);
const char *vk = ts_node_type(value_node);
if (strcmp(nk, "identifier") == 0 &&
(strcmp(vk, "string") == 0 || strcmp(vk, "string_literal") == 0)) {
char *name_text = cbm_node_text(ctx->arena, name_node, ctx->source);
char *value_text = cbm_node_text(ctx->arena, value_node, ctx->source);
const char *unq = unquote_string(ctx->arena, value_text);
if (name_text && unq) {
tbl->items[tbl->count].name = name_text;
tbl->items[tbl->count].value = unq;
tbl->count++;
}
}
}
}
ts_nstack_push_children(&stack, ctx->arena, node);
}
}
/* Python constant resolution: NAME = "value" (assignment node). */
static void scan_string_consts_python(CBMExtractCtx *ctx, chan_const_table_t *tbl) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0 && tbl->count < CHAN_CONST_CAP) {
TSNode node = ts_nstack_pop(&stack);
const char *kind = ts_node_type(node);
if (strcmp(kind, "assignment") == 0) {
TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
TSNode right = ts_node_child_by_field_name(node, TS_FIELD("right"));
if (!ts_node_is_null(left) && !ts_node_is_null(right) &&
strcmp(ts_node_type(left), "identifier") == 0 &&
strcmp(ts_node_type(right), "string") == 0) {
char *name = cbm_node_text(ctx->arena, left, ctx->source);
const char *val = literal_from_arg(ctx, right);
if (!val) {
val = literal_from_first_child(ctx, right);
}
if (name && val) {
tbl->items[tbl->count].name = name;
tbl->items[tbl->count].value = val;
tbl->count++;
}
}
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* Resolve an identifier against the constant table. Returns NULL on miss. */
static const char *resolve_identifier(const chan_const_table_t *tbl, const char *name) {
if (!name) {
return NULL;
}
for (int i = 0; i < tbl->count; i++) {
if (tbl->items[i].name && strcmp(tbl->items[i].name, name) == 0) {
return tbl->items[i].value;
}
}
return NULL;
}
/* ── Enclosing function detection ───────────────────────────────── */
static const char *enclosing_function_qn(CBMExtractCtx *ctx, TSNode node) {
TSNode parent = ts_node_parent(node);
while (!ts_node_is_null(parent)) {
const char *pk = ts_node_type(parent);
if (strcmp(pk, "function_declaration") == 0 || strcmp(pk, "method_definition") == 0 ||
strcmp(pk, "arrow_function") == 0 || strcmp(pk, "function_expression") == 0 ||
strcmp(pk, "function") == 0 || strcmp(pk, "method_signature") == 0 ||
strcmp(pk, "function_definition") == 0 || strcmp(pk, "method_declaration") == 0 ||
strcmp(pk, "function_item") == 0 || strcmp(pk, "def") == 0) {
TSNode name_node = ts_node_child_by_field_name(parent, TS_FIELD("name"));
if (!ts_node_is_null(name_node)) {
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
if (name && name[0]) {
return name;
}
}
return NULL;
}
parent = ts_node_parent(parent);
}
return NULL;
}
/* ── Channel name extraction from arguments ──────────────────────── */
/* Try to extract a channel name from the first argument of a call.
* Tries literal first, then identifier resolution via constant table. */
static const char *extract_channel_name(CBMExtractCtx *ctx, TSNode args,
const chan_const_table_t *consts) {
uint32_t arg_count = ts_node_named_child_count(args);
if (arg_count == 0) {
return NULL;
}
TSNode first = ts_node_named_child(args, 0);
const char *channel_name = literal_from_arg(ctx, first);
if (!channel_name) {
channel_name = literal_from_first_child(ctx, first);
}
if (!channel_name && consts) {
const char *kind = ts_node_type(first);
if (strcmp(kind, "identifier") == 0) {
char *ident = cbm_node_text(ctx->arena, first, ctx->source);
channel_name = resolve_identifier(consts, ident);
}
}
return channel_name;
}
/* ── Emit helper ─────────────────────────────────────────────────── */
static void push_channel(CBMExtractCtx *ctx, const char *channel_name, const char *transport,
CBMChannelDirection direction, TSNode call) {
CBMChannel ch = {
.channel_name = channel_name,
.transport = transport,
.enclosing_func_qn = enclosing_function_qn(ctx, call),
.direction = direction,
};
cbm_channels_push(&ctx->result->channels, ctx->arena, ch);
}
/* ══════════════════════════════════════════════════════════════════
* JS/TS/TSX — Socket.IO, EventEmitter, WebSocket, Kafka, RabbitMQ
* ══════════════════════════════════════════════════════════════════ */
static bool js_is_emit_method(const char *name) {
return name && strcmp(name, "emit") == 0;
}
static bool js_is_listen_method(const char *name) {
return name && (strcmp(name, "on") == 0 || strcmp(name, "addListener") == 0 ||
strcmp(name, "once") == 0);
}
/* Classify receiver for Socket.IO / EventEmitter / WebSocket. */
static const char *js_classify_receiver(CBMExtractCtx *ctx, TSNode object_node) {
char *text = cbm_node_text(ctx->arena, object_node, ctx->source);
if (!text) {
return NULL;
}
const char *tail = text;
const char *dot = strrchr(tail, '.');
if (dot) {
tail = dot + SKIP_ONE;
}
/* Socket.IO */
if (strcmp(tail, "socket") == 0 || strcmp(tail, "io") == 0 || strcmp(tail, "ws") == 0 ||
strcmp(tail, "client") == 0 || strcmp(tail, "server") == 0) {
return "socketio";
}
/* Node.js EventEmitter */
if (strcmp(tail, "emitter") == 0 || strcmp(tail, "eventEmitter") == 0 ||
strcmp(tail, "events") == 0 || strcmp(tail, "bus") == 0 || strcmp(tail, "eventBus") == 0 ||
strcmp(tail, "pubsub") == 0) {
return "event_emitter";
}
/* Kafka */
if (strcmp(tail, "producer") == 0) {
return "kafka";
}
if (strcmp(tail, "consumer") == 0) {
return "kafka";
}
/* RabbitMQ / AMQP */
if (strcmp(tail, "channel") == 0 && !strcmp(text, "channel")) {
return "rabbitmq";
}
return NULL;
}
/* Detect Kafka/RabbitMQ specific send/subscribe patterns. */
static bool js_is_kafka_send(const char *method) {
return method && (strcmp(method, "send") == 0 || strcmp(method, "sendBatch") == 0);
}
static bool js_is_kafka_listen(const char *method) {
return method && (strcmp(method, "subscribe") == 0 || strcmp(method, "run") == 0);
}
static bool js_is_amqp_send(const char *method) {
return method && (strcmp(method, "publish") == 0 || strcmp(method, "sendToQueue") == 0);
}
static bool js_is_amqp_listen(const char *method) {
return method && (strcmp(method, "consume") == 0 || strcmp(method, "assertQueue") == 0);
}
static void js_process_call(CBMExtractCtx *ctx, TSNode call, const chan_const_table_t *consts) {
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("function"));
if (ts_node_is_null(func) || strcmp(ts_node_type(func), "member_expression") != 0) {
return;
}
TSNode object = ts_node_child_by_field_name(func, TS_FIELD("object"));
TSNode property = ts_node_child_by_field_name(func, TS_FIELD("property"));
if (ts_node_is_null(object) || ts_node_is_null(property)) {
return;
}
char *method = cbm_node_text(ctx->arena, property, ctx->source);
const char *transport = js_classify_receiver(ctx, object);
if (!transport) {
return;
}
CBMChannelDirection direction;
if (strcmp(transport, "kafka") == 0) {
if (js_is_kafka_send(method)) {
direction = CBM_CHANNEL_EMIT;
} else if (js_is_kafka_listen(method)) {
direction = CBM_CHANNEL_LISTEN;
} else {
return;
}
} else if (strcmp(transport, "rabbitmq") == 0) {
if (js_is_amqp_send(method)) {
direction = CBM_CHANNEL_EMIT;
} else if (js_is_amqp_listen(method)) {
direction = CBM_CHANNEL_LISTEN;
} else {
return;
}
} else {
/* socketio / event_emitter */
if (js_is_emit_method(method)) {
direction = CBM_CHANNEL_EMIT;
} else if (js_is_listen_method(method)) {
direction = CBM_CHANNEL_LISTEN;
} else {
return;
}
}
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (ts_node_is_null(args)) {
return;
}
const char *channel_name = extract_channel_name(ctx, args, consts);
if (!channel_name) {
return;
}
push_channel(ctx, channel_name, transport, direction, call);
}
static void extract_channels_js(CBMExtractCtx *ctx) {
chan_const_table_t consts = {0};
scan_string_consts_js(ctx, &consts);
/* Second pass: walk the tree looking for call_expression nodes. */
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "call_expression") == 0) {
js_process_call(ctx, node, &consts);
}
ts_nstack_push_children(&stack, ctx->arena, node);
}
}
/* ══════════════════════════════════════════════════════════════════
* Python — python-socketio, Django Channels, FastAPI WebSocket, Kafka
* ══════════════════════════════════════════════════════════════════ */
static const char *py_classify_receiver(CBMExtractCtx *ctx, TSNode object_node) {
char *text = cbm_node_text(ctx->arena, object_node, ctx->source);
if (!text) {
return NULL;
}
const char *tail = text;
const char *dot = strrchr(tail, '.');
if (dot) {
tail = dot + SKIP_ONE;
}
/* python-socketio */
if (strcmp(tail, "sio") == 0 || strcmp(tail, "socketio") == 0 || strcmp(tail, "socket") == 0) {
return "socketio";
}
/* Django Channels */
if (strcmp(tail, "channel_layer") == 0) {
return "django_channels";
}
/* FastAPI/Starlette WebSocket */
if (strcmp(tail, "websocket") == 0 || strcmp(tail, "ws") == 0) {
return "websocket";
}
/* kafka-python */
if (strcmp(tail, "producer") == 0) {
return "kafka";
}
if (strcmp(tail, "consumer") == 0) {
return "kafka";
}
return NULL;
}
/* Table-driven Python method→direction classification.
* NULL transport matches any transport not matched by earlier rows. */
static const struct {
const char *transport; /* NULL = wildcard (socketio fallback) */
const char *method;
int direction;
} py_method_table[] = {
{"kafka", "send", CBM_CHANNEL_EMIT},
{"kafka", "produce", CBM_CHANNEL_EMIT},
{"kafka", "subscribe", CBM_CHANNEL_LISTEN},
{"kafka", "poll", CBM_CHANNEL_LISTEN},
{"django_channels", "send", CBM_CHANNEL_EMIT},
{"django_channels", "group_send", CBM_CHANNEL_EMIT},
{"django_channels", "receive", CBM_CHANNEL_LISTEN},
{"django_channels", "group_add", CBM_CHANNEL_LISTEN},
{"websocket", "send", CBM_CHANNEL_EMIT},
{"websocket", "send_text", CBM_CHANNEL_EMIT},
{"websocket", "send_json", CBM_CHANNEL_EMIT},
{"websocket", "send_bytes", CBM_CHANNEL_EMIT},
{"websocket", "receive", CBM_CHANNEL_LISTEN},
{"websocket", "receive_text", CBM_CHANNEL_LISTEN},
{"websocket", "receive_json", CBM_CHANNEL_LISTEN},
{"websocket", "receive_bytes", CBM_CHANNEL_LISTEN},
{NULL, "emit", CBM_CHANNEL_EMIT},
{NULL, "send", CBM_CHANNEL_EMIT},
{NULL, "on", CBM_CHANNEL_LISTEN},
{NULL, NULL, CHAN_DIR_UNKNOWN},
};
static int py_classify_direction(const char *transport, const char *method) {
for (int i = 0; py_method_table[i].method != NULL; i++) {
const char *t = py_method_table[i].transport;
if (t && strcmp(t, transport) != 0) {
continue;
}
if (strcmp(py_method_table[i].method, method) == 0) {
return py_method_table[i].direction;
}
}
return CHAN_DIR_UNKNOWN;
}
static void py_process_call(CBMExtractCtx *ctx, TSNode call, const chan_const_table_t *consts) {
/* Python call: attribute { object, attribute }, argument_list */
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("function"));
if (ts_node_is_null(func)) {
return;
}
const char *fk = ts_node_type(func);
if (strcmp(fk, "attribute") != 0) {
return;
}
TSNode object = ts_node_child_by_field_name(func, TS_FIELD("object"));
TSNode attr = ts_node_child_by_field_name(func, TS_FIELD("attribute"));
if (ts_node_is_null(object) || ts_node_is_null(attr)) {
return;
}
char *method = cbm_node_text(ctx->arena, attr, ctx->source);
const char *transport = py_classify_receiver(ctx, object);
if (!transport || !method) {
return;
}
int dir = py_classify_direction(transport, method);
if (dir == CHAN_DIR_UNKNOWN) {
return;
}
CBMChannelDirection direction = (CBMChannelDirection)dir;
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (ts_node_is_null(args)) {
return;
}
const char *channel_name = extract_channel_name(ctx, args, consts);
if (!channel_name) {
return;
}
push_channel(ctx, channel_name, transport, direction, call);
}
/* Detect Python decorator-based listeners: @sio.on("event") / @sio.event */
static void py_process_decorator(CBMExtractCtx *ctx, TSNode decorator,
const chan_const_table_t *consts) {
/* decorator: @expression or @call(args) */
uint32_t nc = ts_node_named_child_count(decorator);
if (nc == 0) {
return;
}
TSNode expr = ts_node_named_child(decorator, 0);
const char *ek = ts_node_type(expr);
/* @sio.on("event") → call node wrapping attribute access */
if (strcmp(ek, "call") == 0) {
TSNode func = ts_node_child_by_field_name(expr, TS_FIELD("function"));
if (ts_node_is_null(func) || strcmp(ts_node_type(func), "attribute") != 0) {
return;
}
TSNode object = ts_node_child_by_field_name(func, TS_FIELD("object"));
TSNode attr = ts_node_child_by_field_name(func, TS_FIELD("attribute"));
if (ts_node_is_null(object) || ts_node_is_null(attr)) {
return;
}
char *method = cbm_node_text(ctx->arena, attr, ctx->source);
if (!method || strcmp(method, "on") != 0) {
return;
}
const char *transport = py_classify_receiver(ctx, object);
if (!transport) {
return;
}
TSNode args = ts_node_child_by_field_name(expr, TS_FIELD("arguments"));
if (ts_node_is_null(args)) {
return;
}
const char *channel_name = extract_channel_name(ctx, args, consts);
if (!channel_name) {
return;
}
push_channel(ctx, channel_name, transport, CBM_CHANNEL_LISTEN, decorator);
}
}
static void extract_channels_python(CBMExtractCtx *ctx) {
chan_const_table_t consts = {0};
scan_string_consts_python(ctx, &consts);
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *kind = ts_node_type(node);
if (strcmp(kind, "call") == 0) {
py_process_call(ctx, node, &consts);
} else if (strcmp(kind, "decorator") == 0) {
py_process_decorator(ctx, node, &consts);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Go — gorilla/nhooyr websocket (WriteMessage/ReadMessage)
* ══════════════════════════════════════════════════════════════════ */
static void go_process_call(CBMExtractCtx *ctx, TSNode call) {
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("function"));
if (ts_node_is_null(func)) {
return;
}
const char *fk = ts_node_type(func);
if (strcmp(fk, "selector_expression") != 0) {
return;
}
TSNode field = ts_node_child_by_field_name(func, TS_FIELD("field"));
TSNode operand = ts_node_child_by_field_name(func, TS_FIELD("operand"));
if (ts_node_is_null(field) || ts_node_is_null(operand)) {
return;
}
char *method = cbm_node_text(ctx->arena, field, ctx->source);
if (!method) {
return;
}
/* gorilla/nhooyr websocket patterns */
CBMChannelDirection direction;
if (strcmp(method, "WriteMessage") == 0 || strcmp(method, "WriteJSON") == 0 ||
strcmp(method, "Write") == 0) {
direction = CBM_CHANNEL_EMIT;
} else if (strcmp(method, "ReadMessage") == 0 || strcmp(method, "ReadJSON") == 0 ||
strcmp(method, "Read") == 0) {
direction = CBM_CHANNEL_LISTEN;
} else {
return;
}
/* Verify receiver looks like a websocket connection */
char *recv = cbm_node_text(ctx->arena, operand, ctx->source);
if (!recv) {
return;
}
const char *tail = recv;
const char *dot = strrchr(tail, '.');
if (dot) {
tail = dot + SKIP_ONE;
}
if (strcmp(tail, "conn") != 0 && strcmp(tail, "wsConn") != 0 && strcmp(tail, "ws") != 0 &&
strcmp(tail, "c") != 0 && strcmp(tail, "Conn") != 0 && strcmp(tail, "connection") != 0) {
return;
}
/* Go WebSocket uses connection-level send/receive, not named channels.
* Use the enclosing function name as a pseudo-channel for cross-repo matching. */
const char *func_name = enclosing_function_qn(ctx, call);
const char *channel_name = func_name ? func_name : "(websocket)";
push_channel(ctx, channel_name, "websocket", direction, call);
}
static void extract_channels_go(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "call_expression") == 0) {
go_process_call(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Java — JSR 356 WebSocket, Spring STOMP/WebSocket
* ══════════════════════════════════════════════════════════════════ */
static void java_process_call(CBMExtractCtx *ctx, TSNode call) {
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("name"));
TSNode object = ts_node_child_by_field_name(call, TS_FIELD("object"));
if (ts_node_is_null(func)) {
return;
}
char *method = cbm_node_text(ctx->arena, func, ctx->source);
if (!method) {
return;
}
/* Spring STOMP: template.convertAndSend("/topic/...", msg) */
if (strcmp(method, "convertAndSend") == 0 || strcmp(method, "convertAndSendToUser") == 0) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (ts_node_is_null(args)) {
return;
}
const char *channel_name = extract_channel_name(ctx, args, NULL);
if (channel_name) {
push_channel(ctx, channel_name, "spring_websocket", CBM_CHANNEL_EMIT, call);
}
return;
}
/* JSR 356: session.getBasicRemote().sendText(msg) — detect sendText/sendObject */
if (strcmp(method, "sendText") == 0 || strcmp(method, "sendObject") == 0 ||
strcmp(method, "sendBinary") == 0) {
const char *func_name = enclosing_function_qn(ctx, call);
const char *channel_name = func_name ? func_name : "(websocket)";
push_channel(ctx, channel_name, "websocket", CBM_CHANNEL_EMIT, call);
return;
}
(void)object;
}
/* Detect Java annotation-based WebSocket listeners: @OnMessage, @MessageMapping */
static void java_process_annotation(CBMExtractCtx *ctx, TSNode annotation) {
TSNode name_node = ts_node_child_by_field_name(annotation, TS_FIELD("name"));
if (ts_node_is_null(name_node)) {
return;
}
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
if (!name) {
return;
}
if (strcmp(name, "OnMessage") == 0 || strcmp(name, "OnOpen") == 0 ||
strcmp(name, "OnClose") == 0) {
const char *func_name = enclosing_function_qn(ctx, annotation);
const char *channel_name = func_name ? func_name : "(websocket)";
push_channel(ctx, channel_name, "websocket", CBM_CHANNEL_LISTEN, annotation);
} else if (strcmp(name, "MessageMapping") == 0) {
/* @MessageMapping("/path") — extract the path */
TSNode args = ts_node_child_by_field_name(annotation, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *path = extract_channel_name(ctx, args, NULL);
if (path) {
push_channel(ctx, path, "spring_websocket", CBM_CHANNEL_LISTEN, annotation);
return;
}
}
push_channel(ctx, "(spring_ws)", "spring_websocket", CBM_CHANNEL_LISTEN, annotation);
} else if (strcmp(name, "ServerEndpoint") == 0) {
TSNode args = ts_node_child_by_field_name(annotation, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *path = extract_channel_name(ctx, args, NULL);
if (path) {
push_channel(ctx, path, "websocket", CBM_CHANNEL_LISTEN, annotation);
return;
}
}
}
}
static void extract_channels_java(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *kind = ts_node_type(node);
if (strcmp(kind, "method_invocation") == 0) {
java_process_call(ctx, node);
} else if (strcmp(kind, "marker_annotation") == 0 || strcmp(kind, "annotation") == 0) {
java_process_annotation(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* C# — SignalR (Clients.All.SendAsync / Hub.On)
* ══════════════════════════════════════════════════════════════════ */
static void csharp_process_call(CBMExtractCtx *ctx, TSNode call) {
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("function"));
if (ts_node_is_null(func)) {
return;
}
const char *fk = ts_node_type(func);
if (strcmp(fk, "member_access_expression") != 0) {
return;
}
TSNode name_node = ts_node_child_by_field_name(func, TS_FIELD("name"));
if (ts_node_is_null(name_node)) {
return;
}
char *method = cbm_node_text(ctx->arena, name_node, ctx->source);
if (!method) {
return;
}
/* SignalR: Clients.All.SendAsync("method", data) */
if (strcmp(method, "SendAsync") == 0 || strcmp(method, "SendCoreAsync") == 0) {
char *full = cbm_node_text(ctx->arena, func, ctx->source);
if (full && (strstr(full, "Clients") || strstr(full, "clients"))) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *channel_name = extract_channel_name(ctx, args, NULL);
if (channel_name) {
push_channel(ctx, channel_name, "signalr", CBM_CHANNEL_EMIT, call);
}
}
}
return;
}
/* SignalR: connection.On<T>("method", handler) */
if (strcmp(method, "On") == 0) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *channel_name = extract_channel_name(ctx, args, NULL);
if (channel_name) {
push_channel(ctx, channel_name, "signalr", CBM_CHANNEL_LISTEN, call);
}
}
}
}
static void extract_channels_csharp(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "invocation_expression") == 0) {
csharp_process_call(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Ruby — ActionCable (broadcast / stream_from)
* ══════════════════════════════════════════════════════════════════ */
static void ruby_process_call(CBMExtractCtx *ctx, TSNode call) {
const char *kind = ts_node_type(call);
TSNode method_node;
if (strcmp(kind, "call") == 0) {
method_node = ts_node_child_by_field_name(call, TS_FIELD("method"));
} else {
return;
}
if (ts_node_is_null(method_node)) {
return;
}
char *method = cbm_node_text(ctx->arena, method_node, ctx->source);
if (!method) {
return;
}
/* ActionCable.server.broadcast("channel", data) */
if (strcmp(method, "broadcast") == 0) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *channel_name = extract_channel_name(ctx, args, NULL);
if (channel_name) {
push_channel(ctx, channel_name, "actioncable", CBM_CHANNEL_EMIT, call);
}
}
return;
}
/* stream_from "channel" — listener registration */
if (strcmp(method, "stream_from") == 0 || strcmp(method, "stream_for") == 0) {
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (!ts_node_is_null(args)) {
const char *channel_name = extract_channel_name(ctx, args, NULL);
if (channel_name) {
push_channel(ctx, channel_name, "actioncable", CBM_CHANNEL_LISTEN, call);
}
}
}
}
static void extract_channels_ruby(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "call") == 0) {
ruby_process_call(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Elixir — Phoenix.PubSub, Phoenix.Channel
* ══════════════════════════════════════════════════════════════════ */
/* Extract a string literal from the Nth named child of an args node. */
static const char *elixir_nth_arg_literal(CBMExtractCtx *ctx, TSNode args, uint32_t index) {
uint32_t ac = ts_node_named_child_count(args);
if (ac <= index) {
return NULL;
}
TSNode arg = ts_node_named_child(args, index);
const char *val = literal_from_arg(ctx, arg);
if (!val) {
val = literal_from_first_child(ctx, arg);
}
return val;
}
/* Try to emit a channel from the second argument of an Elixir call. */
static void elixir_emit_second_arg(CBMExtractCtx *ctx, TSNode call, TSNode args,
const char *transport, CBMChannelDirection direction) {
if (ts_node_is_null(args)) {
return;
}
const char *val = elixir_nth_arg_literal(ctx, args, SKIP_ONE);
if (val) {
push_channel(ctx, val, transport, direction, call);
}
}
static void elixir_process_call(CBMExtractCtx *ctx, TSNode call) {
TSNode target = ts_node_child_by_field_name(call, TS_FIELD("target"));
if (ts_node_is_null(target)) {
return;
}
char *target_text = cbm_node_text(ctx->arena, target, ctx->source);
if (!target_text) {
return;
}
TSNode args = ts_node_child_by_field_name(call, TS_FIELD("arguments"));
if (strstr(target_text, "PubSub.broadcast") || strstr(target_text, "PubSub.local_broadcast")) {
elixir_emit_second_arg(ctx, call, args, "phoenix_pubsub", CBM_CHANNEL_EMIT);
return;
}
if (strstr(target_text, "PubSub.subscribe")) {
elixir_emit_second_arg(ctx, call, args, "phoenix_pubsub", CBM_CHANNEL_LISTEN);
return;
}
if (strcmp(target_text, "push") == 0 || strcmp(target_text, "broadcast") == 0 ||
strcmp(target_text, "broadcast!") == 0) {
elixir_emit_second_arg(ctx, call, args, "phoenix_channel", CBM_CHANNEL_EMIT);
}
}
/* Detect handle_in("event", payload, socket) — Phoenix Channel listener */
static void elixir_process_function_def(CBMExtractCtx *ctx, TSNode func_def) {
TSNode name_node = ts_node_child_by_field_name(func_def, TS_FIELD("name"));
if (ts_node_is_null(name_node)) {
return;
}
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
if (!name || strcmp(name, "handle_in") != 0) {
return;
}
/* First parameter is the event name pattern */
TSNode params = ts_node_child_by_field_name(func_def, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t pc = ts_node_named_child_count(params);
if (pc == 0) {
return;
}
TSNode first_param = ts_node_named_child(params, 0);
const char *val = literal_from_arg(ctx, first_param);
if (!val) {
val = literal_from_first_child(ctx, first_param);
}
if (val) {
push_channel(ctx, val, "phoenix_channel", CBM_CHANNEL_LISTEN, func_def);
}
}
static void extract_channels_elixir(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *kind = ts_node_type(node);
if (strcmp(kind, "call") == 0) {
elixir_process_call(ctx, node);
} else if (strcmp(kind, "def") == 0) {
elixir_process_function_def(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Rust — tokio-tungstenite (sink.send / stream.next)
* ══════════════════════════════════════════════════════════════════ */
static void rust_process_call(CBMExtractCtx *ctx, TSNode call) {
TSNode func = ts_node_child_by_field_name(call, TS_FIELD("function"));
if (ts_node_is_null(func)) {
return;
}
const char *fk = ts_node_type(func);
if (strcmp(fk, "field_expression") != 0) {
return;
}
TSNode field = ts_node_child_by_field_name(func, TS_FIELD("field"));
TSNode value = ts_node_child_by_field_name(func, TS_FIELD("value"));
if (ts_node_is_null(field) || ts_node_is_null(value)) {
return;
}
char *method = cbm_node_text(ctx->arena, field, ctx->source);
if (!method) {
return;
}
CBMChannelDirection direction;
if (strcmp(method, "send") == 0 || strcmp(method, "send_all") == 0 ||
strcmp(method, "feed") == 0) {
direction = CBM_CHANNEL_EMIT;
} else if (strcmp(method, "next") == 0 || strcmp(method, "try_next") == 0) {
direction = CBM_CHANNEL_LISTEN;
} else {
return;
}
/* Verify receiver looks like a websocket sink/stream */
char *recv = cbm_node_text(ctx->arena, value, ctx->source);
if (!recv) {
return;
}
const char *tail = recv;
const char *dot = strrchr(tail, '.');
if (dot) {
tail = dot + SKIP_ONE;
}
if (strcmp(tail, "sink") != 0 && strcmp(tail, "ws_sender") != 0 &&
strcmp(tail, "writer") != 0 && strcmp(tail, "write") != 0 && strcmp(tail, "stream") != 0 &&
strcmp(tail, "ws_receiver") != 0 && strcmp(tail, "reader") != 0 &&
strcmp(tail, "read") != 0 && strcmp(tail, "ws_stream") != 0 && strcmp(tail, "ws") != 0) {
return;
}
const char *func_name = enclosing_function_qn(ctx, call);
const char *channel_name = func_name ? func_name : "(websocket)";
push_channel(ctx, channel_name, "websocket", direction, call);
}
static void extract_channels_rust(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->arena, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "call_expression") == 0) {
rust_process_call(ctx, node);
}
uint32_t count = ts_node_child_count(node);
for (int i = (int)count - SKIP_ONE; i >= 0; i--) {
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
}
}
}
/* ══════════════════════════════════════════════════════════════════
* Entry point — language dispatch
* ══════════════════════════════════════════════════════════════════ */
void cbm_extract_channels(CBMExtractCtx *ctx) {
switch (ctx->language) {
case CBM_LANG_JAVASCRIPT:
case CBM_LANG_TYPESCRIPT:
case CBM_LANG_TSX:
extract_channels_js(ctx);
break;
case CBM_LANG_PYTHON:
extract_channels_python(ctx);
break;
case CBM_LANG_GO:
extract_channels_go(ctx);
break;
case CBM_LANG_JAVA:
case CBM_LANG_KOTLIN:
extract_channels_java(ctx);
break;
case CBM_LANG_CSHARP:
extract_channels_csharp(ctx);
break;
case CBM_LANG_RUBY:
extract_channels_ruby(ctx);
break;
case CBM_LANG_ELIXIR:
extract_channels_elixir(ctx);
break;
case CBM_LANG_RUST:
extract_channels_rust(ctx);
break;
default:
break; /* no channel detection for this language */
}
}