aa6df6b906
AST traversal functions used fixed-size TSNode stack[] arrays. When the DFS stack filled up, the child-push loop exited silently, dropping entire subtrees without warning (e.g. 600 TS imports → 511, 130 Express routes capped at 512). Reported in #199; also addresses #213 (large TypeScript files producing zero nodes from stack exhaustion) and #215 (SEGV in template calls from stack overflow). Adds ts_node_stack.h — a growable stack backed by the existing arena allocator. Initial capacity matches previous fixed caps so small files allocate no extra memory; doubles on overflow instead of truncating. Old blocks are abandoned in the arena and freed on arena_destroy at the end of file extraction (no realloc, no mixed lifetimes). Applied to all 14 TSNode fixed stacks across 9 extraction files (extract_calls, extract_channels, extract_defs, extract_env_accesses, extract_imports, extract_semantic, extract_type_assigns, extract_type_refs, extract_usages). walk_defs (uses walk_defs_frame_t, different struct type) deliberately left as-is. 7 regression tests in tests/test_stack_overflow.c cover TS imports >512, JS/Python calls >512, Go calls >1024, Express routes >150, deeply nested calls, and YAML vars >256. Cherry-picked from #217 with merge-conflict resolution against post-#206/#207 main: - Makefile.cbm: keep TEST_STACK_OVERFLOW_SRCS plus main's new TEST_ZSTD_SRCS / TEST_ARTIFACT_SRCS. - extract_channels.c: keep main's scan_string_consts_js name (main also added scan_string_consts_python after the PR was opened); declare CHAN_STACK_CAP in the enum block. - test_stack_overflow.c: replace sprintf with snprintf using the actual remaining buffer size, since macOS clang rejects sprintf and naive snprintf with sizeof(p) on a char* fails -Wformat-truncation. Closes #199, #213, #215. Closes #217.
321 lines
13 KiB
C
321 lines
13 KiB
C
/*
|
|
* test_stack_overflow.c — Regression tests for GitHub issue #199.
|
|
*
|
|
* Verifies that extraction functions do NOT silently drop AST nodes
|
|
* when files exceed the fixed traversal stack capacity (512 for calls,
|
|
* 256 for variables, 64 for Elixir, etc.).
|
|
*
|
|
* These tests generate source code with more call sites / definitions /
|
|
* imports than the stack cap, then assert the extraction count matches
|
|
* the expected total. Before the fix, counts plateau at the cap.
|
|
*/
|
|
#include "test_framework.h"
|
|
#include "cbm.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
/* ── Helpers ───────────────────────────────────────────────────── */
|
|
|
|
static CBMFileResult *extract(const char *src, CBMLanguage lang, const char *proj,
|
|
const char *path) {
|
|
CBMFileResult *r = cbm_extract_file(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL);
|
|
return r;
|
|
}
|
|
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: JavaScript calls exceeding 512 stack cap
|
|
*
|
|
* Generates a JS function with 600 unique function calls.
|
|
* Before fix: walk_calls() stops at ~512 due to CALLS_STACK_CAP.
|
|
* After fix: all 600 calls are extracted.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(js_calls_exceed_512) {
|
|
const int CALL_COUNT = 600;
|
|
/* Generate calls spread across many small functions to create wide AST.
|
|
* Each function has ~20 calls, 30 functions = 600 calls total.
|
|
* The DFS stack must hold sibling function nodes simultaneously. */
|
|
const int FUNCS = 30;
|
|
const int CALLS_PER = CALL_COUNT / FUNCS;
|
|
size_t buf_sz = 256 + (size_t)CALL_COUNT * 48;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
for (int f = 0; f < FUNCS; f++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "function handler_%d() {\n", f);
|
|
for (int c = 0; c < CALLS_PER; c++) {
|
|
int idx = f * CALLS_PER + c;
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), " func_%d();\n", idx);
|
|
}
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "}\n");
|
|
}
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_JAVASCRIPT, "test", "many_calls.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Count calls that match our generated pattern */
|
|
int matched = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strncmp(r->calls.items[i].callee_name, "func_", 5) == 0) {
|
|
matched++;
|
|
}
|
|
}
|
|
|
|
printf(" calls extracted: %d / %d expected\n", matched, CALL_COUNT);
|
|
ASSERT_EQ(matched, CALL_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: Python calls exceeding 512 stack cap
|
|
*
|
|
* Same test in Python syntax to verify language-independence.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(python_calls_exceed_512) {
|
|
const int CALL_COUNT = 600;
|
|
size_t buf_sz = 256 + (size_t)CALL_COUNT * 32;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "def main():\n");
|
|
for (int i = 0; i < CALL_COUNT; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), " func_%d()\n", i);
|
|
}
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_PYTHON, "test", "many_calls.py");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
int matched = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strncmp(r->calls.items[i].callee_name, "func_", 5) == 0) {
|
|
matched++;
|
|
}
|
|
}
|
|
|
|
printf(" calls extracted: %d / %d expected\n", matched, CALL_COUNT);
|
|
ASSERT_EQ(matched, CALL_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: Go calls at exactly 1024 (well past 512 cap)
|
|
*
|
|
* Larger count to ensure the fix handles 2x overflow gracefully.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(go_calls_exceed_1024) {
|
|
const int CALL_COUNT = 1024;
|
|
size_t buf_sz = 256 + (size_t)CALL_COUNT * 40;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "package main\n\nfunc main() {\n");
|
|
for (int i = 0; i < CALL_COUNT; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "\tfunc_%d()\n", i);
|
|
}
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "}\n");
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_GO, "test", "many_calls.go");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
int matched = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strncmp(r->calls.items[i].callee_name, "func_", 5) == 0) {
|
|
matched++;
|
|
}
|
|
}
|
|
|
|
printf(" calls extracted: %d / %d expected\n", matched, CALL_COUNT);
|
|
ASSERT_EQ(matched, CALL_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: Express-style route file (original reporter's scenario)
|
|
*
|
|
* ~150 route definitions — the actual use case from issue #199.
|
|
* Each route has a handler call, so both defs and calls matter.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(express_routes_exceed_512) {
|
|
const int ROUTE_COUNT = 150;
|
|
/* Each route: app.get('/route_NNN', handler_NNN); */
|
|
size_t buf_sz = 512 + (size_t)ROUTE_COUNT * 80;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "const express = require('express');\n");
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "const app = express();\n\n");
|
|
for (int i = 0; i < ROUTE_COUNT; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "app.get('/route_%d', handler_%d);\n", i, i);
|
|
}
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "\napp.listen(3000);\n");
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_JAVASCRIPT, "test", "routes.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
/* Each app.get() is a call — count calls containing "get" */
|
|
int get_calls = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strstr(r->calls.items[i].callee_name, "get") != NULL) {
|
|
get_calls++;
|
|
}
|
|
}
|
|
|
|
printf(" route calls extracted: %d / %d expected\n", get_calls, ROUTE_COUNT);
|
|
ASSERT_EQ(get_calls, ROUTE_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: ES imports exceeding 512 (walk_es_imports uses same cap)
|
|
*
|
|
* Generate a TS file with 600 import statements.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(ts_imports_exceed_512) {
|
|
const int IMPORT_COUNT = 600;
|
|
size_t buf_sz = 256 + (size_t)IMPORT_COUNT * 64;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
for (int i = 0; i < IMPORT_COUNT; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "import { mod_%d } from './module_%d';\n", i, i);
|
|
}
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "console.log('done');\n");
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_TYPESCRIPT, "test", "many_imports.ts");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
printf(" imports extracted: %d / %d expected\n", r->imports.count, IMPORT_COUNT);
|
|
ASSERT_GTE(r->imports.count, IMPORT_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: Deeply nested calls (tests stack depth, not just breadth)
|
|
*
|
|
* Generates nested function calls: a(b(c(d(e(...)))))
|
|
* A deep call chain can overflow the stack even with fewer total nodes.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(js_deeply_nested_calls) {
|
|
const int DEPTH = 200;
|
|
/* Build: outermost( level_0( level_1( ... level_199() ... ))) */
|
|
size_t buf_sz = 256 + (size_t)DEPTH * 32;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "function main() {\n ");
|
|
for (int i = 0; i < DEPTH; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "level_%d(", i);
|
|
}
|
|
/* Close all parens */
|
|
for (int i = 0; i < DEPTH; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), ")");
|
|
}
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), ";\n}\n");
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_JAVASCRIPT, "test", "nested_calls.js");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
int matched = 0;
|
|
for (int i = 0; i < r->calls.count; i++) {
|
|
if (strncmp(r->calls.items[i].callee_name, "level_", 6) == 0) {
|
|
matched++;
|
|
}
|
|
}
|
|
|
|
printf(" nested calls extracted: %d / %d expected\n", matched, DEPTH);
|
|
ASSERT_EQ(matched, DEPTH);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Test: YAML variables exceeding 256 (walk_variables_iter cap)
|
|
*
|
|
* Generate a YAML file with 300 top-level keys.
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
TEST(yaml_vars_exceed_256) {
|
|
const int VAR_COUNT = 300;
|
|
size_t buf_sz = (size_t)VAR_COUNT * 32;
|
|
char *src = malloc(buf_sz);
|
|
ASSERT_NOT_NULL(src);
|
|
|
|
char *p = src;
|
|
for (int i = 0; i < VAR_COUNT; i++) {
|
|
p += snprintf(p, (size_t)(buf_sz - (size_t)(p - src)), "key_%d: value_%d\n", i, i);
|
|
}
|
|
|
|
CBMFileResult *r = extract(src, CBM_LANG_YAML, "test", "many_keys.yaml");
|
|
ASSERT_NOT_NULL(r);
|
|
ASSERT_FALSE(r->has_error);
|
|
|
|
int var_count = 0;
|
|
for (int i = 0; i < r->defs.count; i++) {
|
|
if (strcmp(r->defs.items[i].label, "Variable") == 0) {
|
|
var_count++;
|
|
}
|
|
}
|
|
|
|
printf(" YAML vars extracted: %d / %d expected\n", var_count, VAR_COUNT);
|
|
ASSERT_EQ(var_count, VAR_COUNT);
|
|
|
|
cbm_free_result(r);
|
|
free(src);
|
|
PASS();
|
|
}
|
|
|
|
/* ═══════════════════════════════════════════════════════════════════
|
|
* Suite registration
|
|
* ═══════════════════════════════════════════════════════════════════ */
|
|
|
|
SUITE(stack_overflow) {
|
|
cbm_init();
|
|
|
|
RUN_TEST(js_calls_exceed_512);
|
|
RUN_TEST(python_calls_exceed_512);
|
|
RUN_TEST(go_calls_exceed_1024);
|
|
RUN_TEST(express_routes_exceed_512);
|
|
RUN_TEST(ts_imports_exceed_512);
|
|
RUN_TEST(js_deeply_nested_calls);
|
|
RUN_TEST(yaml_vars_exceed_256);
|
|
|
|
cbm_shutdown();
|
|
}
|