Merge branch 'main' into fix/env-int-parse-strict

This commit is contained in:
Martin Vogel
2026-09-03 21:57:52 +02:00
committed by GitHub
30 changed files with 1634 additions and 307 deletions
+59
View File
@@ -0,0 +1,59 @@
/* @vitest-environment jsdom */
import "@testing-library/jest-dom/vitest";
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { App } from "./App";
import { messages } from "./lib/i18n";
vi.mock("./components/GraphTab", () => ({ GraphTab: () => null }));
vi.mock("./components/StatsTab", () => ({ StatsTab: () => null }));
vi.mock("./components/ControlTab", () => ({ ControlTab: () => null }));
vi.mock("./lib/i18n", async (importOriginal) => {
const actual = await importOriginal<typeof import("./lib/i18n")>();
return { ...actual, useUiMessages: () => messages.en };
});
describe("App", () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
window.history.replaceState(null, "", "/");
});
it("shows the serving binary version", async () => {
vi.stubGlobal("fetch", vi.fn(async () =>
new Response(JSON.stringify({ lang: "en", version: "0.10.8" }), {
status: 200,
headers: { "Content-Type": "application/json" },
}),
));
render(<App />);
expect(await screen.findByText("v0.10.8")).toBeVisible();
});
it("hides the version when the config has no string version", async () => {
const fetchMock = vi.fn(async () =>
new Response(JSON.stringify({ lang: "en", version: 108 }), { status: 200 }),
);
vi.stubGlobal("fetch", fetchMock);
render(<App />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
});
it("hides the version when the config request fails", async () => {
const fetchMock = vi.fn(async () => {
throw new Error("offline");
});
vi.stubGlobal("fetch", fetchMock);
render(<App />);
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
});
});
+24
View File
@@ -33,8 +33,24 @@ function routeUrl(tab: TabId, project: string | null): string {
export function App() {
const t = useUiMessages();
const [route, setRoute] = useState<RouteState>(readRoute);
const [version, setVersion] = useState<string | null>(null);
const { tab: activeTab, project: selectedProject } = route;
useEffect(() => {
let cancelled = false;
void fetch("/api/ui-config")
.then((response) => (response.ok ? response.json() : null))
.then((config) => {
if (!cancelled && typeof config?.version === "string" && config.version) {
setVersion(config.version);
}
})
.catch(() => {});
return () => {
cancelled = true;
};
}, []);
/* Normalize the URL on first load so it always carries the current route. */
useEffect(() => {
const initial = readRoute();
@@ -73,6 +89,14 @@ export function App() {
<span className="text-[13px] font-semibold text-foreground/90 tracking-tight">
Codebase Memory
</span>
{version && (
<span
className="translate-y-px text-[10px] font-mono text-foreground/30"
title="Server version"
>
{version.startsWith("v") ? version : `v${version}`}
</span>
)}
</div>
{/* Tabs inline in header */}
+55 -9
View File
@@ -1,3 +1,7 @@
/* Full declaration set for the same CBMArena, and it must precede cbm.h:
* internal/cbm/arena.h declares a subset and the two share the CBM_ARENA_H
* guard, so whichever is included first is the one this file sees. */
#include "foundation/arena.h" // cbm_arena_init_sized
#include "cbm.h"
#include "arena.h" // CBMArena, cbm_arena_init/alloc/strdup/destroy
#include "helpers.h"
@@ -817,9 +821,13 @@ static void cbm_error_regions_push(cbm_error_regions_t *acc, TSNode n) {
* untouched.
*
* #1746: the Dockerfile grammar places that zero-width missing newline before
* trailing horizontal whitespace rather than at raw EOF. Preserve the broad
* exact-EOF rule above; only extend it past spaces/tabs when the missing token
* is specifically a newline. */
* trailing whitespace rather than at raw EOF. Preserve the broad exact-EOF
* rule above; only extend it past blanks when the missing token is specifically
* a newline. */
static bool cbm_is_blank_not_newline(char c) {
return c == ' ' || c == '\t' || c == '\v' || c == '\f' || c == '\r';
}
static bool cbm_is_eof_terminator_miss(TSNode n, const char *source, int source_len) {
if (!ts_node_is_missing(n) || source_len < 0) {
return false;
@@ -836,7 +844,7 @@ static bool cbm_is_eof_terminator_miss(TSNode n, const char *source, int source_
return false;
}
for (uint32_t i = end; i < (uint32_t)source_len; i++) {
if (source[i] != ' ' && source[i] != '\t') {
if (!cbm_is_blank_not_newline(source[i])) {
return false;
}
}
@@ -1186,11 +1194,27 @@ CBMFileResult *cbm_extract_file(const char *source, int source_len, CBMLanguage
return r;
}
CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path,
int64_t timeout_micros, const char **extra_defines,
const char **include_paths, const CBMMacroTable *macro_table,
const CBMReturnTypeTable *return_type_table) {
/* Initial block for the per-file traversal scratch arena, chosen by measuring
* arena_grow on a 14k-file TypeScript tree: it fires on one file in 12,000 at
* both this size and at 1 MB, and on most files at 256 KB, where the two
* channel walks alone are exactly 262144 bytes. 512 KB therefore buys the same
* growth behaviour as 1 MB for half the resident block per worker. It is also
* exactly MI_LARGE_MAX_OBJ_SIZE in the vendored mimalloc
* (vendored/mimalloc/include/mimalloc/types.h:426, MI_LARGE_PAGE_SIZE/8 with
* MI_ENABLE_LARGE_PAGES defaulting to 1 at :115 and not overridden here), so
* the block is still bin-allocated from a large page. Growth is not free at
* this size for the same reason: arena_grow doubles to 1 MiB, which is above
* that bound and so a singleton OS allocation. One file in twelve thousand
* pays it, which is why the cost is accepted. */
enum { CBM_EXTRACT_SCRATCH_BLOCK = CBM_SZ_512 * CBM_SZ_1K };
static CBMFileResult *extract_file_ex_body(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path,
int64_t timeout_micros, const char **extra_defines,
const char **include_paths,
const CBMMacroTable *macro_table,
const CBMReturnTypeTable *return_type_table,
CBMArena *scratch) {
// Allocate result on heap (arena inside for all string data)
enum { SINGLE = 1 };
CBMFileResult *result = (CBMFileResult *)calloc(SINGLE, sizeof(CBMFileResult));
@@ -1301,6 +1325,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
// Build extraction context
CBMExtractCtx ctx = {
.arena = a,
.scratch = scratch,
.result = result,
.source = source,
.source_len = source_len,
@@ -1429,6 +1454,7 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
// Build context for expanded source — extract only calls via unified extractor
CBMExtractCtx pp_ctx = {
.arena = a,
.scratch = scratch,
.result = result,
.source = expanded,
.source_len = expanded_len,
@@ -1665,6 +1691,26 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
return result;
}
/* Public entry. Owns the traversal scratch arena for the whole of one file's
* extraction: created here, handed to the body as ctx->scratch, destroyed on
* the way out. The body has seven early returns, so bracketing it in a wrapper
* is what keeps that to one create and one destroy. If the arena cannot be
* created, the body is handed NULL and the traversal stacks fall back to the
* result arena, which is what shipped before #1997. */
CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLanguage language,
const char *project, const char *rel_path,
int64_t timeout_micros, const char **extra_defines,
const char **include_paths, const CBMMacroTable *macro_table,
const CBMReturnTypeTable *return_type_table) {
CBMArena scratch;
cbm_arena_init_sized(&scratch, CBM_EXTRACT_SCRATCH_BLOCK);
CBMFileResult *result = extract_file_ex_body(
source, source_len, language, project, rel_path, timeout_micros, extra_defines,
include_paths, macro_table, return_type_table, scratch.nblocks > 0 ? &scratch : NULL);
cbm_arena_destroy(&scratch);
return result;
}
void cbm_free_result(CBMFileResult *result) {
if (!result) {
return;
+11
View File
@@ -269,6 +269,10 @@ typedef struct {
// pass_lsp_cross.c. Default false.
bool requires_lsp_resolution; // synthetic semantic candidate (for example an implicit
// C++ operator). Never fall back to textual resolution.
bool callee_is_locally_bound; // bare call foo() whose callee identifier is bound as a
// parameter of an enclosing function, so it cannot be the
// module-level foo. Python only today. Read by the
// weak-local-binding guard. Default false.
} CBMCall;
typedef struct {
@@ -589,6 +593,13 @@ typedef struct {
typedef struct {
CBMArena *arena;
/* Scratch for AST traversal, owned by the cbm_extract_file_ex call that
* built this context and destroyed when it returns. Nothing a
* CBMFileResult points at may be allocated here: `arena` is the result's
* own, and it outlives extraction by the whole pipeline (#1997). NULL in a
* context built without one, in which case the stacks fall back to
* `arena`. */
CBMArena *scratch;
CBMFileResult *result;
const char *source;
int source_len;
+73 -1
View File
@@ -61,7 +61,8 @@ static const char *lookup_url_builder(const CBMExtractCtx *ctx, const char *name
static int is_string_like(const char *kind) {
return (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);
strcmp(kind, "raw_string_literal") == 0 || strcmp(kind, "string_content") == 0 ||
strcmp(kind, "line_string_literal") == 0);
}
/* Strip surrounding quotes from a string, return arena-allocated copy */
@@ -2286,6 +2287,18 @@ static const char *extract_url_or_topic_arg(CBMExtractCtx *ctx, TSNode args) {
if (strcmp(ts_node_type(arg), "argument") == 0 && ts_node_named_child_count(arg) > 0) {
arg = ts_node_named_child(arg, 0);
}
/* Swift wraps each argument in a value_argument that may lead with its
* label, so `data(from: url)` would otherwise yield the label `from`
* rather than the value. Step past a leading value_argument_label. */
if (strcmp(ts_node_type(arg), "value_argument") == 0 &&
ts_node_named_child_count(arg) > 0) {
TSNode val = ts_node_named_child(arg, 0);
if (strcmp(ts_node_type(val), "value_argument_label") == 0 &&
ts_node_named_child_count(arg) > 1) {
val = ts_node_named_child(arg, 1);
}
arg = val;
}
const char *ak = ts_node_type(arg);
if (strcmp(ak, "keyword_argument") == 0 || strcmp(ak, "pair") == 0) {
@@ -2973,6 +2986,41 @@ static bool python_receiver_is_exempt(CBMExtractCtx *ctx, TSNode receiver) {
return false;
}
/* Name bound by one Python parameter node, or NULL when the shape binds none.
* Covers every binding form a `parameters` / `lambda_parameters` list produces:
* a bare `identifier`, the `name` field of default/typed parameters, and the
* identifier under a `*args` / `**kwargs` splat. A shape with no identifier (the
* bare `*` keyword separator) yields NULL and simply matches nothing. */
/* True when the callee of a BARE Python call `foo()` is bound as a parameter of
* an enclosing function or lambda -- the bare-call counterpart of
* python_receiver_is_exempt above.
*
* A parameter binding shadows any module-level `foo` for the whole body, so
* resolving such a call to a project Function/Method by short name alone
* fabricates the edge BY CONSTRUCTION: `def _run_with_heavy_slot(run): run()`
* must not bind an unrelated `SatoriLive.run`. Unlike a receiver type this is
* decidable from the AST outright, with no flow analysis and no list of
* "generic-looking" callee names -- Python forbids `global` on a parameter, and
* a parameter is in scope for the entire body regardless of position.
*
* The answer is CARRIED BY THE WALK rather than recomputed here. The unified
* walk binds a def's or lambda's parameters when it opens that scope and
* unwinds them when it closes it, so this is an O(1) map lookup. Deciding it
* by ascending the tree instead -- with ts_node_parent() or with a copied walk
* cursor -- costs O(depth) per call, and since every level of f(f(f(...))) is
* itself a bare call, that is quadratic across the file: the 30,000-deep
* fixture in tests/test_stack_overflow.c turned it into a hang rather than a
* slowdown. An O(1) lookup needs no hop cap, so unlike a capped walk this can
* no longer fail open on deep-but-ordinary code.
*
* It still answers false if the walk's own tracking hit an allocation failure,
* which costs a suppression and never a true edge. */
static bool python_callee_is_bound_parameter(CBMExtractCtx *ctx, WalkState *state,
TSNode callee_ident) {
const char *callee_name = cbm_node_text(ctx->arena, callee_ident, ctx->source);
return cbm_walk_python_param_is_bound(state, callee_name);
}
static bool is_objectscript_language(CBMLanguage language) {
return language == CBM_LANG_OBJECTSCRIPT_UDL || language == CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
@@ -3045,6 +3093,19 @@ static TSNode objectscript_call_args(TSNode node) {
: cbm_find_child_by_kind(macro_function, "method_args");
}
/* Swift models a call as a target expression plus a call_suffix, and its grammar
* declares no "arguments" field at all, so the generic field lookup finds
* nothing for every Swift call. Reach the argument list through the suffix
* instead. A trailing closure has a call_suffix with no value_arguments, which
* returns a null node and leaves the call without a string argument, as before. */
static TSNode swift_call_args(TSNode node) {
TSNode suffix = cbm_find_child_by_kind(node, "call_suffix");
if (ts_node_is_null(suffix)) {
return (TSNode){0};
}
return cbm_find_child_by_kind(suffix, "value_arguments");
}
static bool node_has_token(TSNode node, const char *token) {
uint32_t count = ts_node_child_count(node);
for (uint32_t i = 0; i < count; i++) {
@@ -3597,11 +3658,18 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
// (`accelerator.print()` must not bind MockAccelerator.print).
// Imported receivers stay unflagged: module.function() is Python's
// canonical cross-file call and the import map resolves it.
// A BARE Python call foo() whose callee is bound as a parameter of an
// enclosing scope cannot be the module-level foo, so short-name
// resolution would fabricate the edge (`def f(run): run()` must not
// bind SatoriLive.run). Distinct from is_method: there is no receiver
// here, so the weak-member guard cannot see this class at all.
if (ctx->language == CBM_LANG_PYTHON && strcmp(ts_node_type(node), "call") == 0) {
TSNode fn = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "attribute") == 0) {
TSNode obj = ts_node_child_by_field_name(fn, TS_FIELD("object"));
call.is_method = !python_receiver_is_exempt(ctx, obj);
} else if (!ts_node_is_null(fn) && strcmp(ts_node_type(fn), "identifier") == 0) {
call.callee_is_locally_bound = python_callee_is_bound_parameter(ctx, state, fn);
}
}
// TS/JS/TSX receiver-aware guard (#592/#606 direction; same intent
@@ -3634,6 +3702,10 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
if (ts_node_is_null(args) && is_objectscript_language(ctx->language)) {
args = objectscript_call_args(node);
}
// Swift has no "arguments" field either; its args hang off call_suffix.
if (ts_node_is_null(args) && ctx->language == CBM_LANG_SWIFT) {
args = swift_call_args(node);
}
if (!ts_node_is_null(args)) {
call.first_string_arg = extract_url_or_topic_arg(ctx, args);
/* #952: routes registered inside Laravel `prefix()->group()`
+30 -30
View File
@@ -101,8 +101,8 @@ static const char *literal_from_first_child(CBMExtractCtx *ctx, TSNode node) {
* 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0 && tbl->count < CHAN_CONST_CAP) {
TSNode node = ts_nstack_pop(&stack);
@@ -128,15 +128,15 @@ static void scan_string_consts_js(CBMExtractCtx *ctx, chan_const_table_t *tbl) {
}
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0 && tbl->count < CHAN_CONST_CAP) {
TSNode node = ts_nstack_pop(&stack);
@@ -163,7 +163,7 @@ static void scan_string_consts_python(CBMExtractCtx *ctx, chan_const_table_t *tb
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -373,15 +373,15 @@ static void extract_channels_js(CBMExtractCtx *ctx) {
/* 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, 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);
ts_nstack_push_children(&stack, node);
}
}
@@ -549,8 +549,8 @@ static void extract_channels_python(CBMExtractCtx *ctx) {
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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -562,7 +562,7 @@ static void extract_channels_python(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -627,8 +627,8 @@ static void go_process_call(CBMExtractCtx *ctx, TSNode 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -637,7 +637,7 @@ static void extract_channels_go(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -724,8 +724,8 @@ static void java_process_annotation(CBMExtractCtx *ctx, TSNode annotation) {
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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -737,7 +737,7 @@ static void extract_channels_java(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -793,8 +793,8 @@ static void csharp_process_call(CBMExtractCtx *ctx, TSNode 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -803,7 +803,7 @@ static void extract_channels_csharp(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -856,8 +856,8 @@ static void ruby_process_call(CBMExtractCtx *ctx, TSNode 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -866,7 +866,7 @@ static void extract_channels_ruby(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -958,8 +958,8 @@ static void elixir_process_function_def(CBMExtractCtx *ctx, TSNode 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -971,7 +971,7 @@ static void extract_channels_elixir(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -1034,8 +1034,8 @@ static void rust_process_call(CBMExtractCtx *ctx, TSNode 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);
ts_nstack_init(&stack, ctx, CHAN_STACK_CAP);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -1044,7 +1044,7 @@ static void extract_channels_rust(CBMExtractCtx *ctx) {
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
+13 -13
View File
@@ -5316,8 +5316,8 @@ static TSNode emit_elixir_module_class(CBMExtractCtx *ctx, TSNode cur) {
static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
(void)spec;
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_64);
ts_nstack_push(&stack, ctx->arena, node);
ts_nstack_init(&stack, ctx, CBM_SZ_64);
ts_nstack_push(&stack, node);
while (stack.count > 0) {
TSNode cur = ts_nstack_pop(&stack);
@@ -5345,7 +5345,7 @@ static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
for (int di = (int)dbc - SKIP_CHAR; di >= 0; di--) {
TSNode dchild = ts_node_child(do_block, (uint32_t)di);
if (!ts_node_is_null(dchild) && strcmp(ts_node_type(dchild), "call") == 0) {
ts_nstack_push(&stack, ctx->arena, dchild);
ts_nstack_push(&stack, dchild);
}
}
}
@@ -6285,8 +6285,8 @@ static void extract_var_names(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// Used by YAML, TOML, INI, JSON.
static void walk_variables_iter(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_256);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, CBM_SZ_256);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -6310,7 +6310,7 @@ static void walk_variables_iter(CBMExtractCtx *ctx, TSNode root, const CBMLangSp
strcmp(ck, "section") == 0 || strcmp(ck, "object") == 0 ||
strcmp(ck, "array") == 0 || strcmp(ck, "pair") == 0 || strcmp(ck, "element") == 0 ||
strcmp(ck, "content") == 0) {
ts_nstack_push(&stack, ctx->arena, child);
ts_nstack_push(&stack, child);
}
}
}
@@ -7144,10 +7144,10 @@ static void wd_push_children_reverse(wd_stack_t *s, TSNode node, const char *enc
// Push nested class nodes from a class body container onto the defs stack.
// Iteratively walks into wrapper nodes (field_declaration, template_declaration).
static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, wd_stack_t *s,
const char *enclosing_qn, CBMArena *arena) {
const char *enclosing_qn, const CBMExtractCtx *ctx) {
TSNodeStack nc_stack;
ts_nstack_init(&nc_stack, arena, NESTED_CLASS_STACK_CAP);
ts_nstack_push(&nc_stack, arena, body);
ts_nstack_init(&nc_stack, ctx, NESTED_CLASS_STACK_CAP);
ts_nstack_push(&nc_stack, body);
while (nc_stack.count > 0) {
TSNode cur = ts_nstack_pop(&nc_stack);
@@ -7162,7 +7162,7 @@ static void push_nested_class_nodes(TSNode body, const CBMLangSpec *spec, wd_sta
const char *ck = ts_node_type(child);
if (strcmp(ck, "field_declaration") == 0 ||
strcmp(ck, "template_declaration") == 0 || strcmp(ck, "declaration") == 0) {
ts_nstack_push(&nc_stack, arena, child);
ts_nstack_push(&nc_stack, child);
}
}
}
@@ -7278,7 +7278,7 @@ static void extract_typescript_namespace_def(CBMExtractCtx *ctx, TSNode node,
// Push nested class children from a class body container onto the walk stack.
static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_stack_t *s,
const char *new_enclosing, CBMArena *arena) {
const char *new_enclosing, const CBMExtractCtx *ctx) {
/* Use the same language-aware body selection as method extraction. The old
* independent spelling list omitted valid containers such as Scala's
* `template_body` and Solidity's contract body. Methods were extracted
@@ -7294,7 +7294,7 @@ static void push_class_body_children(TSNode node, const CBMLangSpec *spec, wd_st
body = find_class_member_body(node, spec->language);
}
if (!ts_node_is_null(body)) {
push_nested_class_nodes(body, spec, s, new_enclosing, arena);
push_nested_class_nodes(body, spec, s, new_enclosing, ctx);
return;
}
@@ -7880,7 +7880,7 @@ static void walk_defs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec,
if (cbm_kind_in_set(node, spec->class_node_types)) {
extract_class_def(ctx, node, spec);
const char *new_enclosing = compute_class_qn(ctx, node, frame.enclosing_class_qn);
push_class_body_children(node, spec, &s, new_enclosing, ctx->arena);
push_class_body_children(node, spec, &s, new_enclosing, ctx);
continue;
}
+3 -3
View File
@@ -140,8 +140,8 @@ static bool is_env_var_name(const char *s) {
// Iterative env access walker — explicit stack
static void walk_env_accesses(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, 4096);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, 4096);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -163,7 +163,7 @@ static void walk_env_accesses(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec
continue; // don't push children (avoid double-counting)
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
+108 -109
View File
@@ -506,8 +506,8 @@ static bool process_commonjs_require(CBMExtractCtx *ctx, TSNode call) {
static void walk_es_imports(CBMExtractCtx *ctx, TSNode root) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -540,7 +540,7 @@ static void walk_es_imports(CBMExtractCtx *ctx, TSNode root) {
}
if (push_children) {
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
}
@@ -1093,8 +1093,8 @@ static void parse_haskell_imports(CBMExtractCtx *ctx) {
static void parse_zig_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "builtin_function") == 0) {
@@ -1111,7 +1111,7 @@ static void parse_zig_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -1163,8 +1163,8 @@ static void process_wolfram_needs(CBMExtractCtx *ctx, TSNode node) {
static void walk_wolfram_imports(CBMExtractCtx *ctx, TSNode root) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -1176,7 +1176,7 @@ static void walk_wolfram_imports(CBMExtractCtx *ctx, TSNode root) {
process_wolfram_needs(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -1666,8 +1666,8 @@ static void capture_namespace_decl(CBMExtractCtx *ctx) {
static void parse_hare_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "use_statement") == 0) {
@@ -1686,7 +1686,7 @@ static void parse_hare_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -1698,8 +1698,8 @@ static void parse_hare_imports(CBMExtractCtx *ctx) {
static void parse_pascal_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "declUses") == 0) {
@@ -1717,7 +1717,7 @@ static void parse_pascal_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -1730,8 +1730,8 @@ static void parse_pascal_imports(CBMExtractCtx *ctx) {
static void parse_powershell_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "command") == 0) {
@@ -1741,8 +1741,8 @@ static void parse_powershell_imports(CBMExtractCtx *ctx) {
/* Find the last generic_token anywhere under the command — that
* is the module path / namespace / assembly being imported. */
TSNodeStack inner;
ts_nstack_init(&inner, a, CBM_SZ_512);
ts_nstack_push(&inner, a, node);
ts_nstack_init(&inner, ctx, CBM_SZ_512);
ts_nstack_push(&inner, node);
const char *last_tok = NULL;
uint32_t last_start = 0;
while (inner.count > 0) {
@@ -1756,7 +1756,7 @@ static void parse_powershell_imports(CBMExtractCtx *ctx) {
last_start = sb;
}
}
ts_nstack_push_children(&inner, a, c);
ts_nstack_push_children(&inner, c);
}
if (last_tok && last_tok[0]) {
CBMImport imp = {.local_name = path_last(a, last_tok),
@@ -1766,7 +1766,7 @@ static void parse_powershell_imports(CBMExtractCtx *ctx) {
}
continue; /* commands don't nest imports further */
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -1993,17 +1993,16 @@ static void lisp_process_list(CBMExtractCtx *ctx, TSNode node) {
* just root children. Stack-based (not recursive) to avoid deep-nesting stack
* overflow, matching the other walkers in this file. */
static void parse_lisp_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *nt = ts_node_type(node);
if (strcmp(nt, "list") == 0 || strcmp(nt, "list_lit") == 0) {
lisp_process_list(ctx, node);
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2013,8 +2012,8 @@ static void parse_lisp_imports(CBMExtractCtx *ctx) {
static void parse_starlark_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "call") == 0) {
@@ -2042,7 +2041,7 @@ static void parse_starlark_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2053,8 +2052,8 @@ static void parse_starlark_imports(CBMExtractCtx *ctx) {
static void parse_tcl_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "command") == 0) {
@@ -2076,7 +2075,7 @@ static void parse_tcl_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2087,8 +2086,8 @@ static void parse_tcl_imports(CBMExtractCtx *ctx) {
static void parse_teal_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "function_call") == 0) {
@@ -2112,7 +2111,7 @@ static void parse_teal_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2122,8 +2121,8 @@ static void parse_teal_imports(CBMExtractCtx *ctx) {
static void parse_zsh_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "command") == 0) {
@@ -2143,7 +2142,7 @@ static void parse_zsh_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2230,15 +2229,15 @@ static void parse_html_imports(CBMExtractCtx *ctx) {
parse_embedded_imports(ctx);
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "start_tag") == 0 ||
strcmp(ts_node_type(node), "self_closing_tag") == 0) {
html_extract_tag_src(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2297,8 +2296,8 @@ static void push_string_descendant_import(CBMExtractCtx *ctx, TSNode node) {
static void parse_cmake_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "normal_command") == 0) {
@@ -2316,15 +2315,15 @@ static void parse_cmake_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
// --- BitBake imports: require/include path ---
static void parse_bitbake_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2332,7 +2331,7 @@ static void parse_bitbake_imports(CBMExtractCtx *ctx) {
strcmp(k, "inherit_directive") == 0) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2344,8 +2343,8 @@ static void parse_bitbake_imports(CBMExtractCtx *ctx) {
static void parse_meson_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2368,35 +2367,35 @@ static void parse_meson_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
// --- Kconfig imports: source "path" ---
static void parse_kconfig_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (ts_node_is_named(node) && strcmp(ts_node_type(node), "source") == 0) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
// --- GN imports: import("//path") ---
static void parse_gn_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "import_statement") == 0) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2420,8 +2419,8 @@ static void parse_just_imports(CBMExtractCtx *ctx) {
static void parse_nix_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "apply_expression") == 0) {
@@ -2440,15 +2439,15 @@ static void parse_nix_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
// --- Jsonnet imports: import 'path' / importstr 'path' ---
static void parse_jsonnet_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2456,15 +2455,15 @@ static void parse_jsonnet_imports(CBMExtractCtx *ctx) {
strcmp(k, "importbin") == 0)) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
// --- Pkl imports: amends/extends/import "path" ---
static void parse_pkl_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2472,7 +2471,7 @@ static void parse_pkl_imports(CBMExtractCtx *ctx) {
strcmp(k, "importExpr") == 0) {
push_string_descendant_import(ctx, node);
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2480,8 +2479,8 @@ static void parse_pkl_imports(CBMExtractCtx *ctx) {
static void parse_nickel_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
/* The grammar emits an anonymous `import` token followed by a
@@ -2502,7 +2501,7 @@ static void parse_nickel_imports(CBMExtractCtx *ctx) {
}
}
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2526,8 +2525,8 @@ static void parse_thrift_imports(CBMExtractCtx *ctx) {
// --- Cap'n Proto imports: using X = import "path" ---
static void parse_capnp_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2536,7 +2535,7 @@ static void parse_capnp_imports(CBMExtractCtx *ctx) {
push_string_descendant_import(ctx, node);
continue; /* don't double-emit from nested import_path */
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2544,8 +2543,8 @@ static void parse_capnp_imports(CBMExtractCtx *ctx) {
static void parse_dlang_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "import_declaration") == 0) {
@@ -2559,15 +2558,15 @@ static void parse_dlang_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
// --- TableGen imports: include "path.td" ---
static void parse_tablegen_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2576,22 +2575,22 @@ static void parse_tablegen_imports(CBMExtractCtx *ctx) {
push_string_descendant_import(ctx, node);
continue;
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
// --- Crystal imports: require "./path" ---
static void parse_crystal_imports(CBMExtractCtx *ctx) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, CBM_SZ_512);
ts_nstack_push(&stack, ctx->arena, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (ts_node_is_named(node) && strcmp(ts_node_type(node), "require") == 0) {
push_string_descendant_import(ctx, node);
continue;
}
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2599,8 +2598,8 @@ static void parse_crystal_imports(CBMExtractCtx *ctx) {
static void parse_fsharp_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2616,7 +2615,7 @@ static void parse_fsharp_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2624,8 +2623,8 @@ static void parse_fsharp_imports(CBMExtractCtx *ctx) {
static void parse_ada_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "with_clause") == 0) {
@@ -2644,7 +2643,7 @@ static void parse_ada_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2655,8 +2654,8 @@ static void parse_ada_imports(CBMExtractCtx *ctx) {
static void parse_elm_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "import_clause") == 0) {
@@ -2678,7 +2677,7 @@ static void parse_elm_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2689,8 +2688,8 @@ static void parse_elm_imports(CBMExtractCtx *ctx) {
static void parse_move_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "use_declaration") == 0) {
@@ -2707,7 +2706,7 @@ static void parse_move_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2741,8 +2740,8 @@ static char *smali_demangle_descriptor(CBMArena *a, const char *desc) {
static void parse_smali_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2757,7 +2756,7 @@ static void parse_smali_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2767,8 +2766,8 @@ static void parse_smali_imports(CBMExtractCtx *ctx) {
static void parse_tlaplus_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
const char *k = ts_node_type(node);
@@ -2787,7 +2786,7 @@ static void parse_tlaplus_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2799,8 +2798,8 @@ static void parse_tlaplus_imports(CBMExtractCtx *ctx) {
static void parse_vhdl_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "use_clause") == 0) {
@@ -2818,7 +2817,7 @@ static void parse_vhdl_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2828,8 +2827,8 @@ static void parse_vhdl_imports(CBMExtractCtx *ctx) {
static void parse_wit_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "use_item") == 0) {
@@ -2847,7 +2846,7 @@ static void parse_wit_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2858,8 +2857,8 @@ static void parse_wit_imports(CBMExtractCtx *ctx) {
static void parse_smithy_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "use_statement") == 0) {
@@ -2881,7 +2880,7 @@ static void parse_smithy_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
@@ -2891,8 +2890,8 @@ static void parse_smithy_imports(CBMExtractCtx *ctx) {
static void parse_hyprlang_imports(CBMExtractCtx *ctx) {
CBMArena *a = ctx->arena;
TSNodeStack stack;
ts_nstack_init(&stack, a, CBM_SZ_512);
ts_nstack_push(&stack, a, ctx->root);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, ctx->root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (strcmp(ts_node_type(node), "source") == 0) {
@@ -2909,7 +2908,7 @@ static void parse_hyprlang_imports(CBMExtractCtx *ctx) {
}
continue;
}
ts_nstack_push_children(&stack, a, node);
ts_nstack_push_children(&stack, node);
}
}
+30 -11
View File
@@ -4,14 +4,22 @@
* Replaces fixed-size TSNode stack[] arrays that silently drop AST subtrees
* when the stack overflows (GitHub issue #199).
*
* Uses the arena allocator for zero-fragmentation growth: old blocks are
* abandoned (freed when the arena is destroyed at end of file extraction).
* Initial capacity matches the previous fixed caps so small files allocate
* no extra memory.
* Traversal stacks are scratch: nothing in a CBMFileResult ever points into
* one. They are therefore cut from ctx->scratch, which the enclosing
* cbm_extract_file_ex call owns and destroys on the way out, and never from
* ctx->arena, which the pipeline holds for every file until the whole result
* cache is freed (#1997).
*
* Growth abandons the old buffer in that scratch arena, which is free because
* the arena dies with the file. The initial capacities below are unchanged, but
* they are no longer what a small file costs: cbm_extract_file_ex creates the
* scratch arena's first block for every call, including calls that build no
* stack at all, and reclaims it on the way out.
*/
#ifndef CBM_EXTRACT_NODE_STACK_H
#define CBM_EXTRACT_NODE_STACK_H
#include "cbm.h" /* CBMExtractCtx: a stack draws from ctx->scratch */
#include "arena.h"
#include "tree_sitter/api.h"
#include <string.h> /* memcpy */
@@ -20,20 +28,31 @@ typedef struct {
TSNode *items;
int count;
int cap;
/* The arena every allocation for this stack comes from, recorded once by
* ts_nstack_init so push() cannot be handed a different one. It is
* ctx->scratch, or ctx->arena as the fallback when the context has none. */
CBMArena *scratch;
} TSNodeStack;
/* Initialize a stack with the given initial capacity, arena-allocated. */
static inline void ts_nstack_init(TSNodeStack *s, CBMArena *arena, int initial_cap) {
/* Initialize a stack with the given initial capacity, allocated from the
* context's traversal scratch. Taking the context rather than an arena is
* deliberate: it makes handing over ctx->arena, or a local alias of it, a type
* error rather than a retention bug nobody notices. A context built without a
* scratch falls back to ctx->arena, which is the behaviour that shipped before
* #1997, so no caller ever gets a NULL arena and silently loses nodes. */
static inline void ts_nstack_init(TSNodeStack *s, const CBMExtractCtx *ctx, int initial_cap) {
CBMArena *arena = ctx->scratch ? ctx->scratch : ctx->arena;
s->scratch = arena;
s->items = (TSNode *)cbm_arena_alloc(arena, (size_t)initial_cap * sizeof(TSNode));
s->count = 0;
s->cap = s->items ? initial_cap : 0;
}
/* Push a node onto the stack, growing 2x if needed. */
static inline void ts_nstack_push(TSNodeStack *s, CBMArena *arena, TSNode node) {
static inline void ts_nstack_push(TSNodeStack *s, TSNode node) {
if (s->count >= s->cap) {
int new_cap = s->cap ? s->cap * 2 : 512;
TSNode *new_items = (TSNode *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(TSNode));
TSNode *new_items = (TSNode *)cbm_arena_alloc(s->scratch, (size_t)new_cap * sizeof(TSNode));
if (!new_items)
return; /* OOM: best-effort, stop growing */
if (s->items && s->count > 0) {
@@ -54,7 +73,7 @@ static inline TSNode ts_nstack_pop(TSNodeStack *s) {
/*
* Push all children of `node` so they POP in forward (source) order — a drop-in
* replacement for the common idiom:
* for (int i = (int)count - 1; i >= 0; i--) ts_nstack_push(s, a, ts_node_child(node, i));
* for (int i = (int)count - 1; i >= 0; i--) ts_nstack_push(s, ts_node_child(node, i));
*
* That idiom calls ts_node_child(node, i) once per index, and ts_node_child is
* O(i) in tree-sitter (it walks the child iterator from the first child each
@@ -63,12 +82,12 @@ static inline TSNode ts_nstack_pop(TSNodeStack *s) {
* files). This helper enumerates children in a single O(N) cursor pass, then
* reverses the just-pushed segment so pop order is identical to the old idiom.
*/
static inline void ts_nstack_push_children(TSNodeStack *s, CBMArena *arena, TSNode node) {
static inline void ts_nstack_push_children(TSNodeStack *s, TSNode node) {
int base = s->count;
TSTreeCursor cursor = ts_tree_cursor_new(node);
if (ts_tree_cursor_goto_first_child(&cursor)) {
do {
ts_nstack_push(s, arena, ts_tree_cursor_current_node(&cursor));
ts_nstack_push(s, ts_tree_cursor_current_node(&cursor));
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
+6 -6
View File
@@ -125,14 +125,14 @@ static void process_throw_node(CBMExtractCtx *ctx, TSNode node, const CBMLangSpe
// 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);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, 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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -271,8 +271,8 @@ static void try_emit_assignment_write(CBMExtractCtx *ctx, TSNode node, const cha
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);
ts_nstack_init(&stack, ctx, CBM_SZ_512);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
if (cbm_kind_in_set(node, spec->assignment_node_types)) {
@@ -280,7 +280,7 @@ static void walk_readwrites(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
+3 -3
View File
@@ -186,12 +186,12 @@ static void process_type_assign_node(CBMExtractCtx *ctx, TSNode node, const CBML
// Walk AST for assignment patterns where RHS is a constructor call.
static void walk_type_assigns(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, 4096);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, 4096);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
process_type_assign_node(ctx, node, spec, cbm_enclosing_func_qn_cached(ctx, node));
ts_nstack_push_children(&stack, ctx->arena, node);
ts_nstack_push_children(&stack, node);
}
}
+6 -6
View File
@@ -246,14 +246,14 @@ static void process_body_type_ref(CBMExtractCtx *ctx, TSNode node, const char *f
// Walk function body for type references (casts, type assertions, local var types, generics).
static void walk_body_type_refs(CBMExtractCtx *ctx, TSNode root, const char *func_qn) {
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, 4096);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, 4096);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
process_body_type_ref(ctx, node, func_qn);
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
@@ -290,8 +290,8 @@ static void walk_type_refs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *s
}
TSNodeStack stack;
ts_nstack_init(&stack, ctx->arena, 4096);
ts_nstack_push(&stack, ctx->arena, root);
ts_nstack_init(&stack, ctx, 4096);
ts_nstack_push(&stack, root);
while (stack.count > 0) {
TSNode node = ts_nstack_pop(&stack);
@@ -302,7 +302,7 @@ static void walk_type_refs(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *s
}
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));
ts_nstack_push(&stack, ts_node_child(node, (uint32_t)i));
}
}
}
+195
View File
@@ -119,11 +119,196 @@ static uint32_t add_lexical_scope(WalkState *state, TSNode node, CBMLexicalScope
return scope->id;
}
/* ── #1912: Python parameter bindings held live by the walk ───────────────────
*
* A bare Python call `run()` cannot honestly resolve to a project function by
* short name when `run` is bound as a parameter of an enclosing def or lambda:
* the parameter shadows any module-level `run` for the whole body, so the match
* is fabricated by construction.
*
* Deciding that per call by ASCENDING the tree is the trap this replaces. Both
* ts_node_parent() (O(depth) per hop) and a walk-cursor ascent (O(1) per hop,
* O(depth) per call) are per-call costs, and every level of f(f(f(...))) is
* itself a bare call, so the file-wide cost is quadratic or worse -- exactly
* what CBMWalkScope's comment records for the state it replaced. Scanning the
* frame stack has the same shape.
*
* So the walk carries the answer instead: a name -> active-count map, pushed
* when a Python function or lambda scope opens and unwound when it closes.
* Lookup is O(1), which is what removes the need for a hop cap.
*
* A COUNT, not a flag: `def outer(run): def inner(run):` binds one name twice,
* and leaving the inner scope must not unbind the outer one.
*
* Every failure path answers "not bound", which can only ever cost a
* suppression -- never a true edge. */
static uint32_t py_param_hash(const char *s) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
h = (h ^ *p) * 16777619u;
}
return h ? h : 1u; /* 0 marks an empty slot */
}
static CBMParamSlot *py_param_slot(WalkState *state, const char *name, uint32_t hash) {
int mask = state->py_param_slot_capacity - 1;
int i = (int)(hash & (uint32_t)mask);
for (;;) {
CBMParamSlot *slot = &state->py_param_slots[i];
if (slot->hash == 0) {
return slot; /* free slot -- caller decides whether to claim it */
}
if (slot->hash == hash && strcmp(slot->name, name) == 0) {
return slot;
}
i = (i + 1) & mask;
}
}
static bool py_param_grow(WalkState *state) {
int new_capacity = state->py_param_slot_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_slot_capacity) {
return false;
}
CBMParamSlot *grown =
(CBMParamSlot *)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memset(grown, 0, (size_t)new_capacity * sizeof(*grown));
CBMParamSlot *old = state->py_param_slots;
int old_capacity = state->py_param_slot_capacity;
state->py_param_slots = grown;
state->py_param_slot_capacity = new_capacity;
for (int i = 0; i < old_capacity; i++) {
if (old[i].hash != 0) {
*py_param_slot(state, old[i].name, old[i].hash) = old[i];
}
}
return true;
}
static bool py_param_stack_reserve(WalkState *state) {
if (state->py_param_stack_count < state->py_param_stack_capacity) {
return true;
}
int new_capacity = state->py_param_stack_capacity * PAIR_LEN;
if (new_capacity <= state->py_param_stack_capacity) {
return false;
}
const char **grown =
(const char **)cbm_arena_alloc(state->arena, (size_t)new_capacity * sizeof(*grown));
if (!grown) {
return false;
}
memcpy(grown, state->py_param_stack, (size_t)state->py_param_stack_count * sizeof(*grown));
state->py_param_stack = grown;
state->py_param_stack_capacity = new_capacity;
return true;
}
/* Bind one parameter name for the lifetime of the frame currently on top. */
static void py_param_bind(WalkState *state, const char *name) {
if (state->py_param_tracking_failed || !name || !name[0]) {
return;
}
/* Grow before inserting: the probe below must always find a free slot, and
* a table above ~70% load degrades toward linear probing. */
if ((state->py_param_slot_used + 1) * 10 >= state->py_param_slot_capacity * 7) {
if (!py_param_grow(state)) {
state->py_param_tracking_failed = true;
return;
}
}
if (!py_param_stack_reserve(state)) {
state->py_param_tracking_failed = true;
return;
}
uint32_t hash = py_param_hash(name);
CBMParamSlot *slot = py_param_slot(state, name, hash);
if (slot->hash == 0) {
slot->hash = hash;
slot->name = name;
slot->count = 0;
state->py_param_slot_used++;
}
slot->count++;
state->py_param_stack[state->py_param_stack_count++] = name;
}
/* Unwind to a frame's entry height, undoing exactly what that frame bound. */
static void py_param_unwind_to(WalkState *state, int base) {
if (state->py_param_tracking_failed) {
return;
}
while (state->py_param_stack_count > base) {
const char *name = state->py_param_stack[--state->py_param_stack_count];
CBMParamSlot *slot = py_param_slot(state, name, py_param_hash(name));
if (slot->hash != 0 && slot->count > 0) {
slot->count--;
}
}
}
bool cbm_walk_python_param_is_bound(const WalkState *state, const char *name) {
if (!state || !name || !name[0] || state->py_param_tracking_failed ||
state->py_param_slot_used == 0) {
return false;
}
const CBMParamSlot *slot = py_param_slot((WalkState *)state, name, py_param_hash(name));
return slot->hash != 0 && slot->count > 0;
}
/* The identifier a Python parameter node binds. Handles the bare, typed,
* defaulted, keyword-only, *args and **kwargs shapes. */
static const char *py_parameter_name(CBMExtractCtx *ctx, TSNode param) {
if (ts_node_is_null(param)) {
return NULL;
}
if (strcmp(ts_node_type(param), "identifier") == 0) {
return cbm_node_text(ctx->arena, param, ctx->source);
}
TSNode name = ts_node_child_by_field_name(param, TS_FIELD("name"));
if (!ts_node_is_null(name) && strcmp(ts_node_type(name), "identifier") == 0) {
return cbm_node_text(ctx->arena, name, ctx->source);
}
/* `*args` / `**kwargs`, and any typed shape without a `name` field: the
* bound identifier is the first named child. */
TSNode first = ts_node_named_child(param, 0);
if (!ts_node_is_null(first) && strcmp(ts_node_type(first), "identifier") == 0) {
return cbm_node_text(ctx->arena, first, ctx->source);
}
return NULL;
}
/* Bind this node's parameters into the frame just pushed for it. Called after
* every push in push_boundary_scopes, so the top frame is this node's own and
* pop_expired_scopes unwinds them at exactly the right depth. */
static void py_bind_scope_parameters(CBMExtractCtx *ctx, TSNode node, WalkState *state) {
if (ctx->language != CBM_LANG_PYTHON || state->scope_top == 0) {
return;
}
const char *kind = ts_node_type(node);
if (strcmp(kind, "function_definition") != 0 && strcmp(kind, "lambda") != 0) {
return;
}
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t count = ts_node_named_child_count(params);
for (uint32_t i = 0; i < count; i++) {
py_param_bind(state, py_parameter_name(ctx, ts_node_named_child(params, i)));
}
}
static bool push_scope(WalkState *state, uint8_t kind, uint32_t depth, const char *qn) {
if (!ensure_scope_capacity(state)) {
return false;
}
CBMWalkScope *f = &state->scopes[state->scope_top];
f->prev_py_param_stack_count = state->py_param_stack_count;
f->kind = kind;
f->depth = depth;
f->qn = qn;
@@ -260,6 +445,7 @@ static void pop_expired_scopes(WalkState *state, uint32_t cur_depth) {
state->inside_import = f->prev_inside_import;
state->loop_depth = f->prev_loop_depth;
state->branch_depth = f->prev_branch_depth;
py_param_unwind_to(state, f->prev_py_param_stack_count);
}
}
@@ -2354,6 +2540,7 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
}
push_lexical_boundary(node, state, depth);
py_bind_scope_parameters(ctx, node, state);
push_call_scope(state, depth, invocation);
if (is_actual_import_boundary(ctx, node, spec) && !is_export_of_declaration(node)) {
push_scope(state, SCOPE_IMPORT, depth, NULL);
@@ -2387,6 +2574,14 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
state.scope_capacity = MAX_SCOPES;
state.lexical_scopes = state.inline_lexical_scopes;
state.lexical_scope_capacity = INLINE_LEXICAL_SCOPES;
state.py_param_slots = state.inline_py_param_slots;
state.py_param_slot_capacity = INLINE_PY_PARAM_SLOTS;
state.py_param_slot_used = 0;
memset(state.inline_py_param_slots, 0, sizeof(state.inline_py_param_slots));
state.py_param_stack = state.inline_py_param_stack;
state.py_param_stack_capacity = INLINE_PY_PARAM_STACK;
state.py_param_stack_count = 0;
state.py_param_tracking_failed = false;
state.usage_start_index = ctx->result->usages.count;
state.root_lexical_scope_id = add_lexical_scope(&state, ctx->root, CBM_LEXICAL_SCOPE_MODULE);
/* Base walk-state tuple (previously established by the first
+46
View File
@@ -18,6 +18,8 @@
#define INLINE_LEXICAL_SCOPES 64
#define INLINE_LEXICAL_BINDINGS 64
#define INLINE_PYTHON_DIRECTIVES 16
#define INLINE_PY_PARAM_SLOTS 64
#define INLINE_PY_PARAM_STACK 64
// ObjectScript type map: variable name → class name (for instance_method_call
// resolution). Stack-allocated, per-method scope. Overflow is silent (no crash).
@@ -51,6 +53,15 @@ typedef struct {
bool raw_call_emitted;
} CBMInvocationDescriptor;
/* One name bound as a function/lambda parameter by a scope currently OPEN on
* the walk stack. A count, not a flag: `def outer(run): def inner(run):` binds
* the same name twice and the inner pop must not unbind the outer. */
typedef struct {
const char *name;
uint32_t hash;
int count;
} CBMParamSlot;
typedef struct {
const char *qn;
uint32_t depth;
@@ -73,6 +84,9 @@ typedef struct {
bool prev_inside_import;
int prev_loop_depth;
int prev_branch_depth;
/* #1912: py_param_stack height on entry. Pop unwinds back to it, so a
* frame unbinds exactly the parameters it bound and nothing else. */
int prev_py_param_stack_count;
} CBMWalkScope;
typedef enum {
@@ -158,11 +172,43 @@ typedef struct {
CBMPythonDirective inline_python_directives[INLINE_PYTHON_DIRECTIVES];
int python_directive_capacity;
int python_directive_count;
/* #1912 -- Python bare-call shadowing. A name bound as a parameter by ANY
* enclosing function or lambda shadows every project function, so a bare
* `run()` under `def outer(run)` cannot honestly resolve by short name.
*
* Maintained as a live name->count map pushed and popped BY THE WALK, not
* recomputed per call. Walking ancestors (either ts_node_parent or a
* cursor) and scanning the frame stack are both O(depth) per call, and
* since every level of f(f(f(...))) is itself a bare call that is
* quadratic across the file -- the same trap CBMWalkScope records above.
* Lookup here is O(1), so no hop cap and no fail-open cap are needed.
*
* The other binding table (CBMLexicalBinding) cannot serve this: it is
* qsort-ed in cbm_finalize_lexical_usages AFTER the walk and its
* active_end stays 0 until then, so its binary search is invalid from
* handle_calls, which runs mid-walk and before handle_usages. */
CBMParamSlot *py_param_slots;
CBMParamSlot inline_py_param_slots[INLINE_PY_PARAM_SLOTS];
int py_param_slot_capacity;
int py_param_slot_used;
const char **py_param_stack;
const char *inline_py_param_stack[INLINE_PY_PARAM_STACK];
int py_param_stack_capacity;
int py_param_stack_count;
/* Allocation failure: stop tracking and answer "not bound" forever after,
* which can only cost a suppression, never a true edge. */
bool py_param_tracking_failed;
CBMLanguage language;
os_type_map_t os_type_map; // ObjectScript variable → type mapping
} WalkState;
/* #1912: is `name` bound as a parameter by a Python function or lambda scope
* currently open on the walk stack? O(1). Answers false on any failure, so a
* caller can only ever lose a suppression, never a true edge. */
bool cbm_walk_python_param_is_bound(const WalkState *state, const char *name);
// Per-node handler prototypes. Each is called once per node during the
// unified cursor walk, replacing the old recursive walk_* functions.
CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec,
+11
View File
@@ -1765,14 +1765,25 @@ static bool cbm_json_mcp_command_path_probe_safe(const char *command) {
#endif
#ifdef CBM_CLI_ENABLE_TEST_API
#ifdef _WIN32
static CBM_TLS int *g_mcp_command_path_probe_counter = NULL;
#endif
bool cbm_mcp_command_path_probe_safe_for_testing(const char *command, bool windows) {
return cbm_json_mcp_command_path_probe_safe_for_platform(command, windows);
}
void cbm_set_mcp_command_path_probe_counter_for_testing(int *counter) {
#ifdef _WIN32
g_mcp_command_path_probe_counter = counter;
#else
/* The command-path classifier (cbm_json_mcp_probe_command_path) is compiled
* for Windows only, so on POSIX there is no probe to count. The tests still
* install a counter on every platform and assert it stays at zero; a pointer
* stored here but never read is what Clang 23 rejects under -Werror
* (-Wunused-but-set-global). */
(void)counter;
#endif
}
#endif
+91 -26
View File
@@ -754,23 +754,30 @@ typedef struct {
} tool_annotation_def_t;
/* Tool annotations are deliberately explicit. All tools operate on the local
* repository/index domain, so none cross an open-world trust boundary. */
* repository/index domain, so none cross an open-world trust boundary.
*
* The ten pure query tools resolve their store through resolve_store(), whose
* query-only path is strictly non-mutating: a corrupt database is reported and
* left in place, never quarantined or rebuilt — quarantine/rebuild is reserved
* for write-side opens (index_repository, manage_adr writes). That is what
* makes readOnlyHint=true honest for them and lets plan-mode clients expose
* them (the "read-only analysis tools" surface described in #1100). */
static const tool_annotation_def_t TOOL_ANNOTATIONS[] = {
{"index_repository", false, false, true, false},
{"search_graph", false, true, true, false},
{"query_graph", false, true, true, false},
{"trace_path", false, true, true, false},
{"get_code_snippet", false, true, true, false},
{"search_graph", true, false, true, false},
{"query_graph", true, false, true, false},
{"trace_path", true, false, true, false},
{"get_code_snippet", true, false, true, false},
{"get_file_outline", false, true, true, false},
{"get_graph_schema", false, true, true, false},
{"get_graph_schema", true, false, true, false},
{"compare_graphs", true, false, true, false},
{"get_architecture", false, true, true, false},
{"search_code", false, true, true, false},
{"get_architecture", true, false, true, false},
{"search_code", true, false, true, false},
{"list_projects", true, false, true, false},
{"delete_project", false, true, true, false},
{"index_status", false, true, true, false},
{"check_index_coverage", false, true, true, false},
{"detect_changes", false, true, true, false},
{"index_status", true, false, true, false},
{"check_index_coverage", true, false, true, false},
{"detect_changes", true, false, true, false},
{"manage_adr", false, true, false, false},
{"ingest_traces", false, false, false, false},
};
@@ -1624,6 +1631,12 @@ struct cbm_mcp_server {
bool owns_store; /* true if we opened the store */
char *current_project; /* which project store is open for (heap) */
time_t store_last_used; /* last time resolve_store was called for a named project */
/* Set by a query-only resolve that hit a confirmed-corrupt database and
* left it in place (no quarantine, no rebuild). Error builders read this
* so the tool reply names the corruption instead of a misleading
* "project not found". */
bool readonly_resolve_hit_corrupt;
char readonly_corrupt_project[CBM_SZ_1K];
/* Session + auto-index state */
char session_root[CBM_SZ_1K]; /* detected project root path */
@@ -2217,14 +2230,21 @@ typedef enum {
STORE_RECOVERY_NONE,
STORE_RECOVERY_BUSY,
STORE_RECOVERY_TRY_GUARD_UNAVAILABLE,
/* Query-only resolve: a confirmed-corrupt database was reported and left
* in place. Not an error state of the recovery machinery — the caller is
* expected to answer with the corruption, not retry. */
STORE_RECOVERY_CORRUPT,
} store_recovery_status_t;
static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *project,
bool mutation_already_held, bool nonblocking_recovery,
store_recovery_status_t *recovery_status) {
store_recovery_status_t *recovery_status,
bool allow_autorecovery) {
if (recovery_status) {
*recovery_status = STORE_RECOVERY_NONE;
}
srv->readonly_resolve_hit_corrupt = false;
srv->readonly_corrupt_project[0] = '\0';
if (!project) {
return NULL; /* project is required — no implicit fallback */
}
@@ -2254,6 +2274,38 @@ static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *pr
project_db_path(project, path, sizeof(path));
srv->store = path[0] ? cbm_store_open_path_query(path) : NULL;
if (srv->store) {
/* Query-only resolve: classify a failed integrity check without any
* mutation — no lease, no quarantine. A corrupt database is reported
* and left in place for a write-side open (index_repository) to
* quarantine and rebuild; that is what keeps the ten read-only
* tools' readOnlyHint=true honest. A transient failure (lock, IO, or
* the shallow check's own prepare-error-as-corrupt blind spot) is
* answered as busy and retried by the next resolve. */
if (!allow_autorecovery && !cbm_store_check_integrity(srv->store)) {
cbm_store_close(srv->store);
srv->store = NULL;
srv->store = cbm_store_open_path_query(path);
cbm_integrity_verdict_t verdict = srv->store
? cbm_store_check_integrity_verdict(srv->store)
: CBM_INTEGRITY_TRANSIENT;
if (srv->store) {
cbm_store_close(srv->store);
srv->store = NULL;
}
if (verdict == CBM_INTEGRITY_CORRUPT) {
srv->readonly_resolve_hit_corrupt = true;
snprintf(srv->readonly_corrupt_project, sizeof(srv->readonly_corrupt_project), "%s",
project);
cbm_log_warn("store.corrupt_readonly", "project", project, "path", path, "action",
"left in place for a write-side rebuild");
if (recovery_status) {
*recovery_status = STORE_RECOVERY_CORRUPT;
}
} else if (recovery_status) {
*recovery_status = STORE_RECOVERY_BUSY;
}
return NULL;
}
/* Check DB integrity — back up (never silently delete) a corrupt DB */
if (!cbm_store_check_integrity(srv->store)) {
cbm_store_close(srv->store);
@@ -2358,7 +2410,8 @@ static cbm_store_t *resolve_store_internal(cbm_mcp_server_t *srv, const char *pr
}
static cbm_store_t *resolve_store(cbm_mcp_server_t *srv, const char *project) {
return resolve_store_internal(srv, project, false, false, NULL);
/* Query-only callers (every read tool): strictly non-mutating resolve. */
return resolve_store_internal(srv, project, false, false, NULL, false);
}
/* Forward decl — definition lives below alongside list_projects. */
@@ -2489,16 +2542,28 @@ static char *build_no_store_error(const char *project) {
: build_missing_project_error();
}
/* Same contract as build_no_store_error, but when the query-only resolve
* confirmed a corrupt database and left it in place, name that instead of a
* misleading "project not found". */
static char *build_no_store_error_checked(cbm_mcp_server_t *srv, const char *project) {
if (srv->readonly_resolve_hit_corrupt && project &&
strcmp(project, srv->readonly_corrupt_project) == 0) {
return build_project_list_error(
"project store is corrupt (left untouched); run index_repository to rebuild it");
}
return build_no_store_error(project);
}
/* Bail with the right error when no store is available. */
#define REQUIRE_STORE(store, project) \
do { \
if (!(store)) { \
char *_err = build_no_store_error(project); \
char *_res = cbm_mcp_text_result(_err, true); \
free(_err); \
free(project); \
return _res; \
} \
#define REQUIRE_STORE(store, project) \
do { \
if (!(store)) { \
char *_err = build_no_store_error_checked(srv, project); \
char *_res = cbm_mcp_text_result(_err, true); \
free(_err); \
free(project); \
return _res; \
} \
} while (0)
static bool project_has_adr(cbm_store_t *store, const char *project, const char *root_path) {
@@ -11648,7 +11713,7 @@ static char *handle_detect_changes(cbm_mcp_server_t *srv, const char *args) {
char *root_path = get_project_root(srv, project);
if (!root_path) {
char *err = build_no_store_error(project);
char *err = build_no_store_error_checked(srv, project);
char *res = cbm_mcp_text_result(err, true);
free(err);
free(project);
@@ -12418,7 +12483,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
* the UI are visible to each other (#256). */
store_recovery_status_t recovery_status = STORE_RECOVERY_NONE;
cbm_store_t *resolved =
resolve_store_internal(srv, project, mutation_held, !write_request, &recovery_status);
resolve_store_internal(srv, project, mutation_held, !write_request, &recovery_status, true);
if (!resolved) {
char *res = NULL;
if (recovery_status == STORE_RECOVERY_BUSY) {
@@ -12427,7 +12492,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
res =
cbm_mcp_text_result("project recovery requires a nonblocking mutation guard", true);
} else {
char *err = build_no_store_error(project);
char *err = build_no_store_error_checked(srv, project);
res = cbm_mcp_text_result(err, true);
free(err);
}
@@ -12479,7 +12544,7 @@ static char *handle_manage_adr(cbm_mcp_server_t *srv, const char *args) {
invalidate_cached_store(srv);
resolved = NULL;
store = NULL;
resolved = resolve_store_internal(srv, project, true, false, NULL);
resolved = resolve_store_internal(srv, project, true, false, NULL, true);
}
if (resolved) {
store = open_adr_store_for_write(srv, resolved, &owned_rw);
+9 -1
View File
@@ -627,8 +627,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
/* Bare-call local-binding suppression. A member call has a receiver the
* guard above can reason about; a bare `run()` has none, so that guard
* cannot see this class at all. Python-only today because the extraction
* flag is set only for Python — this gate MUST match pass_parallel.c's
* exactly, for the same divergence reason noted above. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
+6 -1
View File
@@ -2492,8 +2492,13 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
/* Bare-call local-binding suppression — see the note in pass_calls.c.
* This gate MUST stay identical to the one there. */
bool suppress_weak_local_binding = lang == CBM_LANG_PYTHON;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy);
/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
+12
View File
@@ -280,6 +280,18 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy);
/* Bare-call counterpart of the guard above. True when a resolved BARE call edge
* binds a callee that is shadowed by an enclosing parameter, and the match came
* from a weak short-name strategy — so the edge is fabricated by construction
* (`def f(run): run()` must not bind an unrelated `SatoriLive.run`). Shares the
* member guard's drop-list, so lsp_* / import / same-module matches are kept.
* Deliberately keyed on the SCOPE FACT, not on the callee's spelling. The
* language set lives at the call sites (pass_calls.c / pass_parallel.c) and must
* be identical in both, or the sequential and parallel resolvers diverge.
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy);
/* #725: drop a suffix_match CALLS edge when the caller language and the
* target file's language disagree. unique_name (candidates == 1) is #1572
* and is left alone; same_module / import_map / lsp_* are kept. JS/TS/TSX
+47 -10
View File
@@ -448,22 +448,59 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* per-language decision made at the call sites in pass_calls.c and
* pass_parallel.c, which MUST stay in lockstep — a gate added to only one of
* them diverges the sequential and parallel resolvers. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy) {
if (!enabled || !is_method || !strategy || !strategy[0]) {
/* The weak short-name strategies that actually reach the call-resolution
* guards: the registry's suffix_match / unique_name and the parallel
* field_type_hint. "fuzzy" is listed as defensive insurance only —
* cbm_registry_fuzzy_resolve is not wired into the sequential/parallel resolvers
* today, so it never reaches these helpers, but naming it keeps a future wiring
* from silently reintroducing the noise. Everything else — same_module /
* import_map / import_map_suffix / qualified_suffix / callee_suffix /
* service_pattern / lsp_* — is a receiver- or import-aware match and is KEPT.
*
* Shared by BOTH weak-call guards below so the drop-list exists exactly once: a
* list that drifted between the member guard and the local-binding guard would
* make the two disagree about what "weak" means. */
static bool weak_short_name_strategy(const char *strategy) {
if (!strategy || !strategy[0]) {
return false;
}
/* Weak short-name strategies that actually reach the call-resolution guards:
* the registry's suffix_match / unique_name and the parallel field_type_hint.
* "fuzzy" is listed as defensive insurance only — cbm_registry_fuzzy_resolve
* is not wired into the sequential/parallel resolvers today, so it never
* reaches this helper, but naming it keeps a future wiring from silently
* reintroducing the noise. Everything else — same_module / import_map /
* import_map_suffix / qualified_suffix / callee_suffix / service_pattern /
* lsp_* — is a receiver- or import-aware match and is KEPT. */
return strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "unique_name") == 0 ||
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *strategy) {
if (!enabled || !is_method) {
return false;
}
return weak_short_name_strategy(strategy);
}
/* Bare-call counterpart of the member guard above. A Python call `foo()` whose
* callee identifier is bound as a parameter of an enclosing scope cannot be the
* module-level `foo`: the parameter shadows it for the whole body. Binding such
* a call to a project Function/Method by a weak short-name strategy fabricates
* the edge by construction (`def _run_with_heavy_slot(run): run()` ->
* SatoriLive.run).
*
* This is deliberately NOT keyed on the callee's spelling. A list of
* "generic-looking" names (get / run / execute) asserts that certain spellings
* are usually noise, which is a claim about corpus fashion rather than about
* what the resolver knew — and it ages invisibly, because nothing fails when the
* distribution shifts, the graph just quietly loses different edges. A parameter
* binding is a fact about THIS file's scope, decidable outright.
*
* `enabled` is the caller's per-language gate, kept out of the helper for the
* same reason as the member guard: the two call sites in pass_calls.c and
* pass_parallel.c MUST enumerate the identical language set, or the sequential
* and parallel resolvers diverge. Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy) {
if (!enabled || !callee_is_locally_bound) {
return false;
}
return weak_short_name_strategy(strategy);
}
static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
+8 -2
View File
@@ -70,6 +70,10 @@
/* ── Constants ────────────────────────────────────────────────── */
#ifndef CBM_VERSION
#define CBM_VERSION "dev"
#endif
/* Max JSON-RPC request body size (1 MB) — transport enforces the same cap. */
#define MAX_BODY_SIZE CBM_HTTP_MAX_BODY
@@ -147,8 +151,10 @@ static void handle_ui_config(cbm_http_conn_t *c, const cbm_http_req_t *req) {
* audit forbids hardcoded external URLs in graph-ui source (external
* targets must come from an auditable backend response, same pattern as
* the /api/repo-info deep-links). */
cbm_http_replyf(c, 200, g_cors_json, "{\"lang\":\"%s\",\"upstream_issues_url\":\"%s\"}",
lang_buf, "https://github.com/DeusData/codebase-memory-mcp/issues/new");
cbm_http_replyf(c, 200, g_cors_json,
"{\"lang\":\"%s\",\"version\":\"%s\",\"upstream_issues_url\":\"%s\"}",
lang_buf, CBM_VERSION,
"https://github.com/DeusData/codebase-memory-mcp/issues/new");
}
/* ── Server state ─────────────────────────────────────────────── */
+12 -1
View File
@@ -6638,8 +6638,19 @@ TEST(cli_agent_client_registry_routes_plan_install_and_uninstall) {
char *plan = cbm_build_install_plan_json(tmpdir, binary_path);
yyjson_doc *plan_doc = plan ? yyjson_read(plan, strlen(plan), 0) : NULL;
yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL;
/* Neither of these agents has a plugin directory, so a planned path under
* one would be invented. Name the two directories rather than searching the
* whole plan for "/plugins/": OpenCode does ship a real plugin file, and
* agent detection finds a command in /usr/local/bin or /opt/homebrew/bin
* whatever HOME and PATH say, so a blanket search passes or fails according
* to what the developer happens to have installed. */
char qoder_plugins[700];
char pi_plugins[700];
snprintf(qoder_plugins, sizeof(qoder_plugins), "%s/plugins/", qoder_dir);
snprintf(pi_plugins, sizeof(pi_plugins), "%s/plugins/", pi_dir);
bool plan_ok =
plan && !strstr(plan, "/plugins/") && !strstr(plan, "plugin_files") &&
plan && !strstr(plan, qoder_plugins) && !strstr(plan, pi_plugins) &&
!strstr(plan, "plugin_files") &&
test_json_string_array_contains(plan_root, "config_files_planned", qoder_settings) &&
test_json_string_array_contains(plan_root, "config_files_planned", amazon_config) &&
test_json_string_array_contains(plan_root, "config_files_planned", roo_config) &&
+313 -36
View File
@@ -7,6 +7,7 @@
*/
#include "test_framework.h"
#include "cbm.h"
#include "foundation/constants.h" /* CBM_SZ_* */
#include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */
#include "../src/foundation/compat_fs.h"
#include <time.h>
@@ -166,6 +167,57 @@ TEST(extract_ts_factory_object_methods_issue341) {
PASS();
}
/* #2010, split out of #1997: AST traversal stacks were allocated from
* result->arena, which the parallel pass holds for every file until the whole
* result cache is freed, so a one-file scratch structure was retained for the
* length of the index. cbm_extract_channels runs for every file and dispatches
* TypeScript to extract_channels_js, whose two walks take a 4096-entry TSNode
* stack each, and the ES import walk takes a 512-entry one:
* 2 * 4096 * 32 + 512 * 32 = 278528 bytes charged to the arena of a one-line
* file.
*
* The bound is derived, not tuned. Measured on this source, total_alloc was
* 365984 before the scratch arena and is 87456 after, exactly that difference.
* Of the 87456 that remain, 7680 is the defs item array at GROW_ARRAY's
* starting capacity of 32 times sizeof(CBMDefinition) 240, and the other 79776
* is everything else this file's extraction interns; none of it is traversal
* scratch. So the bound sits above 87456 with room and a factor of four below
* 365984.
*
* It is a byte budget, not a proof of lifetime; that is
* extract_traversal_stacks_come_from_ctx_scratch_issue2010 in test_mem.c. */
TEST(traversal_stack_not_in_result_arena_issue2010) {
CBMFileResult *r = extract("export const x = 1;\n", CBM_LANG_TYPESCRIPT, "t", "a.ts");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_TRUE(has_def_any(r, "x"));
/* Read the field rather than cbm_arena_total(): this file sees
* internal/cbm/arena.h, which declares a subset of the API. test_mem.c
* includes foundation/arena.h ahead of cbm.h and can call the accessor. */
ASSERT_LT(r->arena.total_alloc, (size_t)CBM_SZ_128 * CBM_SZ_1K);
cbm_free_result(r);
PASS();
}
/* Not a scratch test. The C and C++ preprocessed second pass builds its own
* extraction context (pp_ctx in cbm_extract_file_ex), and that context carries
* ctx->scratch so every context in the file is uniform, but nothing reads it
* there: pp_ctx reaches only cbm_extract_unified and cbm_run_c_lsp, and neither
* extract_unified.c nor anything under internal/cbm/lsp/ includes
* extract_node_stack.h, so no traversal stack is built on that path today.
* This guards the macro-expansion path itself, which had no assertion on a call
* that exists only after expansion. */
TEST(extract_c_macro_hidden_call_survives_preprocessed_pass_issue2010) {
CBMFileResult *r = extract("void target(void) {}\n"
"#define INVOKE() target()\n"
"void caller(void) { INVOKE(); }\n",
CBM_LANG_C, "t", "macro_call.c");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(has_call(r, "target"));
cbm_free_result(r);
PASS();
}
/* --- C/C++ preprocessor macros become Macro nodes (#375) --- */
TEST(extract_c_macros_issue375) {
CBMFileResult *r = extract("#define SIMPLE_MACRO 1\n"
@@ -1225,19 +1277,18 @@ TEST(form_procedure) {
/* --- Oracle PL/SQL --- */
TEST(plsql_package_and_call) {
const char *src =
"CREATE OR REPLACE PACKAGE BODY emp_pkg AS\n"
" FUNCTION hire(p_name VARCHAR2) RETURN NUMBER IS\n"
" v_sal NUMBER;\n"
" BEGIN\n"
" v_sal := util_pkg.calc_salary(p_name);\n"
" IF v_sal > 0 THEN\n"
" RETURN v_sal;\n"
" END IF;\n"
" RAISE no_data_found;\n"
" END;\n"
"END emp_pkg;\n"
"/\n";
const char *src = "CREATE OR REPLACE PACKAGE BODY emp_pkg AS\n"
" FUNCTION hire(p_name VARCHAR2) RETURN NUMBER IS\n"
" v_sal NUMBER;\n"
" BEGIN\n"
" v_sal := util_pkg.calc_salary(p_name);\n"
" IF v_sal > 0 THEN\n"
" RETURN v_sal;\n"
" END IF;\n"
" RAISE no_data_found;\n"
" END;\n"
"END emp_pkg;\n"
"/\n";
CBMFileResult *r = extract(src, CBM_LANG_PLSQL, "t", "emp_pkg.pkb");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
@@ -2220,9 +2271,10 @@ TEST(dbt_source_and_two_arg_ref) {
/* Both dbt builtins name the relation in their LAST string argument:
* source('group','table') -> table, and the two-argument
* ref('package','model') form -> model. */
CBMFileResult *r = extract("SELECT * FROM {{ source('raw', 'customers') }}\n"
"UNION ALL SELECT * FROM {{ ref('analytics', 'legacy_customers') }}\n",
CBM_LANG_SQL, "t", "models/stg_customers.sql");
CBMFileResult *r =
extract("SELECT * FROM {{ source('raw', 'customers') }}\n"
"UNION ALL SELECT * FROM {{ ref('analytics', 'legacy_customers') }}\n",
CBM_LANG_SQL, "t", "models/stg_customers.sql");
ASSERT_NOT_NULL(r);
ASSERT(has_def(r, "Model", "stg_customers"));
ASSERT(has_usage(r, "customers"));
@@ -3294,8 +3346,8 @@ TEST(vue_embedded_structure_negative_controls_issue1410) {
}
TEST(vue_embedded_structure_host_controls_issue1410) {
CBMFileResult *plain = extract("function plainTs(): void { target(); }\n", CBM_LANG_TYPESCRIPT,
"t", "plain.ts");
CBMFileResult *plain =
extract("function plainTs(): void { target(); }\n", CBM_LANG_TYPESCRIPT, "t", "plain.ts");
ASSERT_NOT_NULL(plain);
ASSERT_FALSE(plain->has_error);
ASSERT_EQ(count_defs_named(plain, "Function", "plainTs"), 1);
@@ -3753,11 +3805,11 @@ TEST(extract_java_method_annotations_issue382) {
/* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */
TEST(arkts_component_struct) {
CBMFileResult *r = extract(
"@Entry\n@Component\nstruct Index {\n @State message: string = 'Hello'\n\n"
" build() {\n Column() {\n Text(this.message).fontSize(20)\n }\n"
" .width('100%')\n }\n}\n",
CBM_LANG_ARKTS, "t", "Index.ets");
CBMFileResult *r =
extract("@Entry\n@Component\nstruct Index {\n @State message: string = 'Hello'\n\n"
" build() {\n Column() {\n Text(this.message).fontSize(20)\n }\n"
" .width('100%')\n }\n}\n",
CBM_LANG_ARKTS, "t", "Index.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Struct", "Index"));
@@ -3788,12 +3840,12 @@ TEST(arkts_exported_struct_decorators) {
}
TEST(arkts_member_decorators) {
CBMFileResult *r = extract(
"@Component\nstruct S {\n @State a: number = 0\n @Prop b: string\n"
" @Link c: boolean\n @Provide('k') d: string = ''\n @Consume('k') e: string\n"
" @StorageLink('s') f: number = 1\n @State @Watch('onW') g: boolean = false\n\n"
" build() {\n }\n}\n",
CBM_LANG_ARKTS, "t", "S.ets");
CBMFileResult *r =
extract("@Component\nstruct S {\n @State a: number = 0\n @Prop b: string\n"
" @Link c: boolean\n @Provide('k') d: string = ''\n @Consume('k') e: string\n"
" @StorageLink('s') f: number = 1\n @State @Watch('onW') g: boolean = false\n\n"
" build() {\n }\n}\n",
CBM_LANG_ARKTS, "t", "S.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(decorators_contain(find_def_by_name(r, "a"), "State"));
@@ -3833,11 +3885,11 @@ TEST(arkts_no_phantom_builtin_defs) {
}
TEST(arkts_builder_extend_styles) {
CBMFileResult *r = extract(
"@Builder\nfunction card(t: string) {\n Column() {\n Text(t)\n }\n}\n\n"
"@Extend(Text)\nfunction fancy(size: number) {\n .fontSize(size)\n}\n\n"
"@Styles\nfunction pressed() {\n .backgroundColor('#eee')\n}\n",
CBM_LANG_ARKTS, "t", "b.ets");
CBMFileResult *r =
extract("@Builder\nfunction card(t: string) {\n Column() {\n Text(t)\n }\n}\n\n"
"@Extend(Text)\nfunction fancy(size: number) {\n .fontSize(size)\n}\n\n"
"@Styles\nfunction pressed() {\n .backgroundColor('#eee')\n}\n",
CBM_LANG_ARKTS, "t", "b.ets");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT(has_def(r, "Function", "card"));
@@ -4027,6 +4079,41 @@ static const CBMCall *find_call_by_callee(CBMFileResult *r, const char *callee)
return NULL;
}
/* #1892: the Swift grammar declares no "arguments" field, so the generic field
* lookup read nothing and every Swift call lost its arguments. Without the URL
* the service-pattern table cannot raise an HTTP_CALLS edge or a Route node,
* even though Alamofire/Moya/URLSession are already listed in it. */
TEST(swift_call_string_arg_issue1892) {
CBMFileResult *r =
extract("func listWidgets() { AF.request(\"https://example.com/api/v1/widgets\") }\n",
CBM_LANG_SWIFT, "t", "Client.swift");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMCall *c = find_call_by_callee(r, "AF.request");
ASSERT_NOT_NULL(c);
ASSERT_NOT_NULL(c->first_string_arg);
ASSERT_STR_EQ(c->first_string_arg, "https://example.com/api/v1/widgets");
cbm_free_result(r);
PASS();
}
/* Swift labels its arguments, and each one sits in a value_argument node that
* leads with the label. Reading the first child alone would return `with`
* rather than the path. */
TEST(swift_labeled_call_string_arg_issue1892) {
CBMFileResult *r =
extract("func fetch() { URLSession.shared.dataTask(with: \"/api/v1/widgets/1\") }\n",
CBM_LANG_SWIFT, "t", "Fetch.swift");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
const CBMCall *c = find_call_by_callee(r, "URLSession.shared.dataTask");
ASSERT_NOT_NULL(c);
ASSERT_NOT_NULL(c->first_string_arg);
ASSERT_STR_EQ(c->first_string_arg, "/api/v1/widgets/1");
cbm_free_result(r);
PASS();
}
/* Issue #1009: URL-builder helper pattern — a function returning a URL-shaped
* literal, consumed as client(buildPath(id)). The builder's URL is recorded in
* the per-file constant map and resolved at the call site, for both return
@@ -4954,6 +5041,190 @@ TEST(extract_python_member_call_flags_is_method) {
PASS();
}
/* Python bare-call local-binding flag (the bare-call counterpart of the
* receiver flag above). Pins BOTH directions: a callee shadowed by a parameter
* of an enclosing scope IS flagged so the resolver can suppress a weak
* short-name match, while an unshadowed callee — a genuine module-level
* function, an imported name, or a nested `def` — is NOT, so its true edge
* survives. Every parameter binding form the grammar produces is covered, since
* a form the extractor silently missed would leave that shape unguarded. */
TEST(extract_python_bare_call_flags_locally_bound_callee) {
CBMFileResult *r = extract("from pkg import helper\n"
"\n"
"def outer(run, *rest, timeout=5, label: str = 'x', **opts):\n"
" def inner():\n"
" return run()\n"
" rest()\n"
" timeout()\n"
" label()\n"
" opts()\n"
" module_level()\n"
" helper()\n"
" return inner()\n"
"\n"
"def typed(cb: Callable):\n"
" return cb()\n"
"\n"
"apply_it = lambda fn: fn()\n",
CBM_LANG_PYTHON, "t", "x.py");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
/* callee name -> (expected flag, seen count) */
struct {
const char *callee;
bool expect_bound;
int seen;
} cases[] = {
{"run", true, 0}, /* closure over an ENCLOSING function's parameter */
{"rest", true, 0}, /* *args -> list_splat_pattern */
{"timeout", true, 0}, /* default_parameter */
{"label", true, 0}, /* typed_default_parameter (keyword-only) */
{"opts", true, 0}, /* **kwargs -> dictionary_splat_pattern */
{"cb", true, 0}, /* typed_parameter, no default */
{"fn", true, 0}, /* lambda parameter */
{"module_level", false, 0}, /* unbound: the true cross-file edge */
{"helper", false, 0}, /* imported name, not a parameter */
{"inner", false, 0}, /* nested def: a real target, keep the edge */
};
const int case_count = (int)(sizeof(cases) / sizeof(cases[0]));
for (int i = 0; i < r->calls.count; i++) {
const char *cn = r->calls.items[i].callee_name;
if (!cn) {
continue;
}
for (int c = 0; c < case_count; c++) {
if (strcmp(cn, cases[c].callee) != 0) {
continue;
}
cases[c].seen++;
if (r->calls.items[i].callee_is_locally_bound != cases[c].expect_bound) {
printf(" bare-call flag mismatch for %s(): got %d, expected %d\n", cases[c].callee,
r->calls.items[i].callee_is_locally_bound ? 1 : 0,
cases[c].expect_bound ? 1 : 0);
}
ASSERT_EQ(r->calls.items[i].callee_is_locally_bound, cases[c].expect_bound);
}
}
/* Each shape must appear exactly once, so a missed extraction cannot let the
* loop above pass vacuously. */
for (int c = 0; c < case_count; c++) {
if (cases[c].seen != 1) {
printf(" bare call %s() extracted %d times, expected 1\n", cases[c].callee,
cases[c].seen);
}
ASSERT_EQ(cases[c].seen, 1);
}
cbm_free_result(r);
PASS();
}
/* The bare-call flag is DEPTH-INDEPENDENT, and the binding is UNWOUND when its
* scope closes.
*
* Both properties come from the same design decision. The answer is carried by
* the unified walk -- parameters are bound when a def or lambda scope opens and
* unwound when it closes -- rather than recomputed per call by ascending the
* tree. An ascending walk is O(depth) per call, and every level of f(f(f(...)))
* is itself a bare call, so it is quadratic in a file's nesting depth; that hung
* stack_overflow_b's 30,000-deep fixture rather than merely slowing it. An
* earlier cut capped the ascent at 64 ancestors and FAILED OPEN past it, which
* silently stopped suppressing on deep-but-ordinary code.
*
* Pinned deterministically rather than by wall clock -- a timing assertion would
* be a lottery, not a gate. The depth case fails if a cap is reintroduced; the
* unwind case fails if a frame's bindings outlive its scope. */
TEST(extract_python_bare_call_flag_is_depth_independent) {
/* Shallow: return_statement / block / function_definition — 3 ancestors. */
CBMFileResult *shallow = extract("def shallow(handler):\n"
" return handler()\n",
CBM_LANG_PYTHON, "t", "s.py");
ASSERT_NOT_NULL(shallow);
ASSERT_FALSE(shallow->has_error);
int shallow_seen = 0;
for (int i = 0; i < shallow->calls.count; i++) {
const char *cn = shallow->calls.items[i].callee_name;
if (cn && strcmp(cn, "handler") == 0) {
shallow_seen++;
ASSERT_TRUE(shallow->calls.items[i].callee_is_locally_bound);
}
}
ASSERT_EQ(shallow_seen, 1);
cbm_free_result(shallow);
/* Deep: 200 parenthesized_expression ancestors separate the SAME call from
* its enclosing def. The parameter still shadows it, so it stays flagged --
* depth changes nothing. This is the case a 64-ancestor cap got wrong. */
const int PARENS = 200;
size_t sz = (size_t)PARENS * 2 + 128;
char *src = malloc(sz);
ASSERT_NOT_NULL(src);
char *w = src;
w += snprintf(w, sz, "def deep(handler):\n return ");
memset(w, '(', (size_t)PARENS);
w += PARENS;
w += snprintf(w, sz - (size_t)(w - src), "handler()");
memset(w, ')', (size_t)PARENS);
w += PARENS;
snprintf(w, sz - (size_t)(w - src), "\n");
CBMFileResult *deep = extract(src, CBM_LANG_PYTHON, "t", "d.py");
ASSERT_NOT_NULL(deep);
ASSERT_FALSE(deep->has_error);
int deep_seen = 0;
for (int i = 0; i < deep->calls.count; i++) {
const char *cn = deep->calls.items[i].callee_name;
if (cn && strcmp(cn, "handler") == 0) {
deep_seen++;
ASSERT_TRUE(deep->calls.items[i].callee_is_locally_bound);
}
}
ASSERT_EQ(deep_seen, 1);
cbm_free_result(deep);
free(src);
/* Unwind: `handler` is a parameter of shadowed() and a module-level function
* of the same name. The call INSIDE shadowed() is flagged; the call in
* sibling(), after that scope closed, must NOT be — it really does resolve
* to the module-level def. A binding that outlived its frame would flag it
* and destroy a true edge, which is the one direction this guard must never
* fail in. Nested same-name defs also pin the count: leaving the inner scope
* must not unbind the outer one. */
CBMFileResult *unwound = extract("def handler():\n"
" return 1\n"
"\n"
"def shadowed(handler):\n"
" def inner(handler):\n"
" return handler()\n"
" return inner(handler) or handler()\n"
"\n"
"def sibling():\n"
" return handler()\n",
CBM_LANG_PYTHON, "t", "u.py");
ASSERT_NOT_NULL(unwound);
ASSERT_FALSE(unwound->has_error);
int flagged = 0;
int unflagged = 0;
for (int i = 0; i < unwound->calls.count; i++) {
const char *cn = unwound->calls.items[i].callee_name;
if (!cn || strcmp(cn, "handler") != 0) {
continue;
}
if (unwound->calls.items[i].callee_is_locally_bound) {
flagged++;
} else {
unflagged++;
}
}
/* Two shadowed calls (inner body, and shadowed()'s own tail) and exactly one
* unshadowed call in sibling(). */
ASSERT_EQ(flagged, 2);
ASSERT_EQ(unflagged, 1);
cbm_free_result(unwound);
PASS();
}
/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
* A member call x.foo() with a non-this/super receiver is flagged is_method so
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
@@ -5362,8 +5633,8 @@ TEST(extract_c_test_dir_marks_is_test_issue1294) {
* not be (#1294). */
TEST(extract_python_method_test_dir_marks_is_test_issue1294) {
const char *src = "class Foo:\n"
" def helper(self):\n"
" pass\n";
" def helper(self):\n"
" pass\n";
/* Python's LSP layer injects synthetic builtin stub Methods (str.upper,
* dict.get, ...) into defs.items alongside real ones (py_builtins.c), so
@@ -6623,6 +6894,8 @@ SUITE(extraction) {
RUN_TEST(extract_perl_method_call_flags_is_method);
RUN_TEST(extract_flag_exempt_method_call_not_flagged_is_method);
RUN_TEST(extract_python_member_call_flags_is_method);
RUN_TEST(extract_python_bare_call_flags_locally_bound_callee);
RUN_TEST(extract_python_bare_call_flag_is_depth_independent);
RUN_TEST(extract_ts_member_call_flags_is_method);
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
RUN_TEST(extract_js_member_call_flags_is_method);
@@ -6665,6 +6938,8 @@ SUITE(extraction) {
RUN_TEST(extract_r_box_use_imports_issue218);
RUN_TEST(extract_r_dollar_call_issue219);
RUN_TEST(extract_ts_factory_object_methods_issue341);
RUN_TEST(traversal_stack_not_in_result_arena_issue2010);
RUN_TEST(extract_c_macro_hidden_call_survives_preprocessed_pass_issue2010);
RUN_TEST(extract_c_macros_issue375);
RUN_TEST(extract_cpp_macros_issue375);
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
@@ -6782,6 +7057,8 @@ SUITE(extraction) {
RUN_TEST(swift_constructor_call);
RUN_TEST(swift_chained_call);
RUN_TEST(swift_force_unwrap_scanner_shift);
RUN_TEST(swift_call_string_arg_issue1892);
RUN_TEST(swift_labeled_call_string_arg_issue1892);
RUN_TEST(objc_interface);
RUN_TEST(objc_implementation);
RUN_TEST(dart_top_level_function);
+21
View File
@@ -33,6 +33,9 @@
#include <stdatomic.h>
#include <stdlib.h>
#include <string.h>
#ifndef CBM_VERSION
#define CBM_VERSION "dev"
#endif
#ifndef _WIN32
#include <sys/stat.h>
#endif
@@ -1573,6 +1576,23 @@ TEST(ui_server_ui_config_detects_zh_accept_language) {
PASS();
}
TEST(ui_server_ui_config_includes_serving_version_issue1820) {
th_server_t ts;
ASSERT_EQ(th_server_start(&ts), 0);
char resp[4096];
int n = th_http(cbm_http_server_port(ts.srv), "GET /api/ui-config HTTP/1.1\r\n\r\n", resp,
sizeof(resp));
ASSERT_TRUE(n > 0);
ASSERT_EQ(th_status(resp), 200);
char expected_version[128];
snprintf(expected_version, sizeof(expected_version), "\"version\":\"%s\"", CBM_VERSION);
ASSERT_NOT_NULL(strstr(resp, expected_version));
th_server_stop(&ts);
PASS();
}
TEST(ui_server_ui_config_prefers_config_lang) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_httpd_cfg_XXXXXX");
@@ -2397,6 +2417,7 @@ SUITE(httpd) {
RUN_TEST(ui_server_delete_project_invalid_name_keeps_watch);
RUN_TEST(ui_server_delete_project_unlink_failure_keeps_watch);
RUN_TEST(ui_server_ui_config_detects_zh_accept_language);
RUN_TEST(ui_server_ui_config_includes_serving_version_issue1820);
RUN_TEST(ui_server_ui_config_prefers_config_lang);
RUN_TEST(ui_server_slow_request_hits_deadline);
RUN_TEST(ui_server_access_log_redacts_query);
+58 -39
View File
@@ -934,23 +934,27 @@ TEST(mcp_tools_have_behavior_annotations) {
bool open_world;
} expected[] = {
{"index_repository", false, false, true, false},
/* These query tools can reach resolve_store(), whose corrupt-store
* recovery quarantines/removes database files. Keep the annotations
* conservative until query resolution is strictly non-mutating. */
{"search_graph", false, true, true, false},
{"query_graph", false, true, true, false},
{"trace_path", false, true, true, false},
{"get_code_snippet", false, true, true, false},
/* The ten query tools resolve their store through the strictly
* non-mutating query-only path: a corrupt database is reported and
* left in place, never quarantined or rebuilt. Quarantine/rebuild is
* a write-side job (index_repository, manage_adr writes), so the
* read-only annotations are honest and plan-mode clients can expose
* these tools. get_file_outline arrived after this split and keeps
* its upstream conservative annotation. */
{"search_graph", true, false, true, false},
{"query_graph", true, false, true, false},
{"trace_path", true, false, true, false},
{"get_code_snippet", true, false, true, false},
{"get_file_outline", false, true, true, false},
{"get_graph_schema", false, true, true, false},
{"get_graph_schema", true, false, true, false},
{"compare_graphs", true, false, true, false},
{"get_architecture", false, true, true, false},
{"search_code", false, true, true, false},
{"get_architecture", true, false, true, false},
{"search_code", true, false, true, false},
{"list_projects", true, false, true, false},
{"delete_project", false, true, true, false},
{"index_status", false, true, true, false},
{"check_index_coverage", false, true, true, false},
{"detect_changes", false, true, true, false},
{"index_status", true, false, true, false},
{"check_index_coverage", true, false, true, false},
{"detect_changes", true, false, true, false},
{"manage_adr", false, true, false, false},
{"ingest_traces", false, false, false, false},
};
@@ -7553,9 +7557,11 @@ TEST(tool_cross_repo_honors_source_name_override) {
}
/* Corrupt-store quarantine renames/unlinks the project DB and sidecars, so it
* is a mutation even when resolve_store() was reached by a query tool. Generic
* queries use a blocking guard for that recovery, while manage_adr reads must
* use one nonblocking acquisition and never nest a blocking lease. */
* is a mutation — and exactly the mutation a query-only resolve must never
* perform. A query tool that meets a corrupt store reports the corruption and
* leaves every file in place, taking no mutation lease; the quarantine path
* belongs to write-side opens, where manage_adr reads must use one nonblocking
* acquisition and never nest a blocking lease. */
TEST(tool_corrupt_store_cleanup_guard_is_balanced_and_not_nested) {
char cache[256];
snprintf(cache, sizeof(cache), "%s/cbm-mcp-corrupt-guard-XXXXXX", cbm_tmpdir());
@@ -7581,6 +7587,7 @@ TEST(tool_corrupt_store_cleanup_guard_is_balanced_and_not_nested) {
char *resp =
cbm_mcp_handle_tool(query_srv, "search_graph",
"{\"project\":\"guard-corrupt-project\",\"name_pattern\":\".*\"}");
bool query_reports_corruption = resp && strstr(resp, "corrupt") != NULL;
free(resp);
cbm_mcp_server_free(query_srv);
char query_backup_path[CBM_SZ_1K];
@@ -7588,6 +7595,11 @@ TEST(tool_corrupt_store_cleanup_guard_is_balanced_and_not_nested) {
mcp_find_corrupt_backups(cache, project, query_backup_path, sizeof(query_backup_path));
bool query_quarantined =
!cbm_file_exists(db_path) && query_backup_count == 1 && query_backup_path[0] != '\0';
/* Snapshot the on-disk state HERE: the manage_adr branch below replants
* and then quarantines this same path, and the suite's cleanup unlinks
* it before the assertions run — a later cbm_file_exists() would observe
* that teardown, not the query-only resolve this check pins. */
bool query_db_left_in_place = cbm_file_exists(db_path);
/* Replant the same deterministic corruption to exercise manage_adr's
* already-held lease independently from the query server above. */
@@ -7617,13 +7629,14 @@ TEST(tool_corrupt_store_cleanup_guard_is_balanced_and_not_nested) {
free(saved_cache_copy);
cbm_rmdir(cache);
ASSERT_TRUE(query_quarantined);
ASSERT_EQ(query_probe.begin_count, 1);
ASSERT_EQ(query_probe.end_count, 1);
ASSERT_STR_EQ(query_probe.begin_projects[0], project);
ASSERT_STR_EQ(query_probe.end_projects[0], project);
ASSERT_TRUE(query_probe.db_exists_at_begin);
ASSERT_FALSE(query_probe.db_exists_at_end);
/* A query-only resolve never repairs: no lease is taken, the corrupt DB
* and its sidecars stay in place, and the reply names the corruption
* instead of "project not found". */
ASSERT_FALSE(query_quarantined);
ASSERT_TRUE(query_db_left_in_place);
ASSERT_TRUE(query_reports_corruption);
ASSERT_EQ(query_probe.begin_count, 0);
ASSERT_EQ(query_probe.end_count, 0);
ASSERT_TRUE(adr_quarantined);
ASSERT_EQ(adr_probe.begin_count, 0);
ASSERT_EQ(adr_probe.try_begin_count, 1);
@@ -7636,8 +7649,9 @@ TEST(tool_corrupt_store_cleanup_guard_is_balanced_and_not_nested) {
}
/* Integrity is checked before the lease is requested, but quarantine itself
* must fail closed when that lease is denied. In particular, a rejected query
* may not remove either a recoverable DB generation or its committed WAL. */
* must fail closed when that lease is denied. A query-only resolve never
* reaches that point at all: it asks for no lease, touches no file, and
* leaves both a recoverable DB generation and its committed WAL untouched. */
TEST(tool_corrupt_store_cleanup_guard_denial_preserves_db_and_wal) {
char cache[256];
snprintf(cache, sizeof(cache), "%s/cbm-mcp-corrupt-denied-XXXXXX", cbm_tmpdir());
@@ -7695,9 +7709,9 @@ TEST(tool_corrupt_store_cleanup_guard_denial_preserves_db_and_wal) {
free(saved_cache_copy);
cbm_rmdir(cache);
ASSERT_EQ(begin_count, 1);
ASSERT_EQ(begin_count, 0);
ASSERT_EQ(end_count, 0);
ASSERT_TRUE(guarded_project);
ASSERT_FALSE(guarded_project);
ASSERT_TRUE(db_unchanged);
ASSERT_TRUE(wal_unchanged);
ASSERT_EQ(backup_count, 0);
@@ -7801,10 +7815,11 @@ TEST(tool_manage_adr_corrupt_store_missing_try_guard_reports_configuration) {
PASS();
}
/* Another session may publish a good generation while this query waits for
* the mutation lease. Cleanup must re-open and re-check the path after lease
* acquisition; quarantining based on the stale pre-wait handle loses the new
* generation and returns a false "not indexed" result. */
/* A write-side open must trust only the generation that is current after its
* mutation lease is held: another session may have published a good
* generation while this request waited for the lease. Quarantining based on
* a stale pre-wait handle loses the new generation and returns a false
* "not indexed" result. */
TEST(tool_corrupt_store_cleanup_rechecks_generation_after_guard_wait) {
char cache[256];
snprintf(cache, sizeof(cache), "%s/cbm-mcp-corrupt-recheck-XXXXXX", cbm_tmpdir());
@@ -7832,7 +7847,8 @@ TEST(tool_corrupt_store_cleanup_rechecks_generation_after_guard_wait) {
cbm_mcp_server_set_project_mutation_guard(srv, mcp_replacing_mutation_guard_begin,
mcp_replacing_mutation_guard_end, &replacement);
char *resp = cbm_mcp_handle_tool(
srv, "search_graph", "{\"project\":\"guard-corrupt-recheck\",\"name_pattern\":\".*\"}");
srv, "manage_adr",
"{\"project\":\"guard-corrupt-recheck\",\"mode\":\"update\",\"content\":\"# ADR\\n\\nPending replacement.\"}");
bool response_used_replacement =
resp && !response_contains_json_fragment(resp, "\"isError\":true");
free(resp);
@@ -7908,7 +7924,8 @@ TEST(tool_corrupt_store_cleanup_preserves_existing_backup_and_uses_unique_name)
cbm_mcp_server_set_project_mutation_guard(srv, mcp_mutation_guard_probe_begin,
mcp_mutation_guard_probe_end, &probe);
char *resp = cbm_mcp_handle_tool(
srv, "search_graph", "{\"project\":\"guard-corrupt-unique\",\"name_pattern\":\".*\"}");
srv, "manage_adr",
"{\"project\":\"guard-corrupt-unique\",\"mode\":\"update\",\"content\":\"# ADR\\n\\nPending quarantine.\"}");
free(resp);
cbm_mcp_server_free(srv);
@@ -7981,9 +7998,9 @@ TEST(tool_corrupt_store_cleanup_publish_failure_preserves_db_and_wal) {
mcp_mutation_guard_probe_end, &guard);
mcp_quarantine_hook_probe_t hook = {.deny_step = "before_snapshot_publish"};
cbm_mcp_server_set_quarantine_test_hook(srv, mcp_quarantine_hook_probe, &hook);
char *resp =
cbm_mcp_handle_tool(srv, "search_graph",
"{\"project\":\"guard-corrupt-publish-fail\",\"name_pattern\":\".*\"}");
char *resp = cbm_mcp_handle_tool(
srv, "manage_adr",
"{\"project\":\"guard-corrupt-publish-fail\",\"mode\":\"update\",\"content\":\"# ADR\\n\\nPending publish.\"}");
bool db_unchanged = mcp_file_matches_snapshot(db_path, db_before, db_len);
bool wal_unchanged = mcp_file_matches_snapshot(wal_path, wal_before, wal_len);
@@ -8056,8 +8073,8 @@ TEST(tool_corrupt_store_cleanup_publishes_complete_wal_snapshot_before_delete) {
mcp_quarantine_hook_probe_t hook = {.deny_step = "after_snapshot_publish"};
cbm_mcp_server_set_quarantine_test_hook(srv, mcp_quarantine_hook_probe, &hook);
char *resp = cbm_mcp_handle_tool(
srv, "search_graph",
"{\"project\":\"guard-corrupt-after-publish\",\"name_pattern\":\".*\"}");
srv, "manage_adr",
"{\"project\":\"guard-corrupt-after-publish\",\"mode\":\"update\",\"content\":\"# ADR\\n\\nPending publish.\"}");
bool db_unchanged = mcp_file_matches_snapshot(db_path, db_before, db_len);
bool wal_unchanged = mcp_file_matches_snapshot(wal_path, wal_before, wal_len);
@@ -11071,12 +11088,14 @@ TEST(tool_resolve_store_by_internal_name_issue704) {
free(q_alpha);
/* ── D: the 0-byte ghost is NOT resolvable ─────────────────────── */
/* A query-only resolve reports the corrupt generation and leaves it in
* place — the reply names the corruption instead of "not found". */
char *q_ghost = cbm_mcp_server_handle(
srv, "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\","
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
"\"project\":\"ghost704\",\"name_pattern\":\".*\",\"limit\":5}}}");
ASSERT_NOT_NULL(q_ghost);
ASSERT_NOT_NULL(strstr(q_ghost, "not found"));
ASSERT_NOT_NULL(strstr(q_ghost, "corrupt"));
free(q_ghost);
/* ── E: addressing the drifted db by its FILENAME stays not-found ── */
+52
View File
@@ -14,6 +14,8 @@
#include "graph_buffer/graph_buffer.h"
#include "discover/discover.h"
#include "cbm.h"
#include "lang_specs.h" /* cbm_ts_language */
#include "foundation/constants.h" /* CBM_SZ_* */
#include <stdatomic.h>
#include <stdint.h>
@@ -1287,6 +1289,53 @@ TEST(mem_map_attributes_a_known_allocation) {
PASS();
}
/* #2010, the lifetime half. traversal_stack_not_in_result_arena in
* test_extraction.c pins the byte budget of the result arena, but a smaller
* CHAN_STACK_CAP would satisfy that too. This pins where the bytes actually
* went: the scratch arena takes the two 128 KB channel walks and the result
* arena does not. Builds the extraction context directly, since a completed
* cbm_extract_file_ex has already destroyed its scratch. */
TEST(extract_traversal_stacks_come_from_ctx_scratch_issue2010) {
enum { CHANNEL_WALK_BYTES = 2 * 4096 * (int)sizeof(TSNode) };
const char *src = "export const x = 1;\n";
const TSLanguage *ts_lang = cbm_ts_language(CBM_LANG_TYPESCRIPT);
ASSERT_NOT_NULL((void *)ts_lang);
TSParser *parser = ts_parser_new();
ASSERT_NOT_NULL(parser);
ts_parser_set_language(parser, ts_lang);
TSTree *tree = ts_parser_parse_string(parser, NULL, src, (uint32_t)strlen(src));
ASSERT_NOT_NULL(tree);
CBMFileResult result;
memset(&result, 0, sizeof(result));
cbm_arena_init(&result.arena);
CBMArena scratch;
cbm_arena_init_sized(&scratch, (size_t)CBM_SZ_512 * CBM_SZ_1K);
CBMExtractCtx ctx = {
.arena = &result.arena,
.scratch = &scratch,
.result = &result,
.source = src,
.source_len = (int)strlen(src),
.language = CBM_LANG_TYPESCRIPT,
.project = "t",
.rel_path = "a.ts",
.root = ts_tree_root_node(tree),
};
cbm_extract_channels(&ctx);
ASSERT_GTE(cbm_arena_total(&scratch), (size_t)CHANNEL_WALK_BYTES);
ASSERT_LT(cbm_arena_total(&result.arena), (size_t)CBM_SZ_64 * CBM_SZ_1K);
cbm_arena_destroy(&scratch);
cbm_arena_destroy(&result.arena);
ts_tree_delete(tree);
ts_parser_delete(parser);
PASS();
}
SUITE(mem) {
/* mem API */
RUN_TEST(mem_arena_eager_commit_follows_platform_commit_cost);
@@ -1350,4 +1399,7 @@ SUITE(mem) {
RUN_TEST(parallel_extract_without_source_retention);
RUN_TEST(parallel_extract_tiny_source_retention_budget);
RUN_TEST(parallel_extract_with_slab);
/* extraction scratch arena (#2010) */
RUN_TEST(extract_traversal_stacks_come_from_ctx_scratch_issue2010);
}
+74
View File
@@ -470,6 +470,77 @@ TEST(perl_malformed_source_remains_partial_issue1838) {
PASS();
}
/* ── #1746: trailing blanks before EOF are still just an absent newline ───────
*
* #1610 suppressed the zero-width MISSING terminator but tested for it with
* `end == source_len`. Trailing blanks are extras owned by no node, so
* `ENTRYPOINT ["a"] ` + EOF parks it at [29,29) while source_len is 30.
*
* The reporter's byte-exact controls pin the trigger to the PAIR: `] ` + EOF
* flags, `] ` + newline is clean, `]` + EOF is clean. */
TEST(dockerfile_trailing_blank_at_eof_not_flagged_issue1746) {
const char *cases[] = {
"FROM scratch\nENTRYPOINT [\"a\"] ", /* space + EOF — the report */
"FROM scratch\nENTRYPOINT [\"a\"]\t", /* tab + EOF */
"FROM scratch\nENTRYPOINT [\"a\"]\v", /* vertical tab + EOF */
"FROM scratch\nENTRYPOINT [\"a\"]\f", /* form feed + EOF */
"FROM scratch\nENTRYPOINT [\"a\"] \t ", /* run of blanks + EOF */
"FROM scratch\nENTRYPOINT [\"a\"] \r", /* CRLF file truncated to CR */
};
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
CBMFileResult *r = do_extract(cases[i], CBM_LANG_DOCKERFILE, "Dockerfile");
ASSERT_NOT_NULL(r);
bool flagged = r->parse_incomplete;
if (flagged) {
fprintf(stderr, " case %zu flagged: ranges=%s\n", i,
r->error_ranges ? r->error_ranges : "(none)");
}
cbm_free_result(r);
if (flagged) {
FAIL("trailing blanks must not turn an absent final newline into parse_partial");
}
}
PASS();
}
/* GUARD: widening the tail must not swallow a genuine mid-file failure just
* because the file happens to end in blanks. */
TEST(real_error_before_eof_still_flagged_with_trailing_blank_issue1746) {
size_t n = strlen(C_IFDEF_SPLIT);
char *buf = (char *)malloc(n + 1);
ASSERT_NOT_NULL(buf);
memcpy(buf, C_IFDEF_SPLIT, n + 1);
buf[n - 1] = ' '; /* final newline becomes a blank */
CBMFileResult *r = do_extract(buf, CBM_LANG_C, "split.c");
free(buf);
ASSERT_NOT_NULL(r);
bool flagged = r->parse_incomplete;
bool has_ranges = r->error_ranges != NULL;
cbm_free_result(r);
if (!flagged) {
FAIL("a real mid-file parse failure must still be reported when the file ends in blanks");
}
if (!has_ranges) {
FAIL("a reported failure must still name its line range");
}
PASS();
}
/* GUARD: a WIDTH-BEARING loss at EOF stays honest with a blank tail too — the
* Makefile recipe really is dropped, and only zero-width nodes are excused. */
TEST(width_bearing_error_at_eof_still_flagged_with_trailing_blank_issue1746) {
const char *src = "all:\n\techo hi ";
CBMFileResult *r = do_extract(src, CBM_LANG_MAKEFILE, "Makefile");
ASSERT_NOT_NULL(r);
bool flagged = r->parse_incomplete;
cbm_free_result(r);
if (!flagged) {
FAIL("a width-bearing parse failure at EOF must still be reported when the file ends in blanks");
}
PASS();
}
SUITE(parse_coverage) {
RUN_TEST(c_ifdef_split_brace_sets_parse_incomplete);
RUN_TEST(c_ifdef_split_brace_neighbors_still_extracted);
@@ -491,4 +562,7 @@ SUITE(parse_coverage) {
RUN_TEST(width_bearing_error_at_eof_still_flagged_issue1610);
RUN_TEST(perl_format_followed_by_named_sub_is_complete_issue1838);
RUN_TEST(perl_malformed_source_remains_partial_issue1838);
RUN_TEST(dockerfile_trailing_blank_at_eof_not_flagged_issue1746);
RUN_TEST(real_error_before_eof_still_flagged_with_trailing_blank_issue1746);
RUN_TEST(width_bearing_error_at_eof_still_flagged_with_trailing_blank_issue1746);
}
+184
View File
@@ -5102,6 +5102,60 @@ TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges) {
PASS();
}
/* Python bare-call local-binding suppression, sequential path. The bare-call
* counterpart of the receiver guard above: `run` is a PARAMETER, so `run()`
* cannot be the module-level `run` and must not bind SatoriLive.run.
*
* The positive control is deliberately a CROSS-FILE bare call with no import,
* so it resolves by a weak short-name strategy — one this guard could have
* killed. Asserting a same-file (same_module) edge instead would prove nothing,
* because no guard in this codebase touches same_module for any input.
* Fewer than 50 files exercises pass_calls.c. */
TEST(pipeline_python_bare_local_binding_suppresses_weak_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_seq_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "live.py",
"class SatoriLive:\n"
" def run(self):\n"
" return 1\n");
write_temp_file(tmp, "helpers.py",
"def compute_widget_total():\n"
" return 7\n");
write_temp_file(tmp, "gate.py",
"def _run_with_heavy_slot(run):\n"
" return run()\n"
"\n"
"def uses_free_function():\n"
" return compute_widget_total()\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/py_bare.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* NEGATIVE: the callee is shadowed by a parameter. */
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run"));
/* POSITIVE: an unshadowed cross-file bare call survives. */
ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total"));
/* Tripwire: a run that emitted no edges at all would satisfy the negative
* assertion vacuously. */
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Parallel Python regression for #1276. The field-type heuristic capitalizes
* the receiver token and previously promoted accelerator.print() to
* MockAccelerator.print at 0.85; ordinary suffix matching also selected one
@@ -5199,6 +5253,78 @@ TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges) {
PASS();
}
/* Parallel counterpart. >= 50 files forces pass_parallel.c, which is wired with
* the same gate: a guard wired on only one resolver produces an edge on the
* sequential path and not the parallel one, breaking MT determinism. #1386
* wired both and tested only the sequential path, and the `parallel` suite is
* exactly what catches that. Same both-directions pin as the sequential test. */
TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_bare_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "live.py",
"class SatoriLive:\n"
" def run(self):\n"
" return 1\n"
"\n"
"class BatchJob:\n"
" def execute(self):\n"
" return 2\n");
write_temp_file(tmp, "helpers.py",
"def compute_widget_total():\n"
" return 7\n");
write_temp_file(tmp, "gate.py",
"def _run_with_heavy_slot(run, execute):\n"
" run()\n"
" return execute()\n"
"\n"
"def uses_free_function():\n"
" return compute_widget_total()\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "filler%d.py", i);
snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/py_bare_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* NEGATIVE: both callees are shadowed by parameters. */
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "run"));
ASSERT_FALSE(cross_file_call_exists(s, project, "_run_with_heavy_slot", "execute"));
/* POSITIVE: the unshadowed cross-file bare call survives the parallel path. */
ASSERT_TRUE(cross_file_call_exists(s, project, "uses_free_function", "compute_widget_total"));
/* Tripwire against a vacuous pass. */
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1);
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* Reproduce-first: pass_parallel's fused cross-LSP eligibility currently counts
* only parser-backed calls/call references. A Python binary operator has no
* parser CBMCall; its __add__ semantic record and carrier are created together
@@ -5480,6 +5606,61 @@ TEST(pipeline_native_fetch_classified_as_http_calls) {
PASS();
}
/* #1892: Swift produced no Route node and no HTTP_CALLS edge, because the
* Swift grammar has no "arguments" field and the generic lookup therefore read
* no call arguments at all. Alamofire/URLSession were already in the service
* pattern table; the URL simply never reached it. This is the Swift twin of
* the TypeScript fetch case above. */
TEST(pipeline_swift_http_call_makes_route_issue1892) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_swifthttp_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
/* URLSession, not Alamofire's `AF` shorthand: the service pattern table
* matches the library name in the callee text, and "AF.request" contains
* no such name. */
write_temp_file(tmp, "Sources/Client.swift",
"import Foundation\n"
"final class Client {\n"
" func listWidgets() {\n"
" URLSession.shared.dataTask(with: \"/api/v1/widgets\")\n"
" }\n"
"}\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/swifthttp.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "HTTP_CALLS"), 1);
/* The edge carries the URL, so pass_route_nodes can mint the Route the
* cross-repo matcher joins a server route against. */
cbm_node_t *routes = NULL;
int route_count = 0;
cbm_store_find_nodes_by_label(s, project, "Route", &routes, &route_count);
int widget_routes = 0;
for (int i = 0; i < route_count; i++) {
if (routes[i].qualified_name && strstr(routes[i].qualified_name, "/api/v1/widgets")) {
widget_routes++;
}
}
cbm_store_free_nodes(routes, route_count);
ASSERT_GTE(widget_routes, 1);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Native `fetch()` (#856), parallel path (>= 50 files -> pass_parallel.c's
* resolve_file_calls). Mirrors pipeline_native_fetch_classified_as_http_calls
* but forces the parallel resolver, since the empty-resolution fallback is a
@@ -13141,10 +13322,13 @@ SUITE(pipeline) {
RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel);
RUN_TEST(pipeline_tsjs_receiver_parallel_keeps_service_edges);
RUN_TEST(pipeline_python_receiver_parallel_suppresses_weak_method_edges);
RUN_TEST(pipeline_python_bare_local_binding_suppresses_weak_edge);
RUN_TEST(pipeline_python_bare_local_binding_parallel_suppresses_weak_edge);
RUN_TEST(pipeline_parallel_python_cross_only_dunder_gets_synthetic_carrier);
RUN_TEST(pipeline_parallel_rust_cross_only_macro_hidden_gets_synthetic_carrier);
RUN_TEST(pipeline_arg_url_rejects_non_http_slash_arguments);
RUN_TEST(pipeline_native_fetch_classified_as_http_calls);
RUN_TEST(pipeline_swift_http_call_makes_route_issue1892);
RUN_TEST(pipeline_native_fetch_parallel_classified_as_http_calls);
RUN_TEST(pipeline_local_fetch_shadow_not_classified_as_http);
/* Git history pass */
+74
View File
@@ -902,6 +902,77 @@ TEST(dynamic_suppress_keeps_high_confidence_and_non_methods) {
PASS();
}
TEST(local_binding_suppress_drops_weak_shadowed_bare_calls) {
/* A bare `run()` whose callee is a parameter of an enclosing scope cannot be
* the module-level `run`, so a weak short-name match fabricates the edge. */
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "suffix_match"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "unique_name"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "field_type_hint"));
ASSERT_TRUE(cbm_suppress_weak_local_binding_call(true, true, "fuzzy"));
PASS();
}
TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies) {
/* THE RECALL PIN. A bare call to a genuine module-level function is NOT
* locally bound, so it is never suppressed — whatever the callee is spelled.
* This is the assertion a name-keyed guard (get/run/execute) would fail: it
* would drop these purely because of how the callee reads. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "suffix_match"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "unique_name"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "field_type_hint"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, false, "fuzzy"));
/* Every receiver-/import-aware strategy is kept even when shadowed. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "same_module"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "import_map"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "import_map_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "qualified_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "callee_suffix"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "service_pattern"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_cross"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_py_method"));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, "lsp_direct"));
/* Languages outside the caller's gate are never affected. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(false, true, "suffix_match"));
/* No match (NULL/empty strategy) → nothing to suppress. */
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, NULL));
ASSERT_FALSE(cbm_suppress_weak_local_binding_call(true, true, ""));
PASS();
}
TEST(weak_call_guards_share_one_drop_list) {
/* The member guard and the local-binding guard must agree on what "weak"
* means. They share a single static predicate for exactly this reason; if
* someone re-inlines one of the lists and edits only that copy, the two
* guards start disagreeing and this test catches it at the contract level
* rather than in a corpus months later. */
static const char *const strategies[] = {"suffix_match",
"unique_name",
"field_type_hint",
"fuzzy",
"same_module",
"import_map",
"import_map_suffix",
"qualified_suffix",
"callee_suffix",
"service_pattern",
"lsp_cross",
"lsp_ts_method",
"lsp_py_method",
"lsp_direct",
"",
NULL};
for (int i = 0; strategies[i] != NULL; i++) {
bool member = cbm_suppress_weak_member_match(true, true, strategies[i]);
bool binding = cbm_suppress_weak_local_binding_call(true, true, strategies[i]);
if (member != binding) {
printf(" drop-list divergence on strategy \"%s\": member=%d binding=%d\n",
strategies[i], member, binding);
}
ASSERT_EQ(member, binding);
}
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* Method call THROUGH an imported symbol that is itself an indexed node
@@ -998,4 +1069,7 @@ SUITE(registry) {
RUN_TEST(go_bare_ref_never_binds_field);
RUN_TEST(dynamic_suppress_drops_weak_method_matches);
RUN_TEST(dynamic_suppress_keeps_high_confidence_and_non_methods);
RUN_TEST(local_binding_suppress_drops_weak_shadowed_bare_calls);
RUN_TEST(local_binding_suppress_keeps_unshadowed_and_strong_strategies);
RUN_TEST(weak_call_guards_share_one_drop_list);
}