1089 lines
42 KiB
C
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 */
|
|
}
|
|
}
|