343 lines
14 KiB
C
343 lines
14 KiB
C
#include "cbm.h"
|
|
#include "arena.h"
|
|
#include "helpers.h"
|
|
#include "lang_specs.h"
|
|
#include "extract_unified.h"
|
|
#include "tree_sitter/api.h" // TSNode, ts_node_*
|
|
#include "foundation/constants.h"
|
|
#include "extract_node_stack.h"
|
|
|
|
enum { MAX_EXCEPTION_NAME_LEN = 100, LAST_IDX = 1 };
|
|
#include <stdint.h> // uint32_t
|
|
#include <string.h>
|
|
#include <ctype.h>
|
|
|
|
// Field name length for ts_node_child_by_field_name() calls.
|
|
#define FIELD_LEN_CONSTRUCTOR 11 // strlen("constructor")
|
|
|
|
// --- Throw/Raise extraction ---
|
|
|
|
// Detect whether a node is a throw node for the given spec.
|
|
//
|
|
// Kotlin special case: this grammar models `throw X(...)` as a `jump_expression`
|
|
// (the same node also covers return/break/continue), NOT a `throw_expression`,
|
|
// so the spec's throw_node_types ("throw_expression") never matches. We treat a
|
|
// Kotlin `jump_expression` as a throw only when its first child is the `throw`
|
|
// keyword, which excludes return/break/continue without false positives.
|
|
static bool is_throw_node(TSNode node, const CBMLangSpec *spec) {
|
|
if (cbm_kind_in_set(node, spec->throw_node_types)) {
|
|
return true;
|
|
}
|
|
if (spec->language == CBM_LANG_KOTLIN && strcmp(ts_node_type(node), "jump_expression") == 0) {
|
|
uint32_t nc = ts_node_child_count(node);
|
|
if (nc > 0 && strcmp(ts_node_type(ts_node_child(node, 0)), "throw") == 0) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Resolve exception name from the first meaningful child of a throw/raise node.
|
|
static char *resolve_exception_name(CBMArena *a, TSNode throw_node, const char *source) {
|
|
uint32_t nc = ts_node_child_count(throw_node);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode child = ts_node_child(throw_node, i);
|
|
const char *ck = ts_node_type(child);
|
|
if (strcmp(ck, "raise") == 0 || strcmp(ck, "throw") == 0) {
|
|
continue;
|
|
}
|
|
if (ck[0] == ';' || ck[0] == '(' || ck[0] == ')') {
|
|
continue;
|
|
}
|
|
|
|
if (strcmp(ck, "call") == 0 || strcmp(ck, "call_expression") == 0 ||
|
|
strcmp(ck, "new_expression") == 0 || strcmp(ck, "object_creation_expression") == 0 ||
|
|
strcmp(ck, "instance_expression") == 0) {
|
|
TSNode fn = ts_node_child_by_field_name(child, TS_FIELD("function"));
|
|
if (ts_node_is_null(fn)) {
|
|
fn = ts_node_child_by_field_name(child, "constructor", FIELD_LEN_CONSTRUCTOR);
|
|
}
|
|
if (ts_node_is_null(fn)) {
|
|
fn = ts_node_child_by_field_name(child, TS_FIELD("type"));
|
|
}
|
|
if (ts_node_is_null(fn) && ts_node_named_child_count(child) > 0) {
|
|
fn = ts_node_named_child(child, 0);
|
|
}
|
|
if (!ts_node_is_null(fn)) {
|
|
return cbm_node_text(a, fn, source);
|
|
}
|
|
} else {
|
|
return cbm_node_text(a, child, source);
|
|
}
|
|
break;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Extract exception types from a Java-style throws clause.
|
|
static void extract_throws_clause(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
|
|
const char *func_qn) {
|
|
if (!spec->throws_clause_field || !spec->throws_clause_field[0]) {
|
|
return;
|
|
}
|
|
const char *kind = ts_node_type(node);
|
|
if (strcmp(kind, "method_declaration") != 0 && strcmp(kind, "constructor_declaration") != 0) {
|
|
return;
|
|
}
|
|
TSNode throws_clause = ts_node_child_by_field_name(node, spec->throws_clause_field,
|
|
(uint32_t)strlen(spec->throws_clause_field));
|
|
if (ts_node_is_null(throws_clause)) {
|
|
return;
|
|
}
|
|
uint32_t nc = ts_node_child_count(throws_clause);
|
|
for (uint32_t i = 0; i < nc; i++) {
|
|
TSNode child = ts_node_child(throws_clause, i);
|
|
const char *ck = ts_node_type(child);
|
|
if (strcmp(ck, "type_identifier") == 0 || strcmp(ck, "identifier") == 0 ||
|
|
strcmp(ck, "scoped_type_identifier") == 0) {
|
|
char *exc = cbm_node_text(ctx->arena, child, ctx->source);
|
|
if (exc && exc[0]) {
|
|
CBMThrow thr = {.exception_name = exc, .enclosing_func_qn = func_qn};
|
|
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process a single node for throw extraction (called from iterative walker).
|
|
static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
|
|
if (is_throw_node(node, spec)) {
|
|
char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
|
|
if (exc_name && exc_name[0]) {
|
|
if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
|
|
exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
|
|
}
|
|
CBMThrow thr;
|
|
thr.exception_name = exc_name;
|
|
thr.enclosing_func_qn = cbm_enclosing_func_qn_cached(ctx, node);
|
|
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
|
}
|
|
}
|
|
|
|
extract_throws_clause(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node));
|
|
}
|
|
|
|
// Iterative throw walker
|
|
static void walk_throws(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
|
TSNodeStack stack;
|
|
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
|
|
ts_nstack_push(&stack, ctx->arena, root);
|
|
while (stack.count > 0) {
|
|
TSNode node = ts_nstack_pop(&stack);
|
|
process_throw_node(ctx, node, spec);
|
|
uint32_t count = ts_node_child_count(node);
|
|
for (int i = (int)count - LAST_IDX; i >= 0; i--) {
|
|
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Read/Write detection (iterative) ---
|
|
|
|
// Resolve an assignment LHS node to the bare name being written. Handles
|
|
// common wrappers so WRITES resolve for more languages than a raw identifier:
|
|
// - expression_list (Go `x = ...` desugars left to expression_list[x])
|
|
// - index/subscript (`cache[k] = v` → write the base var `cache`)
|
|
// - field/member/selector access (`self.total = ...`, `obj.Field = ...` →
|
|
// write the trailing field name `total`/`Field`)
|
|
// Returns NULL if no simple write target can be determined.
|
|
static char *resolve_lhs_write_name(CBMExtractCtx *ctx, TSNode left) {
|
|
// Unwrap a single-element expression_list (Go).
|
|
if (strcmp(ts_node_type(left), "expression_list") == 0) {
|
|
if (ts_node_named_child_count(left) != 1) {
|
|
return NULL; // multi-assign: ambiguous, skip
|
|
}
|
|
left = ts_node_named_child(left, 0);
|
|
}
|
|
const char *lk = ts_node_type(left);
|
|
if (strcmp(lk, "identifier") == 0 || strcmp(lk, "simple_identifier") == 0) {
|
|
return cbm_node_text(ctx->arena, left, ctx->source);
|
|
}
|
|
// Indexed write: write the base operand's identifier (`cache[k]` → cache).
|
|
if (strcmp(lk, "index_expression") == 0 || strcmp(lk, "subscript_expression") == 0) {
|
|
TSNode base = ts_node_child_by_field_name(left, TS_FIELD("operand"));
|
|
if (ts_node_is_null(base)) {
|
|
base = ts_node_child_by_field_name(left, TS_FIELD("object"));
|
|
}
|
|
if (ts_node_is_null(base) && ts_node_named_child_count(left) > 0) {
|
|
base = ts_node_named_child(left, 0);
|
|
}
|
|
if (!ts_node_is_null(base)) {
|
|
const char *bk = ts_node_type(base);
|
|
if (strcmp(bk, "identifier") == 0 || strcmp(bk, "simple_identifier") == 0) {
|
|
return cbm_node_text(ctx->arena, base, ctx->source);
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
// Field/member write: write the trailing field name (`self.total` → total,
|
|
// `obj.Field` → Field). Covers Rust field_expression, C#/Java member access.
|
|
if (strcmp(lk, "field_expression") == 0 || strcmp(lk, "member_access_expression") == 0 ||
|
|
strcmp(lk, "field_access") == 0 || strcmp(lk, "selector_expression") == 0) {
|
|
TSNode fld = ts_node_child_by_field_name(left, TS_FIELD("field"));
|
|
if (ts_node_is_null(fld)) {
|
|
fld = ts_node_child_by_field_name(left, TS_FIELD("name"));
|
|
}
|
|
if (!ts_node_is_null(fld)) {
|
|
return cbm_node_text(ctx->arena, fld, ctx->source);
|
|
}
|
|
return NULL;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
// Resolve the write target of a node in an assignment_node_types set. For a
|
|
// plain assignment the target is the "left" field (or first child). For an
|
|
// increment/decrement unary expression (`x++`, `++x`, C#
|
|
// postfix_/prefix_unary_expression) there is no "left" field and the operand
|
|
// may sit on either side of the operator token, so scan named children for the
|
|
// first identifier / member-style operand. Returns a null node when no simple
|
|
// target is found.
|
|
static TSNode resolve_write_lhs_node(TSNode node) {
|
|
TSNode left = ts_node_child_by_field_name(node, TS_FIELD("left"));
|
|
if (!ts_node_is_null(left)) {
|
|
return left;
|
|
}
|
|
const char *nk = ts_node_type(node);
|
|
if (strcmp(nk, "postfix_unary_expression") == 0 || strcmp(nk, "prefix_unary_expression") == 0 ||
|
|
strcmp(nk, "update_expression") == 0) {
|
|
// Only ++/-- mutate their operand. Other unary postfix/prefix forms
|
|
// (C# null-forgiving `x!`, address-of `&x`, deref `*x`, logical `!x`)
|
|
// READ the operand — never treat them as writes.
|
|
bool is_incdec = false;
|
|
uint32_t total = ts_node_child_count(node);
|
|
for (uint32_t i = 0; i < total; i++) {
|
|
TSNode c = ts_node_child(node, i);
|
|
if (ts_node_is_named(c)) {
|
|
continue; // operator is an anonymous token
|
|
}
|
|
const char *op = ts_node_type(c);
|
|
if (strcmp(op, "++") == 0 || strcmp(op, "--") == 0) {
|
|
is_incdec = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!is_incdec) {
|
|
return (TSNode){0};
|
|
}
|
|
uint32_t cnc = ts_node_named_child_count(node);
|
|
for (uint32_t i = 0; i < cnc; i++) {
|
|
TSNode c = ts_node_named_child(node, i);
|
|
const char *ck = ts_node_type(c);
|
|
if (strcmp(ck, "identifier") == 0 || strcmp(ck, "simple_identifier") == 0 ||
|
|
strcmp(ck, "member_access_expression") == 0 ||
|
|
strcmp(ck, "field_expression") == 0 || strcmp(ck, "field_access") == 0 ||
|
|
strcmp(ck, "selector_expression") == 0 || strcmp(ck, "subscript_expression") == 0 ||
|
|
strcmp(ck, "index_expression") == 0) {
|
|
return c;
|
|
}
|
|
}
|
|
return (TSNode){0};
|
|
}
|
|
if (ts_node_child_count(node) > 0) {
|
|
return ts_node_child(node, 0);
|
|
}
|
|
return (TSNode){0};
|
|
}
|
|
|
|
// Try to emit a write for an assignment node.
|
|
static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const char *func_qn) {
|
|
TSNode left = resolve_write_lhs_node(node);
|
|
if (ts_node_is_null(left)) {
|
|
return;
|
|
}
|
|
char *name = resolve_lhs_write_name(ctx, left);
|
|
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
|
CBMReadWrite rw;
|
|
rw.var_name = name;
|
|
rw.is_write = true;
|
|
rw.enclosing_func_qn = func_qn;
|
|
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
|
|
}
|
|
}
|
|
|
|
static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
|
|
TSNodeStack stack;
|
|
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
|
|
ts_nstack_push(&stack, ctx->arena, root);
|
|
while (stack.count > 0) {
|
|
TSNode node = ts_nstack_pop(&stack);
|
|
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
|
|
try_emit_assignment_write(ctx, node, cbm_enclosing_func_qn_cached(ctx, node));
|
|
}
|
|
uint32_t count = ts_node_child_count(node);
|
|
for (int i = (int)count - LAST_IDX; i >= 0; i--) {
|
|
ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i));
|
|
}
|
|
}
|
|
}
|
|
|
|
void cbm_extract_semantic(CBMExtractCtx *ctx) {
|
|
const CBMLangSpec *spec = cbm_lang_spec(ctx->language);
|
|
if (!spec) {
|
|
return;
|
|
}
|
|
|
|
// Throws
|
|
if ((spec->throw_node_types && spec->throw_node_types[0]) ||
|
|
(spec->throws_clause_field && spec->throws_clause_field[0])) {
|
|
walk_throws(ctx, ctx->root, spec);
|
|
}
|
|
|
|
// Reads/Writes
|
|
if (spec->assignment_node_types && spec->assignment_node_types[0]) {
|
|
walk_readwrites(ctx, ctx->root, spec);
|
|
}
|
|
}
|
|
|
|
// --- Unified handlers ---
|
|
|
|
void handle_throws(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
|
bool has_throws = spec->throw_node_types && spec->throw_node_types[0];
|
|
bool has_clause = spec->throws_clause_field && spec->throws_clause_field[0];
|
|
if (!has_throws && !has_clause) {
|
|
return;
|
|
}
|
|
|
|
if (has_throws && is_throw_node(node, spec)) {
|
|
char *exc_name = resolve_exception_name(ctx->arena, node, ctx->source);
|
|
if (exc_name && exc_name[0]) {
|
|
if (strlen(exc_name) > MAX_EXCEPTION_NAME_LEN) {
|
|
exc_name[MAX_EXCEPTION_NAME_LEN] = '\0';
|
|
}
|
|
CBMThrow thr;
|
|
thr.exception_name = exc_name;
|
|
thr.enclosing_func_qn = state->enclosing_func_qn;
|
|
cbm_throws_push(&ctx->result->throws, ctx->arena, thr);
|
|
}
|
|
}
|
|
|
|
extract_throws_clause(ctx, node, spec, state->enclosing_func_qn);
|
|
}
|
|
|
|
void handle_readwrites(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, WalkState *state) {
|
|
if (!spec->assignment_node_types || !spec->assignment_node_types[0]) {
|
|
return;
|
|
}
|
|
|
|
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
|
|
TSNode left = resolve_write_lhs_node(node);
|
|
|
|
if (!ts_node_is_null(left)) {
|
|
char *name = resolve_lhs_write_name(ctx, left);
|
|
if (name && name[0] && !cbm_is_keyword(name, ctx->language)) {
|
|
CBMReadWrite rw;
|
|
rw.var_name = name;
|
|
rw.is_write = true;
|
|
rw.enclosing_func_qn = state->enclosing_func_qn;
|
|
cbm_rw_push(&ctx->result->rw, ctx->arena, rw);
|
|
}
|
|
}
|
|
}
|
|
}
|