mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-16 05:10:08 +08:00
fix(graph): quality round after the v0.11.0 probe — deterministic resolution, findability, coverage, names
Nine findings from the 2026-09-16 graph-quality probe over the eleven bench corpora, each with a
test that fails without its fix and a before/after measurement on the real corpus.
Deterministic resolution (O9). Two indexes of one kernel tree differed in 5,821 IMPORTS and 632
CALLS edges. Three order/load dependencies carried it: the `#include` target was the first by-name
hit whose path ends with the include path — a bucket in worker merge order (include/linux/device.h
vs tools/virtio/linux/device.h); the registry broke suffix_match score ties by bucket position; the
LSP budget-share rule measured wall time. Include targets now prefer the File node, the least nested
path, then the lexicographic path; registry ties prefer the least nested QN, then the smaller QN; the
budget share is measured in thread CPU time like the budget itself. Two kernel indexes are now
identical (8,529,723 nodes, 1,974,940 CALLS, zero churn), as are two elasticsearch indexes.
Findability. search_graph's BM25 path honours the `label` filter and ranks an exact-name definition
above test methods that repeat the token (15/15 probe queries).
Coverage. The parse-coverage gap tree subtracts definitions the walker recovered inside an error
region (multi-line only); kernel/sched/core.c reports 476 flagged lines instead of 7,674.
Precision. Config-language callers (Makefile, CMake, YAML) no longer bind unique_name into code
(kernel Makefile -> C CALLS 195 -> 0). JS-family token names ({}, 1, (x: T) => U) are not
definitions; computed, quoted and escaped member keys stay (TypeScript 204 junk -> 0, 1,782 real
names kept). Names are cut at their first line (elasticsearch YAML 8,306 -> 0); a comment inside an
argument list is not an argument, so no Route node is named by a Java block comment; the wildcard
route `/*` is still a route. HTML/Vue/Svelte/Astro embedded member calls stay unbound.
Recall. A Python call through a self- or cls-rooted attribute keeps a specific unique_name member
target (Django +751, elasticsearch +3,288 CALLS, no new callee names).
Robustness. Edge properties never carry a split UTF-8 sequence (the 120-byte expression cut and the
JSON escaper both trim a partial code point). CLI array flags accept a JSON-array literal. Cypher
seeds a single-hop pattern from its selective end (kernel `MATCH (a)-[:CALLS]->(b) WHERE b.name =
'_printk'`: execution limit -> 7.9 s).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+193
-27
@@ -125,7 +125,79 @@ void cbm_reset_profile(void) {
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* A definition name never spans lines. Config extractors hand over a node's
|
||||
* whole text — a YAML block key, a Makefile recipe (`endif\n\n$(obj)/x.h`), a
|
||||
* ktest .conf directive — and the line break rode into the graph (2026-09-16
|
||||
* probe: 8,301 Field nodes on elastic/elasticsearch, 91 kernel nodes). Cut at
|
||||
* the first line break and drop trailing blanks; the qualified name carries
|
||||
* the same text as its last segment and is cut the same way. */
|
||||
static const char *cbm_first_line(CBMArena *a, const char *text) {
|
||||
if (!text) {
|
||||
return text;
|
||||
}
|
||||
const char *nl = strpbrk(text, "\r\n");
|
||||
if (!nl) {
|
||||
return text;
|
||||
}
|
||||
size_t n = (size_t)(nl - text);
|
||||
while (n > 0 && (text[n - 1] == ' ' || text[n - 1] == '\t')) {
|
||||
n--;
|
||||
}
|
||||
char *cut = (char *)cbm_arena_alloc(a, n + 1);
|
||||
if (!cut) {
|
||||
return text;
|
||||
}
|
||||
memcpy(cut, text, n);
|
||||
cut[n] = '\0';
|
||||
return cut;
|
||||
}
|
||||
|
||||
/* JS/TS files: a definition named by a literal token is the walker naming a
|
||||
* function or variable from an object key or a numeric pattern element —
|
||||
* `{}` (1,534 incoming CALLS on microsoft/TypeScript's test baselines), `1`
|
||||
* (38 nodes), a quoted string. An identifier starts with a letter, `_`, `$`,
|
||||
* `#` (private members) or a non-ASCII byte; anything else is not a name. */
|
||||
static bool cbm_js_family_path(const char *path) {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
const char *dot = strrchr(path, '.');
|
||||
if (!dot) {
|
||||
return false;
|
||||
}
|
||||
static const char *const exts[] = {".js", ".mjs", ".cjs", ".jsx", ".ts",
|
||||
".mts", ".cts", ".tsx", ".ets", NULL};
|
||||
for (int i = 0; exts[i]; i++) {
|
||||
if (strcmp(dot, exts[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool cbm_js_name_is_junk(const char *name) {
|
||||
if (!name || !name[0]) {
|
||||
return false; /* empty names are handled by the callers' own rules */
|
||||
}
|
||||
unsigned char c = (unsigned char)name[0];
|
||||
bool identifier_start = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' ||
|
||||
c == '$' || c == '#' || c >= 0x80;
|
||||
/* Member names JS spells without an identifier start are still names:
|
||||
* computed keys (`[Symbol.iterator]`), string-literal keys (`"my-key"`,
|
||||
* `'x'`) and escaped identifiers (`А`). Measured on the TypeScript
|
||||
* corpus: 1,531 `[…]` members, 97 quoted members, 154 escaped
|
||||
* identifiers — all real definitions. What remains ({…} patterns, numeric
|
||||
* literals, parenthesised types, `...rest`) is a token, not a name. */
|
||||
bool member_key_start = c == '[' || c == '"' || c == '\'' || c == '\\';
|
||||
return !identifier_start && !member_key_start;
|
||||
}
|
||||
|
||||
void cbm_defs_push(CBMDefArray *arr, CBMArena *a, CBMDefinition def) {
|
||||
def.name = cbm_first_line(a, def.name);
|
||||
def.qualified_name = cbm_first_line(a, def.qualified_name);
|
||||
if (cbm_js_family_path(def.file_path) && cbm_js_name_is_junk(def.name)) {
|
||||
return;
|
||||
}
|
||||
GROW_ARRAY(arr, a);
|
||||
arr->items[arr->count++] = def;
|
||||
}
|
||||
@@ -1358,14 +1430,88 @@ static void cbm_mark_pp_error_rows(TSNode n, uint8_t *rows, uint32_t row_count,
|
||||
/* Recovery subtraction (#963): tree-sitter error recovery plus the
|
||||
* ERROR-descending def walker often still extract constructs INSIDE a failed
|
||||
* region (verified: a function in an #ifdef-split ERROR region and even a
|
||||
* `def broken(:` both came back as defs). A region whose every line is
|
||||
* covered by definitions that START inside it is definitely recovered — its
|
||||
* constructs ARE in the graph — so flagging it would be a false miss.
|
||||
* Container defs (Module/Package) are ignored: a file-spanning Module node is
|
||||
* not evidence the region's constructs survived. Conservative: partially
|
||||
* covered regions stay flagged. */
|
||||
static bool cbm_region_is_recovered(uint32_t rs, uint32_t re, const CBMDefArray *defs) {
|
||||
enum { MAX_COVER_DEFS = 256 };
|
||||
* `def broken(:` both came back as defs). The lines a definition that STARTS
|
||||
* inside the region covers are therefore in the graph, and only the lines no
|
||||
* definition covers can honestly be called missed. Container defs
|
||||
* (Module/Package) are ignored: a file-spanning Module node is not evidence
|
||||
* the region's constructs survived.
|
||||
*
|
||||
* This used to be all-or-nothing — a region stayed flagged whole unless every
|
||||
* line was covered. On torvalds/linux (2026-09-16 probe) one ERROR node that
|
||||
* swallowed the second half of kernel/sched/core.c (lines 5522–11284) survived
|
||||
* because of the comment and macro lines between its 286 extracted functions,
|
||||
* and the coverage report told a reader 68 % of the scheduler was unindexed.
|
||||
* Now the uncovered gaps are reported instead, and a gap holding only blank,
|
||||
* comment or preprocessor lines is not a miss at all. */
|
||||
static uint32_t *cbm_line_offsets(const char *src, int src_len, uint32_t *out_lines) {
|
||||
uint32_t lines = 1;
|
||||
for (int i = 0; i < src_len; i++) {
|
||||
if (src[i] == '\n') {
|
||||
lines++;
|
||||
}
|
||||
}
|
||||
uint32_t *offs = (uint32_t *)malloc((size_t)(lines + 1) * sizeof(uint32_t));
|
||||
if (!offs) {
|
||||
*out_lines = 0;
|
||||
return NULL;
|
||||
}
|
||||
uint32_t n = 0;
|
||||
offs[n++] = 0;
|
||||
for (int i = 0; i < src_len && n < lines; i++) {
|
||||
if (src[i] == '\n') {
|
||||
offs[n++] = (uint32_t)i + 1;
|
||||
}
|
||||
}
|
||||
offs[n] = (uint32_t)src_len;
|
||||
*out_lines = n;
|
||||
return offs;
|
||||
}
|
||||
|
||||
/* A 1-based line that holds nothing a reader would call missed code: blank,
|
||||
* a comment line, or a preprocessor directive (the import pass owns #include;
|
||||
* #if/#endif carry no construct of their own). */
|
||||
static bool cbm_line_is_inert(const char *src, const uint32_t *offs, uint32_t nlines,
|
||||
uint32_t line) {
|
||||
if (!src || !offs || line == 0 || line > nlines) {
|
||||
return true;
|
||||
}
|
||||
const char *p = src + offs[line - 1];
|
||||
const char *end = src + offs[line];
|
||||
while (p < end && (*p == ' ' || *p == '\t' || *p == '\r')) {
|
||||
p++;
|
||||
}
|
||||
if (p >= end || *p == '\n') {
|
||||
return true;
|
||||
}
|
||||
if (p[0] == '/' && p + 1 < end && (p[1] == '/' || p[1] == '*')) {
|
||||
return true;
|
||||
}
|
||||
return p[0] == '*' || p[0] == '#';
|
||||
}
|
||||
|
||||
static void cbm_regions_emit_gap(cbm_error_regions_t *out, uint32_t gs, uint32_t ge,
|
||||
const char *src, const uint32_t *offs, uint32_t nlines) {
|
||||
bool live = false;
|
||||
for (uint32_t l = gs; l <= ge && !live; l++) {
|
||||
live = !cbm_line_is_inert(src, offs, nlines, l);
|
||||
}
|
||||
if (!live) {
|
||||
return;
|
||||
}
|
||||
if (out->count < CBM_MAX_ERROR_REGIONS) {
|
||||
out->starts[out->count] = gs;
|
||||
out->ends[out->count] = ge;
|
||||
out->count++;
|
||||
} else {
|
||||
out->dropped++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Replace [rs, re] by the sub-ranges no in-region definition covers. */
|
||||
static void cbm_region_uncovered_gaps(uint32_t rs, uint32_t re, const CBMDefArray *defs,
|
||||
const char *src, const uint32_t *offs, uint32_t nlines,
|
||||
cbm_error_regions_t *out) {
|
||||
enum { MAX_COVER_DEFS = 1024 };
|
||||
uint32_t starts[MAX_COVER_DEFS];
|
||||
uint32_t ends[MAX_COVER_DEFS];
|
||||
int n = 0;
|
||||
@@ -1377,14 +1523,25 @@ static bool cbm_region_is_recovered(uint32_t rs, uint32_t re, const CBMDefArray
|
||||
if (d->start_line < rs || d->start_line > re) {
|
||||
continue; /* recovery evidence must originate inside the region */
|
||||
}
|
||||
/* A one-line definition salvaged from inside an error region is the
|
||||
* walker's guess at broken text (`def x(:` comes back as a def, a
|
||||
* mis-parsed `static _Thread_local int x` as a Variable named `int`,
|
||||
* and Python's synthetic builtins all sit on line 1); only a construct
|
||||
* whose body spans lines proves it was understood. Conservative: real
|
||||
* one-line variables and macros inside a failed region keep their line
|
||||
* flagged. */
|
||||
if (d->end_line <= d->start_line) {
|
||||
continue;
|
||||
}
|
||||
starts[n] = d->start_line;
|
||||
ends[n] = d->end_line < d->start_line ? d->start_line : d->end_line;
|
||||
ends[n] = d->end_line;
|
||||
n++;
|
||||
}
|
||||
if (n == 0) {
|
||||
return false;
|
||||
cbm_regions_emit_gap(out, rs, re, src, offs, nlines);
|
||||
return;
|
||||
}
|
||||
/* Insertion-sort by start, then sweep for gaps in [rs, re]. */
|
||||
/* Insertion-sort by start, then sweep and emit the gaps in [rs, re]. */
|
||||
for (int i = 1; i < n; i++) {
|
||||
uint32_t s = starts[i];
|
||||
uint32_t e = ends[i];
|
||||
@@ -1397,16 +1554,18 @@ static bool cbm_region_is_recovered(uint32_t rs, uint32_t re, const CBMDefArray
|
||||
starts[j + 1] = s;
|
||||
ends[j + 1] = e;
|
||||
}
|
||||
uint32_t covered_to = rs - 1;
|
||||
uint32_t cursor = rs;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (starts[i] > covered_to + 1) {
|
||||
return false; /* uncovered gap */
|
||||
if (starts[i] > cursor) {
|
||||
cbm_regions_emit_gap(out, cursor, starts[i] - 1, src, offs, nlines);
|
||||
}
|
||||
if (ends[i] > covered_to) {
|
||||
covered_to = ends[i];
|
||||
if (ends[i] + 1 > cursor) {
|
||||
cursor = ends[i] + 1;
|
||||
}
|
||||
}
|
||||
return covered_to >= re;
|
||||
if (cursor <= re) {
|
||||
cbm_regions_emit_gap(out, cursor, re, src, offs, nlines);
|
||||
}
|
||||
}
|
||||
|
||||
/* #961: true when 1-based `line` of `src` contains `name` (used to verify a
|
||||
@@ -1598,16 +1757,19 @@ static bool cbm_remap_preprocessed_def(CBMDefinition *def, const CBMPreprocessed
|
||||
return true;
|
||||
}
|
||||
|
||||
static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMDefArray *defs) {
|
||||
int kept = 0;
|
||||
for (int i = 0; i < regs->count; i++) {
|
||||
if (!cbm_region_is_recovered(regs->starts[i], regs->ends[i], defs)) {
|
||||
regs->starts[kept] = regs->starts[i];
|
||||
regs->ends[kept] = regs->ends[i];
|
||||
kept++;
|
||||
}
|
||||
static void cbm_subtract_recovered_regions(cbm_error_regions_t *regs, const CBMDefArray *defs,
|
||||
const char *src, int src_len) {
|
||||
if (regs->count <= 0) {
|
||||
return;
|
||||
}
|
||||
regs->count = kept;
|
||||
uint32_t nlines = 0;
|
||||
uint32_t *offs = cbm_line_offsets(src, src_len, &nlines);
|
||||
cbm_error_regions_t out = {{0}, {0}, 0, regs->dropped};
|
||||
for (int i = 0; i < regs->count; i++) {
|
||||
cbm_region_uncovered_gaps(regs->starts[i], regs->ends[i], defs, src, offs, nlines, &out);
|
||||
}
|
||||
free(offs);
|
||||
*regs = out;
|
||||
}
|
||||
|
||||
/* #1071: a function-like macro invocation whose argument is a type token
|
||||
@@ -2019,6 +2181,10 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
|
||||
TSTree *tree = ts_parser_parse_with_options(parser, NULL, ts_input, opts);
|
||||
uint64_t t1 = now_ns();
|
||||
uint64_t parse_cpu_ns = cbm_thread_cpu_time_ns() - cpu_start_ns;
|
||||
#ifdef CBM_ENABLE_TEST_SEAMS
|
||||
t1 += tl_parse_wall_seam_offset_ns; /* the stall seam inflates every wall reading */
|
||||
#endif
|
||||
|
||||
if (!tree) {
|
||||
result->has_error = true;
|
||||
@@ -2669,7 +2835,7 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
|
||||
* splitting a brace inside a recovered function looks unrecovered:
|
||||
* the refinement keeps only the discarded branch, the function starts
|
||||
* above it, and the evidence falls outside the range. */
|
||||
cbm_subtract_recovered_regions(®s, &result->defs);
|
||||
cbm_subtract_recovered_regions(®s, &result->defs, source, source_len);
|
||||
/* Phase 2: cut what is left down to the lines the preprocessed parse
|
||||
* could not explain. */
|
||||
if (pp_line_map) {
|
||||
|
||||
+25
-20
@@ -269,26 +269,31 @@ typedef struct {
|
||||
* calls at 251 MB with most of that empty slots. Readers index it exactly
|
||||
* as before; only `sizeof` changed. */
|
||||
CBMCallArg *args;
|
||||
int arg_count; // number of captured arguments (<= CBM_MAX_CALL_ARGS)
|
||||
int loop_depth; // enclosing loop nesting at the call site
|
||||
int branch_depth; // enclosing branch nesting at the call site
|
||||
int start_line; // 1-based source line of the call (for def range-match)
|
||||
uint32_t site_start_byte; // exact AST occurrence span; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
bool is_method; // method/member call with an UNRESOLVED receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Python:
|
||||
// x.foo() where x is not self/cls/super() and is not
|
||||
// rooted in an imported name. Read by the weak-member
|
||||
// guard and by the pxc synthetic-carrier dedup key in
|
||||
// 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.
|
||||
int arg_count; // number of captured arguments (<= CBM_MAX_CALL_ARGS)
|
||||
int loop_depth; // enclosing loop nesting at the call site
|
||||
int branch_depth; // enclosing branch nesting at the call site
|
||||
int start_line; // 1-based source line of the call (for def range-match)
|
||||
uint32_t site_start_byte; // exact AST occurrence span; end > start when present
|
||||
uint32_t site_end_byte; // exclusive byte offset in the source file
|
||||
CBMSourceOrigin source_origin; // raw source or C-family preprocessed buffer
|
||||
bool is_method; // method/member call with an UNRESOLVED receiver. Perl:
|
||||
// arrow/method call ($obj->m). TS/JS/TSX: member call
|
||||
// x.foo() whose receiver is not this/super. Python:
|
||||
// x.foo() where x is not self/cls/super() and is not
|
||||
// rooted in an imported name. Read by the weak-member
|
||||
// guard and by the pxc synthetic-carrier dedup key in
|
||||
// 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.
|
||||
bool receiver_is_self_attribute; // Python member call whose receiver is an attribute
|
||||
// chain rooted at self/cls but not self/cls itself
|
||||
// (self.compiler.apply_converters()). An object the
|
||||
// class owns, not a parameter: read by the weak-member
|
||||
// guard's unique-name exemption. Default false.
|
||||
} CBMCall;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -2068,6 +2068,14 @@ static void extract_call_args(CBMExtractCtx *ctx, TSNode args, CBMCall *call) {
|
||||
for (uint32_t ai = 0; ai < argc && call->arg_count < CBM_MAX_CALL_ARGS; ai++) {
|
||||
TSNode arg_node = ts_node_named_child(args, ai);
|
||||
const char *ak = ts_node_type(arg_node);
|
||||
/* tree-sitter lists a comment inside the argument list as a named
|
||||
* child (Java block_comment/line_comment, C-family comment). It is
|
||||
* not an argument: taken as one, a leading block comment became
|
||||
* args[0] and the Route pass read its text as a URL. */
|
||||
if (strcmp(ak, "comment") == 0 || strcmp(ak, "line_comment") == 0 ||
|
||||
strcmp(ak, "block_comment") == 0) {
|
||||
continue;
|
||||
}
|
||||
if (!call->args) {
|
||||
call->args = cbm_arena_calloc(ctx->arena, CBM_MAX_CALL_ARGS * sizeof(CBMCallArg));
|
||||
if (!call->args) {
|
||||
@@ -2965,6 +2973,28 @@ static char *resolve_objectscript_instance_call(CBMArena *a, TSNode node, const
|
||||
* statically-known type here, so the call must not bind by short name alone.
|
||||
* Note `self.client.send()` is NOT exempt: the receiver is `self.client`, an
|
||||
* attribute of unknown type, not `self` itself. */
|
||||
/* True when a Python attribute-call receiver is an attribute chain ROOTED at
|
||||
* self/cls but is not self/cls itself: `self.compiler.apply_converters()` has
|
||||
* receiver `self.compiler`, an object the class owns. The weak-member guard's
|
||||
* unique-name exemption keys on this shape (see cbm_weak_member_unique_name_
|
||||
* exempt): a bare parameter (`accelerator.backward()`) carries no ownership
|
||||
* evidence and stays suppressed. Direct `self.m()` is already exempt via
|
||||
* python_receiver_is_exempt and is deliberately NOT flagged here. */
|
||||
static bool python_receiver_rooted_at_self(CBMExtractCtx *ctx, TSNode receiver) {
|
||||
if (ts_node_is_null(receiver) || strcmp(ts_node_type(receiver), "attribute") != 0) {
|
||||
return false;
|
||||
}
|
||||
TSNode root = receiver;
|
||||
while (!ts_node_is_null(root) && strcmp(ts_node_type(root), "attribute") == 0) {
|
||||
root = ts_node_child_by_field_name(root, TS_FIELD("object"));
|
||||
}
|
||||
if (ts_node_is_null(root) || strcmp(ts_node_type(root), "identifier") != 0) {
|
||||
return false;
|
||||
}
|
||||
char *name = cbm_node_text(ctx->arena, root, ctx->source);
|
||||
return name && (strcmp(name, "self") == 0 || strcmp(name, "cls") == 0);
|
||||
}
|
||||
|
||||
static bool python_receiver_is_exempt(CBMExtractCtx *ctx, TSNode receiver) {
|
||||
if (ts_node_is_null(receiver)) {
|
||||
return false;
|
||||
@@ -3695,6 +3725,7 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
|
||||
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);
|
||||
call.receiver_is_self_attribute = python_receiver_rooted_at_self(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);
|
||||
}
|
||||
|
||||
@@ -714,6 +714,25 @@ static const char *strip_string_delimiters(const char *literal, char *buf, size_
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* A comment opens with the slash a route opens with, and an argument list
|
||||
* that starts with one handed three Java block comments to the Route pass as
|
||||
* URLs (elasticsearch, 2026-09-16). The wildcard route (slash-star alone) is a
|
||||
* path, so the block-comment shape needs its closing star-slash; a route
|
||||
* literal never holds a line break. */
|
||||
bool cbm_service_pattern_is_comment_text(const char *text) {
|
||||
if (!text || text[0] != '/') {
|
||||
return false;
|
||||
}
|
||||
if (strchr(text, '\n') != NULL || strchr(text, '\r') != NULL) {
|
||||
return true;
|
||||
}
|
||||
size_t n = strlen(text);
|
||||
if (text[1] == '*' && n >= 4 && text[n - 2] == '*' && text[n - 1] == '/') {
|
||||
return true;
|
||||
}
|
||||
return text[1] == '/' && (text[2] == ' ' || text[2] == '\t');
|
||||
}
|
||||
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name) {
|
||||
char path_buf[1024];
|
||||
const char *path = strip_string_delimiters(literal, path_buf, sizeof(path_buf));
|
||||
@@ -729,6 +748,9 @@ bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *
|
||||
if (path[0] != '/') {
|
||||
return false;
|
||||
}
|
||||
if (cbm_service_pattern_is_comment_text(path)) {
|
||||
return false;
|
||||
}
|
||||
if (callee_is_delimiter_or_filesystem_builder(callee_name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,12 @@ bool cbm_service_pattern_is_global_fetch(const char *callee_name);
|
||||
* Rejects filesystem paths and non-HTTP string consumers. */
|
||||
bool cbm_service_pattern_is_http_route_literal(const char *literal, const char *callee_name);
|
||||
|
||||
/* True when text that reached a path slot is a source comment rather than a
|
||||
* path: a block comment (slash-star … star-slash), a line comment (slash-slash
|
||||
* followed by a space), or any text with a line break. The wildcard route
|
||||
* (slash-star alone) is a path. */
|
||||
bool cbm_service_pattern_is_comment_text(const char *text);
|
||||
|
||||
/* Per-worker TLS cache for cbm_service_pattern_match results. The
|
||||
* pattern matcher runs once per resolved CALL edge in emit_service_
|
||||
* edge — that's 6 pattern lists × ~30 patterns × strstr per call ≈
|
||||
|
||||
@@ -13752,6 +13752,32 @@ static void cli_add_typed(yyjson_mut_doc *out, yyjson_mut_val *obj, const char *
|
||||
arr = yyjson_mut_arr(out);
|
||||
yyjson_mut_obj_add(obj, yyjson_mut_strcpy(out, key), arr);
|
||||
}
|
||||
/* The help prints `--paths <array>`, and a caller who writes the
|
||||
* array literally — `--paths '["lib","t"]'` — used to get ONE element
|
||||
* holding that literal text (2026-09-16 probe: check_index_coverage
|
||||
* reported the fake path `["lib","t"]`). A value that parses as a JSON
|
||||
* array contributes its elements; anything else is one element. */
|
||||
if (have_value && value && value[0] == '[') {
|
||||
yyjson_doc *lit = yyjson_read(value, strlen(value), 0);
|
||||
yyjson_val *lit_root = lit ? yyjson_doc_get_root(lit) : NULL;
|
||||
if (lit_root && yyjson_is_arr(lit_root)) {
|
||||
size_t idx;
|
||||
size_t max;
|
||||
yyjson_val *elem;
|
||||
yyjson_arr_foreach(lit_root, idx, max, elem) {
|
||||
if (yyjson_is_str(elem)) {
|
||||
yyjson_mut_arr_add_strcpy(out, arr, yyjson_get_str(elem));
|
||||
} else {
|
||||
yyjson_mut_arr_add_val(arr, yyjson_val_mut_copy(out, elem));
|
||||
}
|
||||
}
|
||||
yyjson_doc_free(lit);
|
||||
return;
|
||||
}
|
||||
if (lit) {
|
||||
yyjson_doc_free(lit);
|
||||
}
|
||||
}
|
||||
yyjson_mut_arr_add_strcpy(out, arr, have_value ? value : "");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5014,8 +5014,104 @@ static void execute_return_clause(cbm_query_t *q, cbm_return_clause_t *ret, bind
|
||||
limit_is_engine_budget);
|
||||
}
|
||||
|
||||
/* ── Planner: seed a single-hop pattern from its selective end ──────────────
|
||||
*
|
||||
* The executor scans pattern node 0 and expands from there. Written as
|
||||
* `MATCH (a)-[:CALLS]->(b) WHERE b.name = '_printk'`, that scans every node
|
||||
* in the store before the filter on `b` can discard any — on an 8.5 M node
|
||||
* kernel graph the query hit the execution-time limit while the anchored
|
||||
* spelling `MATCH (b:Function {name:'_printk'})<-[:CALLS]-(a)` answered in
|
||||
* seconds (2026-09-16 probe). A reader should not have to know which end to
|
||||
* write first: when the far node carries an equality on name/qualified_name,
|
||||
* an inline property, or a label and the near node carries less, the pattern
|
||||
* is walked from the far end with the relationship direction inverted. The
|
||||
* result set is identical; only the enumeration order changes. Restricted to
|
||||
* one single-relationship pattern with an explicit RETURN, and never to an
|
||||
* OPTIONAL MATCH, whose anchor is part of its meaning. */
|
||||
static int cypher_cond_selectivity(const cbm_condition_t *c, const char *var) {
|
||||
if (!c || !c->variable || !var || strcmp(c->variable, var) != 0 || c->negated) {
|
||||
return 0;
|
||||
}
|
||||
if (!c->op || strcmp(c->op, "=") != 0 || !c->property) {
|
||||
return 0;
|
||||
}
|
||||
return (strcmp(c->property, "name") == 0 || strcmp(c->property, "qualified_name") == 0) ? 3 : 2;
|
||||
}
|
||||
|
||||
static int cypher_expr_selectivity(const cbm_expr_t *e, const char *var) {
|
||||
if (!e) {
|
||||
return 0;
|
||||
}
|
||||
if (e->type == EXPR_CONDITION) {
|
||||
return cypher_cond_selectivity(&e->cond, var);
|
||||
}
|
||||
if (e->type == EXPR_AND) {
|
||||
int l = cypher_expr_selectivity(e->left, var);
|
||||
int r = cypher_expr_selectivity(e->right, var);
|
||||
return l > r ? l : r;
|
||||
}
|
||||
return 0; /* OR / NOT / XOR: no single equality to seed from */
|
||||
}
|
||||
|
||||
static int cypher_node_selectivity(const cbm_node_pattern_t *n, const cbm_where_clause_t *w) {
|
||||
int s = 0;
|
||||
for (int i = 0; i < n->prop_count; i++) {
|
||||
const char *k = n->props[i].key;
|
||||
int v = (k && (strcmp(k, "name") == 0 || strcmp(k, "qualified_name") == 0)) ? 3 : 2;
|
||||
if (v > s) {
|
||||
s = v;
|
||||
}
|
||||
}
|
||||
if (w && n->variable) {
|
||||
int v = 0;
|
||||
if (w->root) {
|
||||
v = cypher_expr_selectivity(w->root, n->variable);
|
||||
} else if (!w->op || strcmp(w->op, "AND") == 0) {
|
||||
for (int i = 0; i < w->count; i++) {
|
||||
int c = cypher_cond_selectivity(&w->conditions[i], n->variable);
|
||||
if (c > v) {
|
||||
v = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (v > s) {
|
||||
s = v;
|
||||
}
|
||||
}
|
||||
if (s == 0 && n->label) {
|
||||
s = 1;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
static void cypher_plan_seed_from_selective_end(cbm_query_t *q) {
|
||||
if (!q || q->pattern_count != 1 || !q->ret || (q->pattern_optional && q->pattern_optional[0])) {
|
||||
return;
|
||||
}
|
||||
cbm_pattern_t *p = &q->patterns[0];
|
||||
if (p->rel_count != 1 || p->node_count != 2) {
|
||||
return;
|
||||
}
|
||||
if (cypher_node_selectivity(&p->nodes[1], q->where) <=
|
||||
cypher_node_selectivity(&p->nodes[0], q->where)) {
|
||||
return;
|
||||
}
|
||||
cbm_node_pattern_t tmp = p->nodes[0];
|
||||
p->nodes[0] = p->nodes[1];
|
||||
p->nodes[1] = tmp;
|
||||
cbm_rel_pattern_t *r = &p->rels[0];
|
||||
if (r->direction && strcmp(r->direction, "outbound") == 0) {
|
||||
free((void *)r->direction);
|
||||
r->direction = heap_strdup("inbound");
|
||||
} else if (r->direction && strcmp(r->direction, "inbound") == 0) {
|
||||
free((void *)r->direction);
|
||||
r->direction = heap_strdup("outbound");
|
||||
}
|
||||
}
|
||||
|
||||
static int execute_single(cbm_store_t *store, cbm_query_t *q, const char *project, int max_rows,
|
||||
result_builder_t *rb) {
|
||||
cypher_plan_seed_from_selective_end(q);
|
||||
cbm_pattern_t *pat0 = &q->patterns[0];
|
||||
|
||||
/* Step 1: Scan initial nodes */
|
||||
|
||||
@@ -310,6 +310,33 @@ bool cbm_validate_project_name(const char *name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int cbm_utf8_trim_partial(char *buf) {
|
||||
if (!buf) {
|
||||
return 0;
|
||||
}
|
||||
int pos = (int)strlen(buf);
|
||||
if (pos == 0) {
|
||||
return 0;
|
||||
}
|
||||
/* Walk back over continuation bytes to the lead byte; drop the sequence
|
||||
* when fewer continuation bytes follow it than its lead byte announces. */
|
||||
int back = pos - 1;
|
||||
int cont = 0;
|
||||
while (back >= 0 && ((unsigned char)buf[back] & 0xC0) == 0x80) {
|
||||
back--;
|
||||
cont++;
|
||||
}
|
||||
if (back >= 0) {
|
||||
unsigned char lead = (unsigned char)buf[back];
|
||||
int need = lead >= 0xF0 ? 3 : lead >= 0xE0 ? 2 : lead >= 0xC0 ? 1 : 0;
|
||||
if (need > cont) {
|
||||
pos = back;
|
||||
buf[pos] = '\0';
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
int cbm_json_escape(char *buf, int bufsize, const char *src) {
|
||||
if (!buf || bufsize <= 0) {
|
||||
return 0;
|
||||
@@ -319,7 +346,8 @@ int cbm_json_escape(char *buf, int bufsize, const char *src) {
|
||||
return 0;
|
||||
}
|
||||
int pos = 0;
|
||||
for (int i = 0; src[i] && pos < bufsize - JSON_NUL_RESERVE; i++) {
|
||||
int i = 0;
|
||||
for (; src[i] && pos < bufsize - JSON_NUL_RESERVE; i++) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c == '"' || c == '\\') {
|
||||
if (pos + JSON_ESC_LEN > bufsize - JSON_NUL_RESERVE) {
|
||||
@@ -356,5 +384,11 @@ int cbm_json_escape(char *buf, int bufsize, const char *src) {
|
||||
}
|
||||
}
|
||||
buf[pos] = '\0';
|
||||
/* A truncated output must never end inside a multibyte UTF-8 sequence: a
|
||||
* callee cut mid-character persisted bytes SQLite cannot decode as text
|
||||
* (2026-09-16 probe: nine CALLS rows across rust, java and typescript). */
|
||||
if (src[i] != '\0') {
|
||||
pos = cbm_utf8_trim_partial(buf);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -91,9 +91,16 @@ bool cbm_validate_project_name(const char *name);
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* Drop a trailing INCOMPLETE UTF-8 sequence from a NUL-terminated buffer in
|
||||
* place (a lead byte whose continuation bytes were cut off). Returns the new
|
||||
* length. Complete sequences and ASCII are left untouched. Use after any
|
||||
* byte-count truncation of text that will be stored or serialised. */
|
||||
int cbm_utf8_trim_partial(char *buf);
|
||||
|
||||
/* Escape a string for safe embedding in JSON: escapes " \ and control chars.
|
||||
* Writes into buf (including NUL). Returns number of chars written (excl NUL).
|
||||
* If buf is too small, output is truncated but always NUL-terminated. */
|
||||
* If buf is too small, output is truncated but always NUL-terminated, and it
|
||||
* never ends inside a multibyte UTF-8 sequence. */
|
||||
int cbm_json_escape(char *buf, int bufsize, const char *src);
|
||||
|
||||
#endif /* CBM_STR_UTIL_H */
|
||||
|
||||
+35
-5
@@ -3762,6 +3762,8 @@ enum {
|
||||
BM25_BIND_OFFSET = 4,
|
||||
BM25_BIND_INNER = 5,
|
||||
BM25_BIND_FILE = 6,
|
||||
BM25_BIND_LABEL = 7,
|
||||
BM25_BIND_EXACT = 8,
|
||||
BM25_SQL_AUTO_LEN = -1,
|
||||
/* Inner FTS5 candidate cap. SQLite can early-terminate a plain FTS5 query
|
||||
* (no JOIN/WHERE on outer table) of the form:
|
||||
@@ -4019,8 +4021,8 @@ static char *bm25_render(const bm25_output_row_t *rows, int returned, int total,
|
||||
* Returns NULL if FTS5 is unavailable or the query produced no usable tokens,
|
||||
* in which case the caller falls back to the regex-based search path. */
|
||||
static char *bm25_search(cbm_store_t *store, const char *project, const char *query,
|
||||
const char *file_pattern, int limit, int offset, bool tree_format,
|
||||
size_t max_output_bytes) {
|
||||
const char *file_pattern, const char *label, int limit, int offset,
|
||||
bool tree_format, size_t max_output_bytes) {
|
||||
sqlite3 *db = cbm_store_get_db(store);
|
||||
if (!db) {
|
||||
return NULL;
|
||||
@@ -4046,9 +4048,18 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
* because no outer predicate blocks it. We fetch BM25_INNER_LIMIT top candidates
|
||||
* from the FTS5 index, then join/filter/boost only those rows. bm25() returns a
|
||||
* NEGATIVE score (lower = more relevant). */
|
||||
/* Exact-name tier (2026-09-16 probe): a definition whose NAME is the query
|
||||
* outranks every partial hit. BM25 term frequency otherwise rewards a long
|
||||
* test-method name that repeats the token — `table references table with
|
||||
* same name` — over the `Table` class itself, and the label tiers below
|
||||
* then push the class's own methods above it (Method 10 > Class 5). The
|
||||
* definition the reader asked for by name comes first; case-insensitive
|
||||
* exact spelling comes next; everything else keeps its BM25 order. */
|
||||
const char *sql =
|
||||
"SELECT n.id, n.label, n.name, n.qualified_name, n.file_path, n.start_line, n.end_line, "
|
||||
" (fts.base_rank "
|
||||
" - CASE WHEN n.name = ?8 THEN 30.0 "
|
||||
" WHEN lower(n.name) = lower(?8) THEN 20.0 ELSE 0.0 END "
|
||||
" - CASE WHEN n.label IN ('Function','Method') THEN 10.0 "
|
||||
" WHEN n.label = 'Route' THEN 8.0 "
|
||||
" WHEN n.label IN (" CBM_SQL_TYPE_LIKE_LABELS ") THEN 5.0 "
|
||||
@@ -4070,6 +4081,10 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
* must be changed together or results desynchronise from counts. */
|
||||
" AND n.label NOT IN ('File','Folder','Variable','Project') "
|
||||
" AND (?6 IS NULL OR n.file_path LIKE ?6) "
|
||||
/* The caller's label filter applies in query mode exactly as it does
|
||||
* in the structural mode (2026-09-16 probe: `label=Class` was ignored
|
||||
* here and five Methods came back). MIRRORED in the count query. */
|
||||
" AND (?7 IS NULL OR n.label = ?7) "
|
||||
/* rank ties are common (boosted floats) — the id tie-break makes
|
||||
* offset pages contractually stable across calls. */
|
||||
"ORDER BY rank, n.id "
|
||||
@@ -4090,6 +4105,12 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, BM25_BIND_FILE);
|
||||
}
|
||||
if (label && label[0]) {
|
||||
sqlite3_bind_text(stmt, BM25_BIND_LABEL, label, BM25_SQL_AUTO_LEN, MCP_SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(stmt, BM25_BIND_LABEL);
|
||||
}
|
||||
sqlite3_bind_text(stmt, BM25_BIND_EXACT, query, BM25_SQL_AUTO_LEN, MCP_SQLITE_TRANSIENT);
|
||||
|
||||
/* Count hits within the same inner-limit window — capped at BM25_INNER_LIMIT.
|
||||
* Uses the identical subquery structure so the FTS5 early-exit applies here too. */
|
||||
@@ -4107,6 +4128,7 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
* not describe the rows returned. */
|
||||
" AND n.label NOT IN ('File','Folder','Variable','Project')"
|
||||
" AND (?6 IS NULL OR n.file_path LIKE ?6)"
|
||||
" AND (?7 IS NULL OR n.label = ?7)"
|
||||
")";
|
||||
sqlite3_stmt *cs = NULL;
|
||||
if (sqlite3_prepare_v2(db, count_sql, BM25_SQL_AUTO_LEN, &cs, NULL) == SQLITE_OK) {
|
||||
@@ -4121,6 +4143,12 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
} else {
|
||||
sqlite3_bind_null(cs, BM25_BIND_FILE);
|
||||
}
|
||||
if (label && label[0]) {
|
||||
sqlite3_bind_text(cs, BM25_BIND_LABEL, label, BM25_SQL_AUTO_LEN,
|
||||
MCP_SQLITE_TRANSIENT);
|
||||
} else {
|
||||
sqlite3_bind_null(cs, BM25_BIND_LABEL);
|
||||
}
|
||||
if (sqlite3_step(cs) == SQLITE_ROW) {
|
||||
total = sqlite3_column_int(cs, 0);
|
||||
}
|
||||
@@ -4153,10 +4181,10 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
|
||||
int row_count = 0;
|
||||
while (rows && row_count < row_cap && sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
const char *qn = (const char *)sqlite3_column_text(stmt, BM25_COL_QN);
|
||||
const char *label = (const char *)sqlite3_column_text(stmt, BM25_COL_LABEL);
|
||||
const char *row_label = (const char *)sqlite3_column_text(stmt, BM25_COL_LABEL);
|
||||
const char *file = (const char *)sqlite3_column_text(stmt, BM25_COL_FILE);
|
||||
rows[row_count].qualified_name = heap_strdup(qn ? qn : "");
|
||||
rows[row_count].label = heap_strdup(label ? label : "");
|
||||
rows[row_count].label = heap_strdup(row_label ? row_label : "");
|
||||
rows[row_count].file_path = heap_strdup(file ? file : "");
|
||||
rows[row_count].start_line = sqlite3_column_int(stmt, BM25_COL_START);
|
||||
rows[row_count].end_line = sqlite3_column_int(stmt, BM25_COL_END);
|
||||
@@ -5089,9 +5117,11 @@ static char *handle_search_graph(cbm_mcp_server_t *srv, const char *args) {
|
||||
}
|
||||
if (query && query[0]) {
|
||||
char *q_file_pattern = cbm_mcp_get_string_arg(args, "file_pattern");
|
||||
char *bm25_json = bm25_search(store, project, query, q_file_pattern, limit, offset,
|
||||
char *q_label = cbm_mcp_get_string_arg(args, "label");
|
||||
char *bm25_json = bm25_search(store, project, query, q_file_pattern, q_label, limit, offset,
|
||||
!json_format, max_output_bytes);
|
||||
free(q_file_pattern);
|
||||
free(q_label);
|
||||
if (bm25_json) {
|
||||
free(query);
|
||||
free(project);
|
||||
|
||||
@@ -626,15 +626,29 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
|
||||
* reintroduce the #592/#606 false-edge class for .ets files. */
|
||||
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
|
||||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
|
||||
lang == CBM_LANG_ARKTS;
|
||||
lang == CBM_LANG_ARKTS ||
|
||||
/* Files whose calls are embedded JS/TS (<script>
|
||||
* bodies): the calls carry the JS receiver flag
|
||||
* but the FILE language gated them out, so
|
||||
* generated Dokka pages bound localStorage.getItem
|
||||
* to a docs bundle (2026-09-16 probe: 4,207 junk
|
||||
* edges on JetBrains/Exposed). */
|
||||
lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
|
||||
/* 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;
|
||||
/* The member guard's one exemption (Python, self/cls-rooted receiver,
|
||||
* unique_name, not a builtin type's method) — see
|
||||
* cbm_weak_member_unique_name_exempt. MUST match pass_parallel.c exactly. */
|
||||
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_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
|
||||
call->receiver_is_self_attribute, call->callee_name,
|
||||
res.strategy)) ||
|
||||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
|
||||
call->callee_is_locally_bound, res.strategy);
|
||||
|
||||
|
||||
@@ -1874,6 +1874,10 @@ typedef struct {
|
||||
static void sanitize_expr(char *expr_buf, const char *expr) {
|
||||
if (expr) {
|
||||
snprintf(expr_buf, 128, "%.*s", 120, expr);
|
||||
/* The 120-byte cut can land inside a multibyte character; a torn
|
||||
* sequence persisted as invalid UTF-8 in edge properties (2026-09-16
|
||||
* probe: rust, java, typescript stores). */
|
||||
cbm_utf8_trim_partial(expr_buf);
|
||||
for (char *p = expr_buf; *p; p++) {
|
||||
if (*p == '"') {
|
||||
*p = '\'';
|
||||
@@ -1960,10 +1964,18 @@ static bool is_path_keyword(const char *keyword) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* A route path is one line that opens with a slash. A block or line comment
|
||||
* opens with a slash too, and an argument list that starts with one used to
|
||||
* hand the comment text to the Route pass (three Java block comments became
|
||||
* Route nodes on elasticsearch, 2026-09-16). */
|
||||
static bool is_route_path_shaped(const char *val) {
|
||||
return val && val[0] == '/' && !cbm_service_pattern_is_comment_text(val);
|
||||
}
|
||||
|
||||
static const char *find_route_path_in_args(const CBMCall *call, const char **out_handler) {
|
||||
*out_handler = NULL;
|
||||
/* 1. First string arg starting with / */
|
||||
if (call->first_string_arg && call->first_string_arg[0] == '/') {
|
||||
if (is_route_path_shaped(call->first_string_arg)) {
|
||||
*out_handler = call->second_arg_name;
|
||||
return call->first_string_arg;
|
||||
}
|
||||
@@ -1972,7 +1984,7 @@ static const char *find_route_path_in_args(const CBMCall *call, const char **out
|
||||
for (int ai = 0; ai < call->arg_count && !found; ai++) {
|
||||
const CBMCallArg *ca = &call->args[ai];
|
||||
const char *val = ca->value ? ca->value : ca->expr;
|
||||
if (!val || val[0] != '/') {
|
||||
if (!is_route_path_shaped(val)) {
|
||||
continue;
|
||||
}
|
||||
if ((ca->keyword && is_path_keyword(ca->keyword)) || (!ca->keyword && ca->index == 0)) {
|
||||
@@ -2931,12 +2943,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
|
||||
* note there. ArkTS belongs to the JS/TS family (#1842). */
|
||||
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
|
||||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
|
||||
lang == CBM_LANG_ARKTS;
|
||||
lang == CBM_LANG_ARKTS ||
|
||||
/* embedded-script hosts — see pass_calls.c */
|
||||
lang == CBM_LANG_HTML || lang == CBM_LANG_VUE ||
|
||||
lang == CBM_LANG_SVELTE || lang == CBM_LANG_ASTRO;
|
||||
/* 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;
|
||||
/* The member guard's one exemption — MUST match pass_calls.c exactly. */
|
||||
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_weak_member_unique_name_exempt(lang == CBM_LANG_PYTHON,
|
||||
call->receiver_is_self_attribute,
|
||||
call->callee_name, res.strategy)) ||
|
||||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
|
||||
call->callee_is_locally_bound, res.strategy);
|
||||
|
||||
|
||||
@@ -1599,6 +1599,49 @@ static bool is_c_family_source(const char *source_rel) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Directory depth of a repo-relative path: how many directories sit above it. */
|
||||
static int include_path_depth(const char *path) {
|
||||
int depth = 0;
|
||||
for (const char *p = path; *p; p++) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
/* Total order among nodes whose file path ends with the include path. The
|
||||
* by-name hits arrive in node-registration order, which under parallel
|
||||
* extraction is the workers' merge order and differs run to run; taking the
|
||||
* first hit made `#include <linux/device.h>` target include/linux/device.h in
|
||||
* one index of the kernel and tools/virtio/linux/device.h in the next (5,821
|
||||
* IMPORTS edges moved, and every CALLS edge resolved through those files'
|
||||
* import maps moved with them). The include names a file, so a File node
|
||||
* outranks a symbol declared in it; among files the least nested path wins
|
||||
* (include/ over tools/virtio/), then the smaller path, then the smaller QN
|
||||
* — a function of the candidate set alone (O9). */
|
||||
static bool include_target_outranks(const cbm_gbuf_node_t *cand, const cbm_gbuf_node_t *best) {
|
||||
if (!best) {
|
||||
return true;
|
||||
}
|
||||
bool cand_file = strcmp(cand->label, "File") == 0;
|
||||
bool best_file = strcmp(best->label, "File") == 0;
|
||||
if (cand_file != best_file) {
|
||||
return cand_file;
|
||||
}
|
||||
int cd = include_path_depth(cand->file_path);
|
||||
int bd = include_path_depth(best->file_path);
|
||||
if (cd != bd) {
|
||||
return cd < bd;
|
||||
}
|
||||
int by_path = strcmp(cand->file_path, best->file_path);
|
||||
if (by_path != 0) {
|
||||
return by_path < 0;
|
||||
}
|
||||
return cand->qualified_name && best->qualified_name &&
|
||||
strcmp(cand->qualified_name, best->qualified_name) < 0;
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_exact_file_node(const cbm_pipeline_ctx_t *ctx,
|
||||
const char *file_path,
|
||||
const char *source_file_qn) {
|
||||
@@ -1651,18 +1694,12 @@ static const cbm_gbuf_node_t *resolve_exact_file_node(const cbm_pipeline_ctx_t *
|
||||
strcmp(cand->qualified_name, source_file_qn) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (strcmp(cand->label, "File") == 0) {
|
||||
return cand;
|
||||
}
|
||||
if (!best) {
|
||||
if (include_target_outranks(cand, best)) {
|
||||
best = cand;
|
||||
}
|
||||
}
|
||||
if (best) {
|
||||
return best;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
return best;
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *ctx,
|
||||
|
||||
@@ -294,6 +294,20 @@ 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);
|
||||
|
||||
/* True if `name` is a method of a Python builtin type (str/bytes/list/dict/set/
|
||||
* file) or a builtin function seen as an attribute call. A language fact, kept
|
||||
* as a sorted table like the Perl builtins. */
|
||||
bool cbm_python_is_builtin_member(const char *name);
|
||||
|
||||
/* The member guard's exemption: a Python member call whose receiver is an
|
||||
* attribute chain rooted at self/cls (an object the class owns), whose callee has
|
||||
* exactly one project definition (strategy unique_name) and is not a builtin
|
||||
* type's own method keeps its edge. Combine at the call sites as
|
||||
* `suppress && !exempt`; both pass_calls.c and pass_parallel.c must do the same.
|
||||
* Pure; unit-tested in test_registry.c. */
|
||||
bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_attribute,
|
||||
const char *callee_name, 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
|
||||
|
||||
+150
-13
@@ -229,6 +229,37 @@ static int candidate_score(const char *candidate_qn, const char *module_qn, int
|
||||
return score;
|
||||
}
|
||||
|
||||
/* Number of '.'-separated segments in a QN: how deeply the definition is
|
||||
* nested (file depth + enclosing types). */
|
||||
static int qn_depth(const char *qn) {
|
||||
int depth = 1;
|
||||
for (const char *p = qn; *p; p++) {
|
||||
if (*p == '.') {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
/* Total order among candidates that tie on candidate_score. The registry's
|
||||
* by-name bucket is in registration order, which follows the file list; a
|
||||
* tie broken by bucket position made the resolved target a function of
|
||||
* that order (kernel: 632 CALLS edges differed between two indexes of the
|
||||
* same tree, `dev_name` flipping between twenty same-named struct fields
|
||||
* and unresolved, `sg_set_buf` between include/linux and tools/virtio).
|
||||
* The rule: the least nested definition wins (a top-level function over a
|
||||
* same-named member two types deep, include/linux over tools/virtio/linux),
|
||||
* then the lexicographically smaller QN — a pure function of the candidate
|
||||
* set, never of the order it was built in (O9). */
|
||||
static bool candidate_outranks_on_tie(const char *candidate, const char *best) {
|
||||
int cd = qn_depth(candidate);
|
||||
int bd = qn_depth(best);
|
||||
if (cd != bd) {
|
||||
return cd < bd;
|
||||
}
|
||||
return strcmp(candidate, best) < 0;
|
||||
}
|
||||
|
||||
/* Pick candidate with highest composite score (test-deprioritization + namespace
|
||||
* proximity). `is_test_flags` is the candidates' cached test verdicts, or NULL
|
||||
* when the caller has none (a filtered subset carries its own copy). */
|
||||
@@ -239,7 +270,8 @@ static const char *best_by_import_distance(const char **candidates, const uint8_
|
||||
for (int i = 0; i < count; i++) {
|
||||
int score =
|
||||
candidate_score(candidates[i], module_qn, is_test_flags ? (int)is_test_flags[i] : -1);
|
||||
if (score > best_score) {
|
||||
if (score > best_score ||
|
||||
(score == best_score && best && candidate_outranks_on_tie(candidates[i], best))) {
|
||||
best_score = score;
|
||||
best = candidates[i];
|
||||
}
|
||||
@@ -559,6 +591,84 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
|
||||
return weak_short_name_strategy(strategy);
|
||||
}
|
||||
|
||||
/* ── Python builtin-type members ─────────────────────────────────────────
|
||||
* Methods of Python's builtin types (str, bytes, list, dict, set, file objects)
|
||||
* plus the builtin functions that appear as attribute calls. Like PERL_BUILTINS
|
||||
* this is a fact about the LANGUAGE, not about corpus fashion: a member call
|
||||
* whose name is a builtin's own method — `parts.extend(...)`, `line.strip()`,
|
||||
* `d.items()` — is that builtin's method whenever the receiver is untyped, so a
|
||||
* project symbol that merely shares the spelling must not bind. MUST stay
|
||||
* sorted ASCII-ascending for bsearch. */
|
||||
static const char *const PYTHON_BUILTIN_MEMBERS[] = {
|
||||
"add", "append", "capitalize",
|
||||
"casefold", "center", "clear",
|
||||
"close", "copy", "count",
|
||||
"decode", "difference", "discard",
|
||||
"encode", "endswith", "expandtabs",
|
||||
"extend", "fileno", "find",
|
||||
"flush", "format", "get",
|
||||
"index", "insert", "intersection",
|
||||
"isalnum", "isalpha", "isdecimal",
|
||||
"isdigit", "isidentifier", "islower",
|
||||
"isnumeric", "isprintable", "isspace",
|
||||
"istitle", "isupper", "issubset",
|
||||
"issuperset", "items", "join",
|
||||
"keys", "ljust", "lower",
|
||||
"lstrip", "partition", "pop",
|
||||
"popitem", "print", "read",
|
||||
"readable", "readline", "readlines",
|
||||
"remove", "replace", "reverse",
|
||||
"rfind", "rindex", "rjust",
|
||||
"rpartition", "rsplit", "rstrip",
|
||||
"seek", "setdefault", "sort",
|
||||
"split", "splitlines", "startswith",
|
||||
"strip", "swapcase", "symmetric_difference",
|
||||
"tell", "title", "truncate",
|
||||
"union", "update", "upper",
|
||||
"values", "writable", "write",
|
||||
"writelines", "zfill",
|
||||
};
|
||||
|
||||
static int python_builtin_member_cmp(const void *key, const void *elem) {
|
||||
return strcmp((const char *)key, *(const char *const *)elem);
|
||||
}
|
||||
|
||||
bool cbm_python_is_builtin_member(const char *name) {
|
||||
if (!name || !name[0]) {
|
||||
return false;
|
||||
}
|
||||
return bsearch(name, PYTHON_BUILTIN_MEMBERS,
|
||||
sizeof(PYTHON_BUILTIN_MEMBERS) / sizeof(PYTHON_BUILTIN_MEMBERS[0]),
|
||||
sizeof(PYTHON_BUILTIN_MEMBERS[0]), python_builtin_member_cmp) != NULL;
|
||||
}
|
||||
|
||||
/* The member guard's one exemption. A Python member call whose receiver could
|
||||
* not be typed still carries real evidence when three facts line up: the callee
|
||||
* name has exactly ONE definition in the whole project (strategy unique_name),
|
||||
* the receiver is an attribute chain rooted at self/cls — an object the class
|
||||
* owns, not a parameter handed in — and the name is not a builtin type's own
|
||||
* method. `self.compiler.apply_converters()` names the only apply_converters in
|
||||
* django and nothing else it could mean exists. Measured on django/django
|
||||
* (v0.10.8 → v0.11.0 A/B, 2026-09-16): the unconditional guard dropped 800 such
|
||||
* self-rooted edges, every sampled one a real call in the source. A bare
|
||||
* parameter receiver (`accelerator.backward()`, #1276's own case) carries no
|
||||
* ownership evidence and stays suppressed, as does every builtin-member name
|
||||
* (`self.parts.extend()`). Python only: the JS/TS family keeps the TS-LSP trade
|
||||
* recorded in the resolution probe. Pure; unit-tested in test_registry.c. */
|
||||
bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_attribute,
|
||||
const char *callee_name, const char *strategy) {
|
||||
if (!is_python || !receiver_is_self_attribute) {
|
||||
return false;
|
||||
}
|
||||
if (!strategy || strcmp(strategy, "unique_name") != 0) {
|
||||
return false;
|
||||
}
|
||||
if (!callee_name || !callee_name[0]) {
|
||||
return false;
|
||||
}
|
||||
return !cbm_python_is_builtin_member(simple_name(callee_name));
|
||||
}
|
||||
|
||||
/* 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
|
||||
@@ -611,13 +721,30 @@ static const char *path_basename(const char *path) {
|
||||
return slash ? slash + 1 : path;
|
||||
}
|
||||
|
||||
/* Build and configuration languages have no cross-language call semantics: a
|
||||
* Makefile's `$(eval ...)` or a CMake `function(...)` names nothing in a C
|
||||
* file, so a bare-name bind into another language is always a collision
|
||||
* (2026-09-16 probe: kernel Makefile targets bound to `sk_psock.eval`). */
|
||||
static bool build_config_language(CBMLanguage lang) {
|
||||
return lang == CBM_LANG_MAKEFILE || lang == CBM_LANG_CMAKE || lang == CBM_LANG_YAML ||
|
||||
lang == CBM_LANG_TOML || lang == CBM_LANG_JSON || lang == CBM_LANG_INI ||
|
||||
lang == CBM_LANG_DOCKERFILE;
|
||||
}
|
||||
|
||||
bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const char *target_file_path,
|
||||
const char *strategy) {
|
||||
/* Two same-named symbols in different languages: suffix_match picks one
|
||||
* winner by import-distance and attaches every bare-name call to it
|
||||
* (#725, Bash/Python main, JS/Python commit). unique_name is the
|
||||
* candidates==1 case (#1572) and is not this guard. */
|
||||
if (!strategy || strcmp(strategy, "suffix_match") != 0) {
|
||||
* candidates==1 case (#1572) and is not this guard — except for a build
|
||||
* or configuration caller, where even a unique match into another
|
||||
* language is a collision by construction. */
|
||||
if (!strategy) {
|
||||
return false;
|
||||
}
|
||||
bool config_caller = build_config_language(caller_lang);
|
||||
if (strcmp(strategy, "suffix_match") != 0 &&
|
||||
!(config_caller && strcmp(strategy, "unique_name") == 0)) {
|
||||
return false;
|
||||
}
|
||||
if (caller_lang == CBM_LANG_COUNT || !target_file_path || !target_file_path[0]) {
|
||||
@@ -1088,27 +1215,37 @@ static bool receiver_chain_admits(const char *callee_name, const char *candidate
|
||||
return true;
|
||||
}
|
||||
|
||||
/* The candidate's parent segment: the one before its final name. */
|
||||
/* The candidate's ancestry: every segment before its final name. */
|
||||
const char *cand_last = strrchr(candidate_qn, '.');
|
||||
if (!cand_last || cand_last == candidate_qn) {
|
||||
return true; /* top-level candidate — no parent to look for */
|
||||
return true; /* top-level candidate — no ancestry to look for */
|
||||
}
|
||||
const char *parent = cand_last;
|
||||
while (parent > candidate_qn && parent[-1] != '.') {
|
||||
parent--;
|
||||
}
|
||||
size_t parent_len = (size_t)(cand_last - parent);
|
||||
|
||||
/* Walk the chain — every segment before the final callee name. A trailing
|
||||
* "()" is dropped so JSONEncoder().encode reads as JSONEncoder. */
|
||||
* "()" is dropped so JSONEncoder().encode reads as JSONEncoder. A chain
|
||||
* segment admits the candidate when it names ANY segment of the candidate's
|
||||
* ancestry, not only the immediate parent: Settings.builder().putList binds
|
||||
* settings.Settings.Builder.putList because Settings owns Builder, and
|
||||
* RequestOptions.DEFAULT.toBuilder().setWarningsHandler binds
|
||||
* RequestOptions.Builder.setWarningsHandler the same way. Measured on
|
||||
* elastic/elasticsearch: the parent-only rule refused 9,473 unique-name
|
||||
* calls, most of them exactly these factory chains into a nested Builder.
|
||||
* A foreign root still finds nothing in the ancestry — Base64.getEncoder
|
||||
* does not admit DocOffsetsCodec.getEncoder, URLSession.shared.data does
|
||||
* not admit PickedFile.data — so the #1893 refusals hold. */
|
||||
for (const char *seg = dotted; seg < last_dot;) {
|
||||
const char *end = strchr(seg, '.');
|
||||
size_t len = (size_t)(end - seg);
|
||||
if (len >= 2 && seg[len - 2] == '(' && seg[len - 1] == ')') {
|
||||
len -= 2; /* an empty "()" — JSONEncoder().encode names JSONEncoder */
|
||||
}
|
||||
if (len == parent_len && strncmp(seg, parent, parent_len) == 0) {
|
||||
return true;
|
||||
for (const char *anc = candidate_qn; anc < cand_last;) {
|
||||
const char *anc_end = strchr(anc, '.');
|
||||
size_t anc_len = (size_t)(anc_end - anc);
|
||||
if (anc_len == len && len > 0 && strncmp(seg, anc, len) == 0) {
|
||||
return true;
|
||||
}
|
||||
anc = anc_end + SKIP_ONE;
|
||||
}
|
||||
seg = end + SKIP_ONE;
|
||||
}
|
||||
|
||||
+49
-26
@@ -1837,8 +1837,7 @@ static bool cli_scope_fixture_start(cli_scope_fixture_t *fixture, const char *ta
|
||||
return fixture->client != NULL;
|
||||
}
|
||||
|
||||
static bool cli_scope_host_serving_within(const cli_scope_fixture_t *fixture,
|
||||
uint32_t timeout_ms) {
|
||||
static bool cli_scope_host_serving_within(const cli_scope_fixture_t *fixture, uint32_t timeout_ms) {
|
||||
cbm_daemon_runtime_status_t status = {0};
|
||||
return fixture->endpoint &&
|
||||
cbm_daemon_runtime_request_status(fixture->endpoint, &fixture->identity, timeout_ms,
|
||||
@@ -1994,8 +1993,8 @@ TEST(cli_install_into_host_namespace_still_drains_host_cohort) {
|
||||
: -1;
|
||||
/* Negative probe: see CLI_SCOPE_HOST_DRAINED_PROBE_MS. The drain itself is
|
||||
* asserted positively below via host_exit == CLI_SCOPE_HOST_DRAINED. */
|
||||
bool host_serving = ready && cli_scope_host_serving_within(&fixture,
|
||||
CLI_SCOPE_HOST_DRAINED_PROBE_MS);
|
||||
bool host_serving =
|
||||
ready && cli_scope_host_serving_within(&fixture, CLI_SCOPE_HOST_DRAINED_PROBE_MS);
|
||||
const char *events = read_test_file(activation_log);
|
||||
bool drained_in_log = events && strstr(events, "cohort drained") != NULL &&
|
||||
strstr(events, "\"daemon_active_clients\":1") != NULL;
|
||||
@@ -9384,8 +9383,8 @@ TEST(cli_codex_respects_codex_home) {
|
||||
snprintf(expected_config, sizeof(expected_config), "%s/config.toml", codex_home);
|
||||
yyjson_doc *plan_doc = json ? yyjson_read(json, strlen(json), 0) : NULL;
|
||||
yyjson_val *plan_root = plan_doc ? yyjson_doc_get_root(plan_doc) : NULL;
|
||||
bool plans_config = test_json_string_array_contains(plan_root, "config_files_planned",
|
||||
expected_config);
|
||||
bool plans_config =
|
||||
test_json_string_array_contains(plan_root, "config_files_planned", expected_config);
|
||||
bool plans_instructions = test_json_string_array_contains(
|
||||
plan_root, "instruction_files_planned", expected_instructions);
|
||||
bool plans_cleanup = json && strstr(json, "remove_managed_block_if_present") != NULL;
|
||||
@@ -9441,8 +9440,8 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
|
||||
struct stat state;
|
||||
int fresh_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false);
|
||||
char *fresh_agents = read_test_file_alloc(agents_path);
|
||||
bool fresh_pointer_installed = fresh_rc == 0 && fresh_agents &&
|
||||
strcmp(fresh_agents, test_codex_activation_block) == 0;
|
||||
bool fresh_pointer_installed =
|
||||
fresh_rc == 0 && fresh_agents && strcmp(fresh_agents, test_codex_activation_block) == 0;
|
||||
char *config = read_test_file_alloc(config_path);
|
||||
bool other_surfaces_installed =
|
||||
fresh_rc == 0 && config && strstr(config, "[mcp_servers.codebase-memory-mcp]") &&
|
||||
@@ -9463,8 +9462,7 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
|
||||
bool unowned_preserved =
|
||||
user_pointer_written > 0 && (size_t)user_pointer_written < sizeof(expected_user_pointer) &&
|
||||
dry_rc == 0 && unowned_rc == 0 && after_dry && after_unowned &&
|
||||
strcmp(after_dry, user_only) == 0 &&
|
||||
strcmp(after_unowned, expected_user_pointer) == 0;
|
||||
strcmp(after_dry, user_only) == 0 && strcmp(after_unowned, expected_user_pointer) == 0;
|
||||
free(after_dry);
|
||||
free(after_unowned);
|
||||
|
||||
@@ -9509,11 +9507,10 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
|
||||
int malformed_rc = cbm_install_agent_configs(tmpdir, binary_path, false, false);
|
||||
char *after_malformed = read_test_file_alloc(agents_path);
|
||||
config = read_test_file_alloc(config_path);
|
||||
bool malformed_preserved = malformed_rc != 0 && after_malformed &&
|
||||
strcmp(after_malformed, malformed) == 0 && config &&
|
||||
strstr(config, "[mcp_servers.codebase-memory-mcp]") &&
|
||||
strstr(config, "SessionStart") && stat(skill_path, &state) == 0 &&
|
||||
stat(profile_path, &state) == 0;
|
||||
bool malformed_preserved =
|
||||
malformed_rc != 0 && after_malformed && strcmp(after_malformed, malformed) == 0 && config &&
|
||||
strstr(config, "[mcp_servers.codebase-memory-mcp]") && strstr(config, "SessionStart") &&
|
||||
stat(skill_path, &state) == 0 && stat(profile_path, &state) == 0;
|
||||
free(config);
|
||||
free(after_malformed);
|
||||
|
||||
@@ -9521,8 +9518,8 @@ TEST(cli_codex_install_uses_global_activation_pointer_issue1689) {
|
||||
char *uninstall_argv[] = {"uninstall", "--yes"};
|
||||
int uninstall_rc = cli_test_cmd_uninstall(2, uninstall_argv);
|
||||
char *after_uninstall = read_test_file_alloc(agents_path);
|
||||
bool uninstall_preserved_foreign = uninstall_rc == 0 && after_uninstall &&
|
||||
strcmp(after_uninstall, user_only) == 0;
|
||||
bool uninstall_preserved_foreign =
|
||||
uninstall_rc == 0 && after_uninstall && strcmp(after_uninstall, user_only) == 0;
|
||||
free(after_uninstall);
|
||||
|
||||
restore_test_env("HOME", saved_home);
|
||||
@@ -11803,8 +11800,7 @@ TEST(cli_codex_migrates_to_single_hook_representation) {
|
||||
restore_test_env("PATH", saved_path);
|
||||
restore_test_env("CODEX_HOME", saved_codex);
|
||||
test_rmdir_r(tmpdir);
|
||||
if (!lifecycle_ok || !migrated || !independent_cleanup ||
|
||||
!pointer_only_on_preflight_failure)
|
||||
if (!lifecycle_ok || !migrated || !independent_cleanup || !pointer_only_on_preflight_failure)
|
||||
FAIL("Codex lifecycle preflight must be idempotent, preserve the activation pointer "
|
||||
"contract, and independently clean owned side files");
|
||||
PASS();
|
||||
@@ -11844,8 +11840,7 @@ TEST(cli_codex_pointer_migration_precedes_hook_preflight_issue1689) {
|
||||
test_rmdir_r(tmpdir);
|
||||
FAIL("failed to build expected Codex pointer migration");
|
||||
}
|
||||
if (write_test_file(config_path, ambiguous) != 0 ||
|
||||
write_test_file(agents_path, legacy) != 0) {
|
||||
if (write_test_file(config_path, ambiguous) != 0 || write_test_file(agents_path, legacy) != 0) {
|
||||
test_rmdir_r(tmpdir);
|
||||
FAIL("failed to write Codex cleanup preflight fixture");
|
||||
}
|
||||
@@ -11868,14 +11863,13 @@ TEST(cli_codex_pointer_migration_precedes_hook_preflight_issue1689) {
|
||||
strcmp(config_after_plan, ambiguous) == 0 &&
|
||||
strcmp(agents_after_plan, legacy) == 0;
|
||||
|
||||
int install_rc =
|
||||
cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
|
||||
int install_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
|
||||
char *config_after_install = read_test_file_alloc(config_path);
|
||||
char *agents_after_install = read_test_file_alloc(agents_path);
|
||||
struct stat state;
|
||||
bool preflight_failed_closed =
|
||||
install_rc != 0 && config_after_install && strcmp(config_after_install, ambiguous) == 0 &&
|
||||
stat(skill_path, &state) != 0 && stat(profile_path, &state) != 0;
|
||||
bool preflight_failed_closed = install_rc != 0 && config_after_install &&
|
||||
strcmp(config_after_install, ambiguous) == 0 &&
|
||||
stat(skill_path, &state) != 0 && stat(profile_path, &state) != 0;
|
||||
bool pointer_migrated =
|
||||
agents_after_install && strcmp(agents_after_install, legacy_migrated) == 0;
|
||||
|
||||
@@ -15206,6 +15200,34 @@ TEST(cli_build_args_json_bare_boolean_issue680) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An array-typed flag given a JSON array literal — the shape the help's
|
||||
* `<array>` invites — contributes the literal's elements, not one element
|
||||
* holding the literal text (2026-09-16 probe: check_index_coverage reported
|
||||
* the fake path `["lib","t"]`). Repeated plain values still accumulate. */
|
||||
TEST(cli_build_args_json_array_flag_accepts_json_literal) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--project", "p", "--paths", "[\"lib\",\"t\"]"};
|
||||
char *json = cbm_cli_build_args_json("check_index_coverage", 4, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT_NULL(err);
|
||||
ASSERT(strstr(json, "\"paths\":[\"lib\",\"t\"]") != NULL);
|
||||
free(json);
|
||||
|
||||
char *argv2[] = {"--project", "p", "--paths", "lib", "--paths", "t"};
|
||||
json = cbm_cli_build_args_json("check_index_coverage", 6, argv2, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"paths\":[\"lib\",\"t\"]") != NULL);
|
||||
free(json);
|
||||
|
||||
/* A value that merely starts with '[' but is not JSON stays one element. */
|
||||
char *argv3[] = {"--project", "p", "--paths", "[weird"};
|
||||
json = cbm_cli_build_args_json("check_index_coverage", 4, argv3, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"paths\":[\"[weird\"]") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An unknown flag for a KNOWN tool must be rejected loudly, not silently
|
||||
* typed as a string and dropped server-side (#997). GF1 eval: `trace_path
|
||||
* --max-depth 1` was accepted, the real --depth stayed at default 3, and
|
||||
@@ -16347,6 +16369,7 @@ SUITE(cli) {
|
||||
RUN_TEST(cli_build_args_json_string_flag_issue680);
|
||||
RUN_TEST(cli_build_args_json_integer_flag_issue680);
|
||||
RUN_TEST(cli_build_args_json_bare_boolean_issue680);
|
||||
RUN_TEST(cli_build_args_json_array_flag_accepts_json_literal);
|
||||
RUN_TEST(cli_build_args_json_unknown_flag_rejected);
|
||||
RUN_TEST(cli_build_args_json_repeated_array_issue680);
|
||||
RUN_TEST(cli_build_args_json_kebab_to_snake_issue680);
|
||||
|
||||
@@ -4468,6 +4468,63 @@ TEST(cypher_exec_prop_array_with_internal_commas) {
|
||||
|
||||
/* A string property must not end at an ESCAPED quote: the scan stopped at the
|
||||
* first '"' regardless of a preceding backslash, cutting the value short. */
|
||||
/* Planner (2026-09-16): a single-hop pattern whose FAR node carries the
|
||||
* selective filter is walked from that end. `MATCH (a)-[:CALLS]->(b) WHERE
|
||||
* b.name = 'X'` used to scan every node before the filter on b could act — on
|
||||
* the 8.5 M node kernel graph it hit the execution-time limit while the
|
||||
* anchored spelling answered in seconds. The rows must be the same in every
|
||||
* spelling, including the inline-property and inbound-direction forms, and
|
||||
* the caller-side variable must still bind the right nodes after the swap. */
|
||||
TEST(cypher_single_hop_seeds_from_selective_far_node) {
|
||||
cbm_store_t *s = setup_cypher_store();
|
||||
|
||||
cbm_cypher_result_t where_form = {0};
|
||||
ASSERT_EQ(cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE b.name = 'ValidateOrder' "
|
||||
"RETURN a.name ORDER BY a.name",
|
||||
"test", 0, &where_form),
|
||||
0);
|
||||
ASSERT_EQ(where_form.row_count, 1);
|
||||
ASSERT_STR_EQ(where_form.rows[0][0], "HandleOrder");
|
||||
cbm_cypher_result_free(&where_form);
|
||||
|
||||
cbm_cypher_result_t inline_form = {0};
|
||||
ASSERT_EQ(cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b {name: 'SubmitOrder'}) "
|
||||
"RETURN a.name",
|
||||
"test", 0, &inline_form),
|
||||
0);
|
||||
ASSERT_EQ(inline_form.row_count, 1);
|
||||
ASSERT_STR_EQ(inline_form.rows[0][0], "ValidateOrder");
|
||||
cbm_cypher_result_free(&inline_form);
|
||||
|
||||
/* Inbound spelling: the far node is now the CALLER; direction inverts back. */
|
||||
cbm_cypher_result_t inbound_form = {0};
|
||||
ASSERT_EQ(cbm_cypher_execute(s,
|
||||
"MATCH (callee)<-[:CALLS]-(caller) WHERE caller.name = "
|
||||
"'HandleOrder' RETURN callee.name ORDER BY callee.name",
|
||||
"test", 0, &inbound_form),
|
||||
0);
|
||||
ASSERT_EQ(inbound_form.row_count, 2);
|
||||
ASSERT_STR_EQ(inbound_form.rows[0][0], "LogError");
|
||||
ASSERT_STR_EQ(inbound_form.rows[1][0], "ValidateOrder");
|
||||
cbm_cypher_result_free(&inbound_form);
|
||||
|
||||
/* Count aggregation through the swapped seed. */
|
||||
cbm_cypher_result_t count_form = {0};
|
||||
ASSERT_EQ(cbm_cypher_execute(s,
|
||||
"MATCH (a)-[:CALLS]->(b) WHERE b.name = 'LogError' "
|
||||
"RETURN count(a) AS callers",
|
||||
"test", 0, &count_form),
|
||||
0);
|
||||
ASSERT_EQ(count_form.row_count, 1);
|
||||
ASSERT_STR_EQ(count_form.rows[0][0], "1");
|
||||
cbm_cypher_result_free(&count_form);
|
||||
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cypher_exec_prop_string_with_escaped_quote) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
cbm_store_upsert_project(s, "test", "/tmp/test");
|
||||
@@ -4792,4 +4849,5 @@ SUITE(cypher) {
|
||||
/* Composite property projection (arrays/objects, escaped quotes) */
|
||||
RUN_TEST(cypher_exec_prop_array_with_internal_commas);
|
||||
RUN_TEST(cypher_exec_prop_string_with_escaped_quote);
|
||||
RUN_TEST(cypher_single_hop_seeds_from_selective_far_node);
|
||||
}
|
||||
|
||||
@@ -2941,6 +2941,106 @@ TEST(commonlisp_defmacro) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* 2026-09-16 probe: config extractors handed multi-line node text over as a
|
||||
* name (8,301 elasticsearch YAML Fields, 91 kernel Makefile/.conf nodes with a
|
||||
* line break inside), and JS/TS baselines named functions `{}` and variables
|
||||
* `1`. The definition push is the one place every extractor goes through. */
|
||||
TEST(defs_push_cuts_multiline_names_and_rejects_js_literal_names) {
|
||||
CBMArena a;
|
||||
cbm_arena_init(&a);
|
||||
CBMDefArray defs = {0};
|
||||
|
||||
CBMDefinition make = {0};
|
||||
make.name = "endif\n\n$(obj)/pm_data-offsets.h";
|
||||
make.qualified_name = "proj.arch.arm.mach-at91.Makefile.endif\n\n$(obj)/pm_data-offsets.h";
|
||||
make.label = "Function";
|
||||
make.file_path = "arch/arm/mach-at91/Makefile";
|
||||
cbm_defs_push(&defs, &a, make);
|
||||
ASSERT_EQ(defs.count, 1);
|
||||
ASSERT_STR_EQ(defs.items[0].name, "endif");
|
||||
ASSERT_STR_EQ(defs.items[0].qualified_name, "proj.arch.arm.mach-at91.Makefile.endif");
|
||||
|
||||
CBMDefinition yaml = {0};
|
||||
yaml.name = "Test get datafeed stats given missing datafeed_id \r\n - do:";
|
||||
yaml.qualified_name =
|
||||
"proj.spec.Test get datafeed stats given missing datafeed_id \r\n - do:";
|
||||
yaml.label = "Field";
|
||||
yaml.file_path = "rest-api-spec/test/ml/get_datafeed_stats.yml";
|
||||
cbm_defs_push(&defs, &a, yaml);
|
||||
ASSERT_EQ(defs.count, 2);
|
||||
ASSERT_STR_EQ(defs.items[1].name, "Test get datafeed stats given missing datafeed_id");
|
||||
|
||||
/* JS/TS: a literal token is not a name. */
|
||||
CBMDefinition brace = {0};
|
||||
brace.name = "{}";
|
||||
brace.qualified_name = "proj.tests.baselines.x.{}";
|
||||
brace.label = "Function";
|
||||
brace.file_path = "tests/baselines/reference/x.js";
|
||||
cbm_defs_push(&defs, &a, brace);
|
||||
CBMDefinition one = {0};
|
||||
one.name = "1";
|
||||
one.qualified_name = "proj.tests.cases.y.1";
|
||||
one.label = "Variable";
|
||||
one.file_path = "tests/cases/y.ts";
|
||||
cbm_defs_push(&defs, &a, one);
|
||||
ASSERT_EQ(defs.count, 2);
|
||||
|
||||
/* Real JS names, including private members and `$`-prefixed ones, stay. */
|
||||
CBMDefinition priv = {0};
|
||||
priv.name = "#secret";
|
||||
priv.qualified_name = "proj.src.a.Klass.#secret";
|
||||
priv.label = "Method";
|
||||
priv.file_path = "src/a.ts";
|
||||
cbm_defs_push(&defs, &a, priv);
|
||||
CBMDefinition dollar = {0};
|
||||
dollar.name = "$scope";
|
||||
dollar.qualified_name = "proj.src.b.$scope";
|
||||
dollar.label = "Variable";
|
||||
dollar.file_path = "src/b.js";
|
||||
cbm_defs_push(&defs, &a, dollar);
|
||||
ASSERT_EQ(defs.count, 4);
|
||||
|
||||
/* Member keys JS spells without an identifier start are names too:
|
||||
* computed, string-literal and escaped (1,782 real definitions in the
|
||||
* TypeScript corpus wore these spellings). */
|
||||
const char *member_keys[] = {"[Symbol.iterator]", "\"my-key\"", "'x'", "\\u0410"};
|
||||
for (int i = 0; i < 4; i++) {
|
||||
CBMDefinition member = {0};
|
||||
member.name = member_keys[i];
|
||||
member.qualified_name = member_keys[i];
|
||||
member.label = "Method";
|
||||
member.file_path = "src/c.ts";
|
||||
cbm_defs_push(&defs, &a, member);
|
||||
}
|
||||
ASSERT_EQ(defs.count, 8);
|
||||
|
||||
/* Tokens that are not names in any spelling: patterns, numeric literals,
|
||||
* parenthesised types, rest elements. */
|
||||
const char *tokens[] = {"{ b11 } = { b11: \"string\" }", "0x0", "3.2e1",
|
||||
"(x: number) => string", "...a", "?"};
|
||||
for (int i = 0; i < 6; i++) {
|
||||
CBMDefinition token = {0};
|
||||
token.name = tokens[i];
|
||||
token.qualified_name = tokens[i];
|
||||
token.label = "Variable";
|
||||
token.file_path = "tests/cases/z.js";
|
||||
cbm_defs_push(&defs, &a, token);
|
||||
}
|
||||
ASSERT_EQ(defs.count, 8);
|
||||
|
||||
/* Other languages may legitimately name operators: untouched. */
|
||||
CBMDefinition op = {0};
|
||||
op.name = "<$>";
|
||||
op.qualified_name = "proj.Data.Functor.<$>";
|
||||
op.label = "Function";
|
||||
op.file_path = "src/Data/Functor.hs";
|
||||
cbm_defs_push(&defs, &a, op);
|
||||
ASSERT_EQ(defs.count, 9);
|
||||
|
||||
cbm_arena_destroy(&a);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(makefile_rule_as_function) {
|
||||
CBMFileResult *r = extract("all:\n\t@echo hello\n", CBM_LANG_MAKEFILE, "test", "Makefile");
|
||||
ASSERT_NOT_NULL(r);
|
||||
@@ -7145,6 +7245,38 @@ static int find_call_args(const CBMFileResult *r, const char *callee, const char
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* tree-sitter lists a comment inside an argument list as a named child; it is
|
||||
* not an argument. A Java constructor call whose argument list opened with a
|
||||
* slash-star comment handed the comment text to the Route pass as a URL
|
||||
* (three Route nodes named by comments on elasticsearch, 2026-09-16). */
|
||||
TEST(call_args_skip_comments_between_arguments) {
|
||||
CBMFileResult *r = extract("class A {\n"
|
||||
" void f() {\n"
|
||||
" app.get(/* the orders listing */ \"/orders\", handler);\n"
|
||||
" new TestCase(\n"
|
||||
" /*\n"
|
||||
" * a multi-line note\n"
|
||||
" */\n"
|
||||
" \"x\", // trailing\n"
|
||||
" 1);\n"
|
||||
" }\n"
|
||||
"}\n",
|
||||
CBM_LANG_JAVA, "t", "A.java");
|
||||
ASSERT_NOT_NULL(r);
|
||||
const char *arg0 = NULL;
|
||||
const char *arg1 = NULL;
|
||||
int argc = find_call_args(r, "get", &arg0, &arg1);
|
||||
ASSERT_EQ(argc, 2);
|
||||
ASSERT_STR_EQ(arg0, "\"/orders\"");
|
||||
ASSERT_STR_EQ(arg1, "handler");
|
||||
argc = find_call_args(r, "TestCase", &arg0, &arg1);
|
||||
ASSERT_EQ(argc, 2);
|
||||
ASSERT_STR_EQ(arg0, "\"x\"");
|
||||
ASSERT_STR_EQ(arg1, "1");
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(objectscript_data_flows_class_method_args) {
|
||||
CBMFileResult *r = extract("Class MyApp.Caller Extends %RegisteredObject\n"
|
||||
"{\n"
|
||||
@@ -8014,6 +8146,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(objectscript_macro_constant_no_extra_call);
|
||||
RUN_TEST(objectscript_udl_method_return_type);
|
||||
RUN_TEST(objectscript_udl_scalar_return_type_not_resolved);
|
||||
RUN_TEST(call_args_skip_comments_between_arguments);
|
||||
RUN_TEST(objectscript_data_flows_class_method_args);
|
||||
RUN_TEST(objectscript_data_flows_instance_method_args);
|
||||
RUN_TEST(iris_export_xml_simple_class);
|
||||
@@ -8224,6 +8357,7 @@ SUITE(extraction) {
|
||||
RUN_TEST(commonlisp_defun);
|
||||
RUN_TEST(commonlisp_multiple_functions);
|
||||
RUN_TEST(commonlisp_defmacro);
|
||||
RUN_TEST(defs_push_cuts_multiline_names_and_rejects_js_literal_names);
|
||||
RUN_TEST(makefile_rule_as_function);
|
||||
RUN_TEST(makefile_multiple_targets);
|
||||
RUN_TEST(makefile_variable_extraction);
|
||||
|
||||
@@ -32,6 +32,19 @@ TEST(infrascan_http_route_literal_guard_rejects_filesystem_paths) {
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/orders", "requests.get"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("https://orders.example/api/orders",
|
||||
"requests.get"));
|
||||
/* A comment that leads an argument list is not a route (elasticsearch
|
||||
* RestHandler routes: three Java block comments became Route nodes). */
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal(
|
||||
"/*\n * Deprecated in #64227, 7.12/8.0.\n */",
|
||||
"Route.builder"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("// legacy path", "Route.builder"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/_cat\n/indices", "Route.builder"));
|
||||
ASSERT_FALSE(cbm_service_pattern_is_http_route_literal("/* all */", "Route.builder"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/_cat/indices", "Route.builder"));
|
||||
/* The wildcard path (slash-star alone) is a route, not a comment:
|
||||
* elasticsearch's RestClient.buildUri(null, wildcard) registers one. */
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/*", "RestClient.buildUri"));
|
||||
ASSERT_TRUE(cbm_service_pattern_is_http_route_literal("/api/*", "app.get"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
|
||||
+129
-9
@@ -3786,6 +3786,124 @@ TEST(tool_search_graph_bm25_reports_candidate_saturation) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Shared fixture for the two BM25 findability probes (2026-09-16, measured on
|
||||
* JetBrains/Exposed and django/django): a Class named exactly like the query,
|
||||
* that class's own Methods, and test Methods whose long names repeat the
|
||||
* query token — the shapes that outranked the class and hid it entirely. */
|
||||
static void bm25_findability_fixture(cbm_store_t *store, const char *project) {
|
||||
cbm_store_upsert_project(store, project, "/tmp/bm25-findability");
|
||||
struct {
|
||||
const char *label, *name, *qn, *file;
|
||||
} rows[] = {
|
||||
{"Class", "Table", "bm25-find.core.Table.Table", "core/Table.kt"},
|
||||
{"Method", "unquoted", "bm25-find.core.Table.Table.unquoted", "core/Table.kt"},
|
||||
{"Method", "describe", "bm25-find.core.Table.Table.describe", "core/Table.kt"},
|
||||
{"Method", "table references table with same name in other database",
|
||||
"bm25-find.tests.SchemaTests.table_references_table_with_same_name", "tests/Schema.kt"},
|
||||
{"Method", "table references table with same name in mysql",
|
||||
"bm25-find.tests.SchemaTests.table_references_table_with_same_name_mysql",
|
||||
"tests/Schema.kt"},
|
||||
{"Function", "get_object_or_404", "bm25-find.shortcuts.get_object_or_404", "shortcuts.py"},
|
||||
{"Method", "test_get_object_or_404",
|
||||
"bm25-find.tests.GetObjectOr404Tests.test_get_object_or_404", "tests/tests.py"},
|
||||
{"Method", "test_get_object_or_404_queryset_attribute_error",
|
||||
"bm25-find.tests.GetObjectOr404Tests.test_get_object_or_404_queryset_attribute_error",
|
||||
"tests/tests.py"},
|
||||
{"Method", "test_get_object_or_404_bad_class",
|
||||
"bm25-find.tests.GetListObjectOr404Test.test_get_object_or_404_bad_class",
|
||||
"tests/async.py"},
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(rows) / sizeof(rows[0]); i++) {
|
||||
cbm_node_t node = {.project = project,
|
||||
.label = rows[i].label,
|
||||
.name = rows[i].name,
|
||||
.qualified_name = rows[i].qn,
|
||||
.file_path = rows[i].file,
|
||||
.start_line = (int)i + 1,
|
||||
.end_line = (int)i + 2};
|
||||
cbm_store_upsert_node(store, &node);
|
||||
}
|
||||
cbm_store_exec(store, "INSERT INTO nodes_fts(nodes_fts) VALUES('delete-all');");
|
||||
cbm_store_exec(store, "INSERT INTO nodes_fts(rowid, name, qualified_name, label, "
|
||||
"file_path) SELECT id, cbm_camel_split(name), qualified_name, "
|
||||
"label, file_path FROM nodes;");
|
||||
}
|
||||
|
||||
/* The label filter must apply in query (BM25) mode exactly as in structural
|
||||
* mode: `query=Table label=Class` returns the class, and no Method. */
|
||||
TEST(tool_search_graph_bm25_applies_label_filter) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *store = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(store);
|
||||
const char *project = "bm25-find";
|
||||
cbm_mcp_server_set_project(srv, project);
|
||||
bm25_findability_fixture(store, project);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":554,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
|
||||
"\"project\":\"bm25-find\",\"query\":\"Table\",\"label\":\"Class\","
|
||||
"\"limit\":5,\"format\":\"json\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"bm25-find.core.Table.Table\",\"Class\""));
|
||||
ASSERT_NULL(strstr(inner, "\"Method\""));
|
||||
/* The reported total describes the filtered rows, not the unfiltered window. */
|
||||
ASSERT_NOT_NULL(strstr(inner, "\"total\":1"));
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The definition whose NAME is the query ranks first: the `Table` class above
|
||||
* its own methods and above test methods that repeat "table" three times; the
|
||||
* `get_object_or_404` function above the test methods that contain it. */
|
||||
TEST(tool_search_graph_bm25_ranks_exact_name_first) {
|
||||
cbm_mcp_server_t *srv = cbm_mcp_server_new(NULL);
|
||||
ASSERT_NOT_NULL(srv);
|
||||
cbm_store_t *store = cbm_mcp_server_store(srv);
|
||||
ASSERT_NOT_NULL(store);
|
||||
const char *project = "bm25-find";
|
||||
cbm_mcp_server_set_project(srv, project);
|
||||
bm25_findability_fixture(store, project);
|
||||
|
||||
char *resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":555,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
|
||||
"\"project\":\"bm25-find\",\"query\":\"Table\",\"limit\":5,\"format\":\"json\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
char *inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
const char *rows = strstr(inner, "\"rows\":[");
|
||||
ASSERT_NOT_NULL(rows);
|
||||
const char *first_qn = strstr(rows, "[\"");
|
||||
ASSERT_NOT_NULL(first_qn);
|
||||
ASSERT_EQ(strncmp(first_qn, "[\"bm25-find.core.Table.Table\"", 29), 0);
|
||||
free(inner);
|
||||
free(resp);
|
||||
|
||||
resp = cbm_mcp_server_handle(
|
||||
srv, "{\"jsonrpc\":\"2.0\",\"id\":556,\"method\":\"tools/call\","
|
||||
"\"params\":{\"name\":\"search_graph\",\"arguments\":{"
|
||||
"\"project\":\"bm25-find\",\"query\":\"get_object_or_404\",\"limit\":5,"
|
||||
"\"format\":\"json\"}}}");
|
||||
ASSERT_NOT_NULL(resp);
|
||||
inner = extract_text_content(resp);
|
||||
ASSERT_NOT_NULL(inner);
|
||||
rows = strstr(inner, "\"rows\":[");
|
||||
ASSERT_NOT_NULL(rows);
|
||||
first_qn = strstr(rows, "[\"");
|
||||
ASSERT_NOT_NULL(first_qn);
|
||||
ASSERT_EQ(strncmp(first_qn, "[\"bm25-find.shortcuts.get_object_or_404\"", 40), 0);
|
||||
free(inner);
|
||||
free(resp);
|
||||
cbm_mcp_server_free(srv);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `query` selects the BM25 fast path, which returns before semantic_query is
|
||||
* evaluated. Accepting both therefore used to report success while silently
|
||||
* discarding one of the caller's two requested result sets. The contract is
|
||||
@@ -5583,9 +5701,9 @@ TEST(tool_check_index_coverage_reports_truncation_marker_issue963) {
|
||||
|
||||
/* The marked file: both real ranges survive, the marker is flagged, and the
|
||||
* "12" from the marker never becomes a range of its own. */
|
||||
char *marked =
|
||||
cbm_mcp_handle_tool(srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/marked.c\"],\"format\":\"json\"}");
|
||||
char *marked = cbm_mcp_handle_tool(
|
||||
srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/marked.c\"],\"format\":\"json\"}");
|
||||
ASSERT_NOT_NULL(marked);
|
||||
char *marked_inner = extract_text_content(marked);
|
||||
ASSERT_NOT_NULL(marked_inner);
|
||||
@@ -5597,9 +5715,9 @@ TEST(tool_check_index_coverage_reports_truncation_marker_issue963) {
|
||||
free(marked);
|
||||
|
||||
/* The same ranges without a marker must NOT be reported as truncated. */
|
||||
char *plain =
|
||||
cbm_mcp_handle_tool(srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/plain.c\"],\"format\":\"json\"}");
|
||||
char *plain = cbm_mcp_handle_tool(
|
||||
srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/plain.c\"],\"format\":\"json\"}");
|
||||
ASSERT_NOT_NULL(plain);
|
||||
char *plain_inner = extract_text_content(plain);
|
||||
ASSERT_NOT_NULL(plain_inner);
|
||||
@@ -5610,9 +5728,9 @@ TEST(tool_check_index_coverage_reports_truncation_marker_issue963) {
|
||||
|
||||
/* The reader's own limit stops the list early, so it must say so even
|
||||
* though the producer sent no marker. */
|
||||
char *widest =
|
||||
cbm_mcp_handle_tool(srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/wide.c\"],\"format\":\"json\"}");
|
||||
char *widest = cbm_mcp_handle_tool(
|
||||
srv, "check_index_coverage",
|
||||
"{\"project\":\"coverage-marker\",\"paths\":[\"src/wide.c\"],\"format\":\"json\"}");
|
||||
ASSERT_NOT_NULL(widest);
|
||||
char *wide_inner = extract_text_content(widest);
|
||||
ASSERT_NOT_NULL(wide_inner);
|
||||
@@ -20418,6 +20536,8 @@ SUITE(mcp) {
|
||||
RUN_TEST(tool_output_byte_budgets);
|
||||
RUN_TEST(tool_search_graph_query_honors_file_pattern_issue552);
|
||||
RUN_TEST(tool_search_graph_bm25_reports_candidate_saturation);
|
||||
RUN_TEST(tool_search_graph_bm25_applies_label_filter);
|
||||
RUN_TEST(tool_search_graph_bm25_ranks_exact_name_first);
|
||||
RUN_TEST(tool_search_graph_rejects_bm25_and_semantic_query_together);
|
||||
RUN_TEST(tool_search_graph_semantic_ceiling_never_emits_unusable_continuation);
|
||||
RUN_TEST(tool_search_graph_semantic_pagination_is_lossless_and_independent);
|
||||
|
||||
+130
-24
@@ -112,21 +112,21 @@ static const char *PY_CLEAN = "def ok():\n"
|
||||
* Guards against over-suppression: the preprocessor explains the guarded
|
||||
* region but explains nothing about the garbage, so BOTH must stay flagged
|
||||
* and they must be reported as two separate ranges, not one big one. */
|
||||
static const char *C_IFDEF_SPLIT_PLUS_GARBAGE = "#include <stdio.h>\n" /* 1 */
|
||||
"\n" /* 2 */
|
||||
"void ok_before(void) { }\n" /* 3 */
|
||||
"\n" /* 4 */
|
||||
"#ifdef FEATURE_A\n" /* 5 */
|
||||
"static int guarded(int x) {\n" /* 6 */
|
||||
"#else\n" /* 7 */
|
||||
static const char *C_IFDEF_SPLIT_PLUS_GARBAGE = "#include <stdio.h>\n" /* 1 */
|
||||
"\n" /* 2 */
|
||||
"void ok_before(void) { }\n" /* 3 */
|
||||
"\n" /* 4 */
|
||||
"#ifdef FEATURE_A\n" /* 5 */
|
||||
"static int guarded(int x) {\n" /* 6 */
|
||||
"#else\n" /* 7 */
|
||||
"static int guarded_alt(int x) {\n" /* 8 */
|
||||
"#endif\n" /* 9 */
|
||||
" return x + 1;\n" /* 10 */
|
||||
"}\n" /* 11 */
|
||||
"\n" /* 12 */
|
||||
"%%% ((( &&& ))) %%%\n" /* 13 */
|
||||
"\n" /* 14 */
|
||||
"void ok_after(void) { }\n"; /* 15 */
|
||||
"#endif\n" /* 9 */
|
||||
" return x + 1;\n" /* 10 */
|
||||
"}\n" /* 11 */
|
||||
"\n" /* 12 */
|
||||
"%%% ((( &&& ))) %%%\n" /* 13 */
|
||||
"\n" /* 14 */
|
||||
"void ok_after(void) { }\n"; /* 15 */
|
||||
|
||||
/* Perl formats have a line-oriented body terminated by a lone dot. The
|
||||
* following named sub pins the important recovery boundary: a grammar must
|
||||
@@ -494,7 +494,8 @@ TEST(real_error_before_eof_still_flagged_without_final_newline_issue1610) {
|
||||
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 also lacks its final newline");
|
||||
FAIL("a real mid-file parse failure must still be reported when the file also lacks its "
|
||||
"final newline");
|
||||
}
|
||||
if (!has_ranges) {
|
||||
FAIL("a reported failure must still name its line range");
|
||||
@@ -529,11 +530,8 @@ TEST(perl_format_followed_by_named_sub_is_complete_issue1838) {
|
||||
bool has_error_ranges = r->error_ranges != NULL;
|
||||
bool has_following_sub = has_def(r, "after_format");
|
||||
if (partial || error_regions != 0 || has_error_ranges || !has_following_sub) {
|
||||
fprintf(stderr,
|
||||
" Perl format result: partial=%d regions=%d ranges=%s following_sub=%d\n",
|
||||
partial,
|
||||
error_regions,
|
||||
r->error_ranges ? r->error_ranges : "(none)",
|
||||
fprintf(stderr, " Perl format result: partial=%d regions=%d ranges=%s following_sub=%d\n",
|
||||
partial, error_regions, r->error_ranges ? r->error_ranges : "(none)",
|
||||
has_following_sub);
|
||||
cbm_free_result(r);
|
||||
FAIL("a valid Perl format and its following named sub must parse completely");
|
||||
@@ -619,7 +617,8 @@ TEST(width_bearing_error_at_eof_still_flagged_with_trailing_blank_issue1746) {
|
||||
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");
|
||||
FAIL("a width-bearing parse failure at EOF must still be reported when the file ends in "
|
||||
"blanks");
|
||||
}
|
||||
PASS();
|
||||
}
|
||||
@@ -734,7 +733,6 @@ TEST(c_clean_file_stays_unflagged_after_refinement) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
|
||||
/* The whole-file class, and the reason the parse_unusable kind exists.
|
||||
*
|
||||
* The Phase 2 refinement that narrows a whole-file range using the
|
||||
@@ -806,18 +804,31 @@ TEST(c_ifdef_split_is_partial_never_unusable) {
|
||||
* says so, instead of leaving a wrong note in the plan. (The plan's Phase 0
|
||||
* also listed the pointer form as failing. It does not fail today.) */
|
||||
TEST(c_thread_local_grammar_limit_is_pinned_issue963) {
|
||||
/* The init form used to read as "parses clean", but that was a masking
|
||||
* effect, not a clean parse: the grammar leaves an ERROR on line 1 and
|
||||
* salvages a one-line Variable named `int` from it, which the old
|
||||
* all-or-nothing recovery rule accepted as evidence the line was
|
||||
* understood. Since 2026-09-16 a one-line salvage is not evidence, so the
|
||||
* line is flagged — honest, because `x` is what the graph lacks. */
|
||||
CBMFileResult *ok = do_extract("static _Thread_local int x = 0;\n"
|
||||
"void f(void) { x = 1; }\n",
|
||||
CBM_LANG_C, "tls_init.c");
|
||||
ASSERT_NOT_NULL(ok);
|
||||
ASSERT_FALSE(ok->parse_incomplete);
|
||||
ASSERT_TRUE(ok->parse_incomplete);
|
||||
ASSERT_NOT_NULL(ok->error_ranges);
|
||||
ASSERT_STR_EQ("1-1", ok->error_ranges);
|
||||
ASSERT_TRUE(has_def(ok, "f"));
|
||||
cbm_free_result(ok);
|
||||
|
||||
/* The pointer form: the same masking, the same honest answer now. */
|
||||
CBMFileResult *ptr = do_extract("static _Thread_local int *p;\n"
|
||||
"void f(void) { p = 0; }\n",
|
||||
CBM_LANG_C, "tls_ptr.c");
|
||||
ASSERT_NOT_NULL(ptr);
|
||||
ASSERT_FALSE(ptr->parse_incomplete);
|
||||
ASSERT_TRUE(ptr->parse_incomplete);
|
||||
ASSERT_NOT_NULL(ptr->error_ranges);
|
||||
ASSERT_STR_EQ("1-1", ptr->error_ranges);
|
||||
ASSERT_TRUE(has_def(ptr, "f"));
|
||||
cbm_free_result(ptr);
|
||||
|
||||
CBMFileResult *arr = do_extract("static _Thread_local char b[8];\n"
|
||||
@@ -879,6 +890,99 @@ TEST(coverage_range_never_ends_past_the_last_line_issue963) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Residual ranges (2026-09-16) ─────────────────────────────────────────
|
||||
* The recovery subtraction used to be all-or-nothing: a region stayed flagged
|
||||
* whole unless every one of its lines was covered by a definition that started
|
||||
* inside it. On torvalds/linux one ERROR node that swallowed the second half of
|
||||
* kernel/sched/core.c (lines 5522-11284) survived on the strength of the
|
||||
* comment and macro lines BETWEEN its 286 extracted functions, and
|
||||
* check_index_coverage told a reader 68 % of the scheduler was unindexed. The
|
||||
* ranges must now name only the lines no extracted definition covers. */
|
||||
|
||||
/* A brace-unbalanced junk line between two clean functions: the recovery
|
||||
* walker still extracts both functions, so the reported range must not cover
|
||||
* either of them, only the junk. */
|
||||
static const char *C_JUNK_BETWEEN_FUNCTIONS = "int alpha(void) {\n" /* 1 */
|
||||
" return 1;\n" /* 2 */
|
||||
"}\n" /* 3 */
|
||||
"\n" /* 4 */
|
||||
"} ] junk ( {\n" /* 5 */
|
||||
"\n" /* 6 */
|
||||
"int beta(void) {\n" /* 7 */
|
||||
" return 2;\n" /* 8 */
|
||||
"}\n" /* 9 */
|
||||
"\n" /* 10 */
|
||||
"int gamma(void) {\n" /* 11 */
|
||||
" return 3;\n" /* 12 */
|
||||
"}\n"; /* 13 */
|
||||
|
||||
static int range_covers_line(const char *ranges, unsigned int line) {
|
||||
const char *p = ranges;
|
||||
while (p && *p) {
|
||||
unsigned int s = 0;
|
||||
unsigned int e = 0;
|
||||
if (sscanf(p, "%u-%u", &s, &e) == 2 && s <= line && line <= e) {
|
||||
return 1;
|
||||
}
|
||||
p = strchr(p, ',');
|
||||
if (p) {
|
||||
p++;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(coverage_range_never_covers_an_extracted_definition) {
|
||||
CBMFileResult *r = do_extract(C_JUNK_BETWEEN_FUNCTIONS, CBM_LANG_C, "junk.c");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_TRUE(has_def(r, "alpha"));
|
||||
ASSERT_TRUE(has_def(r, "beta"));
|
||||
ASSERT_TRUE(has_def(r, "gamma"));
|
||||
ASSERT_TRUE(r->parse_incomplete);
|
||||
ASSERT_NOT_NULL(r->error_ranges);
|
||||
/* Every extracted definition's own lines are in the graph, so no reported
|
||||
* range may cover its start line. */
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const CBMDefinition *d = &r->defs.items[i];
|
||||
if (!d->label || strcmp(d->label, "Module") == 0) {
|
||||
continue;
|
||||
}
|
||||
ASSERT_FALSE(range_covers_line(r->error_ranges, d->start_line));
|
||||
}
|
||||
/* And the junk line itself is still reported. */
|
||||
ASSERT_TRUE(range_covers_line(r->error_ranges, 5u));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The lines between recovered definitions are comments and blanks here: a gap
|
||||
* with no code in it is not a miss, so nothing may be reported for it. The
|
||||
* junk line stays reported. */
|
||||
static const char *C_JUNK_WITH_COMMENT_GAPS = "int alpha(void) {\n" /* 1 */
|
||||
" return 1;\n" /* 2 */
|
||||
"}\n" /* 3 */
|
||||
"/* between alpha and the junk */\n" /* 4 */
|
||||
"} ] junk ( {\n" /* 5 */
|
||||
"// trailing note\n" /* 6 */
|
||||
"int beta(void) {\n" /* 7 */
|
||||
" return 2;\n" /* 8 */
|
||||
"}\n"; /* 9 */
|
||||
|
||||
TEST(coverage_gap_of_only_comments_is_not_a_miss) {
|
||||
CBMFileResult *r = do_extract(C_JUNK_WITH_COMMENT_GAPS, CBM_LANG_C, "gaps.c");
|
||||
ASSERT_NOT_NULL(r);
|
||||
ASSERT_TRUE(has_def(r, "alpha"));
|
||||
ASSERT_TRUE(has_def(r, "beta"));
|
||||
ASSERT_TRUE(r->parse_incomplete);
|
||||
ASSERT_NOT_NULL(r->error_ranges);
|
||||
ASSERT_TRUE(range_covers_line(r->error_ranges, 5u));
|
||||
ASSERT_FALSE(range_covers_line(r->error_ranges, 1u));
|
||||
ASSERT_FALSE(range_covers_line(r->error_ranges, 7u));
|
||||
ASSERT_FALSE(range_covers_line(r->error_ranges, 9u));
|
||||
cbm_free_result(r);
|
||||
PASS();
|
||||
}
|
||||
|
||||
SUITE(parse_coverage) {
|
||||
RUN_TEST(c_ifdef_split_brace_sets_parse_incomplete);
|
||||
RUN_TEST(c_ifdef_split_brace_neighbors_still_extracted);
|
||||
@@ -917,4 +1021,6 @@ SUITE(parse_coverage) {
|
||||
RUN_TEST(c_thread_local_grammar_limit_is_pinned_issue963);
|
||||
RUN_TEST(coverage_repeated_error_line_reports_one_range_issue963);
|
||||
RUN_TEST(coverage_range_never_ends_past_the_last_line_issue963);
|
||||
RUN_TEST(coverage_range_never_covers_an_extracted_definition);
|
||||
RUN_TEST(coverage_gap_of_only_comments_is_not_a_miss);
|
||||
}
|
||||
|
||||
@@ -5710,6 +5710,123 @@ TEST(pipeline_python_receiver_suppresses_weak_method_edge) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The member guard's one exemption (2026-09-16, v0.10.8 → v0.11.0 A/B on
|
||||
* django/django): a member call whose receiver is an attribute chain rooted at
|
||||
* self/cls, whose callee has exactly ONE definition in the project and is not a
|
||||
* builtin type's own method keeps its unique_name edge —
|
||||
* `self.compiler.apply_converters()` names the only apply_converters there is.
|
||||
* A bare parameter receiver and the builtin-member class the guard was built
|
||||
* for (`parts.extend()` -> a project ListMixin.extend) stay suppressed.
|
||||
* Fewer than 50 files exercises pass_calls.c. */
|
||||
TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_python_uniq_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
write_temp_file(tmp, "compiler.py",
|
||||
"class SQLCompiler:\n"
|
||||
" def apply_converters(self, rows):\n"
|
||||
" return rows\n");
|
||||
write_temp_file(tmp, "mixin.py",
|
||||
"class ListMixin:\n"
|
||||
" def extend(self, other):\n"
|
||||
" return other\n");
|
||||
write_temp_file(tmp, "caller.py",
|
||||
"class RawIterable:\n"
|
||||
" def __init__(self, compiler):\n"
|
||||
" self.compiler = compiler\n"
|
||||
"\n"
|
||||
" def iterate(self):\n"
|
||||
" return self.compiler.apply_converters([])\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def collect(parts):\n"
|
||||
" parts.extend([1])\n"
|
||||
" return parts\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"def handed_in(compiler):\n"
|
||||
" return compiler.apply_converters([])\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/python_uniq.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);
|
||||
|
||||
/* POSITIVE: the only apply_converters in the project binds through
|
||||
* self.compiler, an object the class owns. */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "iterate", "apply_converters"));
|
||||
/* NEGATIVE: the same unique name through a bare parameter carries no
|
||||
* ownership evidence and stays suppressed (#1276's own shape). */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "handed_in", "apply_converters"));
|
||||
/* NEGATIVE: list.extend on a parameter must not bind ListMixin.extend. */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "collect", "extend"));
|
||||
/* Tripwire against a vacuous run. */
|
||||
ASSERT_GTE(cbm_store_count_edges_by_type(s, project, "CALLS"), 1);
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Embedded-script hosts (2026-09-16 probe on JetBrains/Exposed): a member call
|
||||
* inside an HTML <script> body is extracted as JavaScript and carries the JS
|
||||
* receiver flag, but the weak-member guard was gated on the FILE language, so
|
||||
* 4,207 generated Dokka pages bound `localStorage.getItem` to a docs bundle
|
||||
* function through unique_name. HTML (and Vue/Svelte/Astro) join the gate.
|
||||
* POSITIVE tripwire: a bare project call in a plain .js file still resolves. */
|
||||
TEST(pipeline_html_embedded_member_call_stays_unbound) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_html_member_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
|
||||
/* One directory level each: write_temp_file creates a single parent. */
|
||||
write_temp_file(tmp, "scripts/storage.js",
|
||||
"function getItem(key) {\n"
|
||||
" return key;\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "scripts/theme.js",
|
||||
"import { getItem } from './storage.js';\n"
|
||||
"\n"
|
||||
"function loadTheme() {\n"
|
||||
" return getItem('theme');\n"
|
||||
"}\n");
|
||||
write_temp_file(tmp, "docs/index.html",
|
||||
"<html><head><script>\n"
|
||||
"var mode = localStorage.getItem('dokka-dark-mode');\n"
|
||||
"</script></head><body></body></html>\n");
|
||||
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/html_member.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: localStorage's own method must not bind the docs bundle. */
|
||||
ASSERT_FALSE(cross_file_call_exists(s, project, "index", "getItem"));
|
||||
/* POSITIVE: the bare project call in storage.js resolves as before. */
|
||||
ASSERT_TRUE(cross_file_call_exists(s, project, "loadTheme", "getItem"));
|
||||
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Fixture for the #1928 cross-language reference-guard probes (sequential and
|
||||
* parallel twins). pad_files > 0 adds filler files to push the index over the
|
||||
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
|
||||
@@ -7589,6 +7706,60 @@ TEST(pipeline_python_project) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `#include <linux/device.h>` from arch/x/bugs.c: two headers end with the
|
||||
* include path (include/linux/device.h, tools/virtio/linux/device.h) and each
|
||||
* declares `struct device`. The exact-file lookup returned the first matching
|
||||
* node in by-name bucket order — the workers' merge order, different run to
|
||||
* run: 5,821 kernel IMPORTS edges moved between two indexes of one tree and
|
||||
* the CALLS resolved through their import maps moved with them. The target is
|
||||
* the least nested File node, whichever order the nodes were registered in. */
|
||||
static void add_device_header(cbm_gbuf_t *gb, const char *rel, const char *qn_stem) {
|
||||
char fqn[256];
|
||||
snprintf(fqn, sizeof(fqn), "%s.h.__file__", qn_stem);
|
||||
cbm_gbuf_upsert_node(gb, "File", "device.h", fqn, rel, 0, 0, "{}");
|
||||
char cqn[256];
|
||||
snprintf(cqn, sizeof(cqn), "%s.device", qn_stem);
|
||||
cbm_gbuf_upsert_node(gb, "Class", "device", cqn, rel, 10, 40, "{}");
|
||||
}
|
||||
|
||||
static const cbm_gbuf_node_t *resolve_device_h_from(cbm_gbuf_t *gb) {
|
||||
atomic_int cancelled = 0;
|
||||
cbm_pipeline_ctx_t ctx = {
|
||||
.project_name = "p",
|
||||
.repo_path = "/tmp/p",
|
||||
.gbuf = gb,
|
||||
.registry = NULL,
|
||||
.cancelled = &cancelled,
|
||||
};
|
||||
CBMImport imp = {0};
|
||||
imp.module_path = "linux/device.h";
|
||||
imp.local_name = "h";
|
||||
return cbm_pipeline_resolve_import_node(&ctx, "arch/x/bugs.c", "p.arch.x.bugs.c.__file__", &imp,
|
||||
NULL);
|
||||
}
|
||||
|
||||
TEST(pipeline_header_include_target_is_independent_of_registration_order) {
|
||||
cbm_gbuf_t *a = cbm_gbuf_new("p", "/tmp/cbm_include_order_a");
|
||||
cbm_gbuf_t *b = cbm_gbuf_new("p", "/tmp/cbm_include_order_b");
|
||||
ASSERT_NOT_NULL(a);
|
||||
ASSERT_NOT_NULL(b);
|
||||
add_device_header(a, "include/linux/device.h", "p.include.linux.device");
|
||||
add_device_header(a, "tools/virtio/linux/device.h", "p.tools.virtio.linux.device");
|
||||
add_device_header(b, "tools/virtio/linux/device.h", "p.tools.virtio.linux.device");
|
||||
add_device_header(b, "include/linux/device.h", "p.include.linux.device");
|
||||
|
||||
const cbm_gbuf_node_t *ta = resolve_device_h_from(a);
|
||||
const cbm_gbuf_node_t *tb = resolve_device_h_from(b);
|
||||
ASSERT_NOT_NULL(ta);
|
||||
ASSERT_NOT_NULL(tb);
|
||||
ASSERT_STR_EQ(ta->qualified_name, "p.include.linux.device.h.__file__");
|
||||
ASSERT_STR_EQ(tb->qualified_name, "p.include.linux.device.h.__file__");
|
||||
|
||||
cbm_gbuf_free(a);
|
||||
cbm_gbuf_free(b);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* #768 end-to-end: `import { A, B } from './lib'` must survive the REAL
|
||||
* pipeline (extract -> gbuf dedup -> raw SQLite dump) as TWO IMPORTS edges
|
||||
* with distinct local_name — and the dumped DB must satisfy its own schema.
|
||||
@@ -10517,6 +10688,61 @@ TEST(registry_receiver_chain_keeps_project_extension_issue1893) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A factory chain into a nested type: Settings.builder().putList names the only
|
||||
* putList in the project, Settings.Builder.putList, and Settings — the chain's
|
||||
* root — is in the candidate's ancestry even though the candidate's immediate
|
||||
* parent (Builder) is not spelled in the chain. The parent-only rule refused
|
||||
* 9,473 such unique-name calls on elastic/elasticsearch (v0.10.8 → v0.11.0 A/B,
|
||||
* 2026-09-16). */
|
||||
TEST(registry_receiver_chain_admits_factory_chain_into_nested_type) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "putList", "org.elasticsearch.common.settings.Settings.Builder.putList",
|
||||
"Method");
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(
|
||||
reg, "Settings.builder().putList", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.settings.Settings.Builder.putList");
|
||||
ASSERT_STR_EQ(r.strategy, "unique_name");
|
||||
|
||||
/* The same shape through a value in between: RequestOptions.DEFAULT
|
||||
* .toBuilder().setWarningsHandler -> RequestOptions.Builder.setWarningsHandler. */
|
||||
cbm_registry_add(reg, "setWarningsHandler",
|
||||
"org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler",
|
||||
"Method");
|
||||
r = cbm_registry_resolve(reg, "RequestOptions.DEFAULT.toBuilder().setWarningsHandler",
|
||||
"org.elasticsearch.client.Rest", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name,
|
||||
"org.elasticsearch.client.RequestOptions.Builder.setWarningsHandler");
|
||||
|
||||
cbm_registry_free(reg);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The ancestry rule must not widen the gate to foreign roots: Base64 is the
|
||||
* JDK's, so the project's DocOffsetsCodec.getEncoder stays refused, exactly as
|
||||
* #1893's URLSession case above. */
|
||||
TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule) {
|
||||
cbm_registry_t *reg = cbm_registry_new();
|
||||
cbm_registry_add(reg, "getEncoder", "org.elasticsearch.index.codec.DocOffsetsCodec.getEncoder",
|
||||
"Method");
|
||||
|
||||
cbm_resolution_t r = cbm_registry_resolve(
|
||||
reg, "Base64.getEncoder", "org.elasticsearch.index.IndexSettings", NULL, NULL, 0);
|
||||
ASSERT_NULL(r.qualified_name);
|
||||
|
||||
/* Nor may a package segment admit it: Math.toIntExact must not bind
|
||||
* org.elasticsearch.common.Math.toIntExact through "Math" appearing as an
|
||||
* ancestry segment ONLY IF that segment really is the type — here it is, and
|
||||
* that is the correct outcome (the project's own Math.toIntExact). */
|
||||
cbm_registry_add(reg, "toIntExact", "org.elasticsearch.common.Math.toIntExact", "Method");
|
||||
r = cbm_registry_resolve(reg, "Math.toIntExact", "org.elasticsearch.index.IndexSettings", NULL,
|
||||
NULL, 0);
|
||||
ASSERT_STR_EQ(r.qualified_name, "org.elasticsearch.common.Math.toIntExact");
|
||||
|
||||
cbm_registry_free(reg);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A lower-case root names a value, whose type the chain does not show. The gate
|
||||
* must not look at it, or every ordinary vm.load style call would be refused. */
|
||||
TEST(registry_receiver_chain_ignores_lowercase_root_issue1893) {
|
||||
@@ -14841,6 +15067,8 @@ SUITE(pipeline) {
|
||||
#endif
|
||||
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
|
||||
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
|
||||
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
|
||||
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel);
|
||||
RUN_TEST(pipeline_go_bare_ref_never_binds_field);
|
||||
@@ -14876,6 +15104,7 @@ SUITE(pipeline) {
|
||||
RUN_TEST(usages_kotlin_no_duplicate_calls);
|
||||
/* Language integration tests */
|
||||
RUN_TEST(pipeline_python_project);
|
||||
RUN_TEST(pipeline_header_include_target_is_independent_of_registration_order);
|
||||
RUN_TEST(pipeline_imports_multi_symbol_edges);
|
||||
RUN_TEST(pipeline_go_cross_package_call);
|
||||
RUN_TEST(pipeline_swift_cross_package_import);
|
||||
@@ -15019,6 +15248,8 @@ SUITE(pipeline) {
|
||||
RUN_TEST(registry_receiver_chain_refuses_library_unique_name_issue1893);
|
||||
RUN_TEST(registry_receiver_chain_refuses_library_suffix_match_issue1893);
|
||||
RUN_TEST(registry_receiver_chain_keeps_project_extension_issue1893);
|
||||
RUN_TEST(registry_receiver_chain_admits_factory_chain_into_nested_type);
|
||||
RUN_TEST(registry_receiver_chain_still_refuses_foreign_root_with_ancestry_rule);
|
||||
RUN_TEST(registry_receiver_chain_ignores_lowercase_root_issue1893);
|
||||
RUN_TEST(registry_receiver_chain_ignores_bare_name_issue1893);
|
||||
RUN_TEST(registry_fuzzy_confidence_single);
|
||||
|
||||
@@ -820,6 +820,96 @@ TEST(perl_suppress_keeps_high_confidence_and_genuine_calls) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* 2026-09-16 probe on torvalds/linux: a Makefile target bound the C function
|
||||
* `sk_psock.eval` through unique_name. Build and configuration languages have
|
||||
* no cross-language call semantics, so for them a unique match into another
|
||||
* language is a collision by construction; a code caller keeps #1572. */
|
||||
TEST(cross_language_config_caller_drops_unique_name_too) {
|
||||
ASSERT_TRUE(cbm_suppress_cross_language_suffix_match(CBM_LANG_MAKEFILE, "include/linux/skmsg.h",
|
||||
"unique_name"));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_cross_language_suffix_match(CBM_LANG_CMAKE, "src/main.c", "unique_name"));
|
||||
ASSERT_TRUE(
|
||||
cbm_suppress_cross_language_suffix_match(CBM_LANG_YAML, "app/models.py", "unique_name"));
|
||||
/* A code caller's unique_name into another language is still #1572. */
|
||||
ASSERT_FALSE(cbm_suppress_cross_language_suffix_match(
|
||||
CBM_LANG_PYTHON, "web/src/pages/Editor.js", "unique_name"));
|
||||
/* Same-language config targets are untouched. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_cross_language_suffix_match(CBM_LANG_MAKEFILE, "lib/Makefile", "unique_name"));
|
||||
/* Receiver-aware strategies are never this guard's business. */
|
||||
ASSERT_FALSE(
|
||||
cbm_suppress_cross_language_suffix_match(CBM_LANG_MAKEFILE, "src/main.c", "same_module"));
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Same-named candidates that tie on test-status and namespace proximity used
|
||||
* to be settled by bucket position, i.e. by the order files were registered:
|
||||
* two indexes of one kernel tree differed by 632 CALLS edges, `dev_name`
|
||||
* landing on any of twenty same-named struct fields or on nothing at all.
|
||||
* The tie is now a function of the candidate set: the least nested
|
||||
* definition, then the smaller QN — whichever order the registry was built
|
||||
* in (O9). */
|
||||
TEST(registry_tie_break_is_independent_of_registration_order) {
|
||||
const char *cands[] = {
|
||||
"proj.drivers.media.cec.i2c.ch7322.ch7322_conn_match.dev_name", /* struct field */
|
||||
"proj.sound.soc.codecs.tas2783-sdw.tas2783_prv.dev_name", /* struct field */
|
||||
"proj.include.linux.device.dev_name", /* the function */
|
||||
"proj.arch.um.drivers.pty.pty_chan.dev_name", /* struct field */
|
||||
};
|
||||
const char *labels[] = {"Field", "Field", "Function", "Field"};
|
||||
const int n = 4;
|
||||
|
||||
cbm_registry_t *forward = cbm_registry_new();
|
||||
cbm_registry_t *backward = cbm_registry_new();
|
||||
for (int i = 0; i < n; i++) {
|
||||
cbm_registry_add(forward, "dev_name", cands[i], labels[i]);
|
||||
cbm_registry_add(backward, "dev_name", cands[n - 1 - i], labels[n - 1 - i]);
|
||||
}
|
||||
|
||||
/* A far-away caller with no imports: every candidate scores the same. */
|
||||
cbm_resolution_t f =
|
||||
cbm_registry_resolve(forward, "dev_name", "proj.kernel.irq.msi", NULL, NULL, 0);
|
||||
cbm_resolution_t b =
|
||||
cbm_registry_resolve(backward, "dev_name", "proj.kernel.irq.msi", NULL, NULL, 0);
|
||||
ASSERT_NOT_NULL(f.qualified_name);
|
||||
ASSERT_NOT_NULL(b.qualified_name);
|
||||
ASSERT_STR_EQ(f.qualified_name, "proj.include.linux.device.dev_name");
|
||||
ASSERT_STR_EQ(b.qualified_name, "proj.include.linux.device.dev_name");
|
||||
ASSERT_STR_EQ(f.strategy, "suffix_match");
|
||||
ASSERT_STR_EQ(b.strategy, "suffix_match");
|
||||
|
||||
/* Equal depth: the smaller QN, from either order. */
|
||||
cbm_registry_t *lex_a = cbm_registry_new();
|
||||
cbm_registry_t *lex_b = cbm_registry_new();
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_a, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.include.linux.scatterlist.sg_next", "Function");
|
||||
cbm_registry_add(lex_b, "sg_next", "proj.tools.virtio.scatterlist.sg_next", "Function");
|
||||
cbm_resolution_t la =
|
||||
cbm_registry_resolve(lex_a, "sg_next", "proj.drivers.scsi.arm_scsi", NULL, NULL, 0);
|
||||
cbm_resolution_t lb =
|
||||
cbm_registry_resolve(lex_b, "sg_next", "proj.drivers.scsi.arm_scsi", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(la.qualified_name, "proj.include.linux.scatterlist.sg_next");
|
||||
ASSERT_STR_EQ(lb.qualified_name, "proj.include.linux.scatterlist.sg_next");
|
||||
|
||||
/* Proximity still outranks depth: the sibling wins over a shallower stranger. */
|
||||
cbm_registry_t *near = cbm_registry_new();
|
||||
cbm_registry_add(near, "vnic_rq_free", "proj.lib.vnic_rq_free", "Function");
|
||||
cbm_registry_add(near, "vnic_rq_free", "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free",
|
||||
"Function");
|
||||
cbm_resolution_t nr = cbm_registry_resolve(near, "vnic_rq_free",
|
||||
"proj.drivers.scsi.fnic.fnic_res", NULL, NULL, 0);
|
||||
ASSERT_STR_EQ(nr.qualified_name, "proj.drivers.scsi.fnic.vnic_rq.vnic_rq_free");
|
||||
|
||||
cbm_registry_free(forward);
|
||||
cbm_registry_free(backward);
|
||||
cbm_registry_free(lex_a);
|
||||
cbm_registry_free(lex_b);
|
||||
cbm_registry_free(near);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(cross_language_suffix_match_drops_py_vs_js) {
|
||||
/* #725: two same-named symbols in different languages. suffix_match is the
|
||||
* strategy that collapses them; unique_name is #1572 and must stay. */
|
||||
@@ -939,6 +1029,53 @@ TEST(dynamic_suppress_keeps_high_confidence_and_non_methods) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(python_builtin_member_table_matches_builtin_type_methods) {
|
||||
/* Ends and middle of the sorted table, so a mis-sorted insert shows up. */
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("add"));
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("extend"));
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("items"));
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("print"));
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("startswith"));
|
||||
ASSERT_TRUE(cbm_python_is_builtin_member("zfill"));
|
||||
/* Project-specific spellings are not builtin members. */
|
||||
ASSERT_FALSE(cbm_python_is_builtin_member("apply_converters"));
|
||||
ASSERT_FALSE(cbm_python_is_builtin_member("lazy_model_operation"));
|
||||
ASSERT_FALSE(cbm_python_is_builtin_member("describe"));
|
||||
ASSERT_FALSE(cbm_python_is_builtin_member(NULL));
|
||||
ASSERT_FALSE(cbm_python_is_builtin_member(""));
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(weak_member_unique_name_exempt_is_python_self_rooted_unique_and_specific_only) {
|
||||
/* The exemption: Python, self/cls-rooted receiver, unique_name, callee not
|
||||
* a builtin member. */
|
||||
ASSERT_TRUE(cbm_weak_member_unique_name_exempt(true, true, "self.compiler.apply_converters",
|
||||
"unique_name"));
|
||||
ASSERT_TRUE(cbm_weak_member_unique_name_exempt(true, true, "cls.registry.lazy_model_operation",
|
||||
"unique_name"));
|
||||
/* A bare parameter/local receiver carries no ownership evidence: #1276's
|
||||
* accelerator.backward() and c.describe() stay suppressed. */
|
||||
ASSERT_FALSE(
|
||||
cbm_weak_member_unique_name_exempt(true, false, "accelerator.backward", "unique_name"));
|
||||
/* A builtin type's own method stays suppressed even on self. */
|
||||
ASSERT_FALSE(
|
||||
cbm_weak_member_unique_name_exempt(true, true, "self.parts.extend", "unique_name"));
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, "self.out.print", "unique_name"));
|
||||
/* Only unique_name carries the one-definition evidence. */
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, "self.compiler.apply_converters",
|
||||
"suffix_match"));
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, "self.compiler.apply_converters",
|
||||
"field_type_hint"));
|
||||
/* Python only: the JS/TS family keeps its recorded trade. */
|
||||
ASSERT_FALSE(
|
||||
cbm_weak_member_unique_name_exempt(false, true, "this.compiler.apply", "unique_name"));
|
||||
/* Degenerate inputs never exempt. */
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, NULL, "unique_name"));
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, "", "unique_name"));
|
||||
ASSERT_FALSE(cbm_weak_member_unique_name_exempt(true, true, "self.x.y", NULL));
|
||||
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. */
|
||||
@@ -1103,10 +1240,14 @@ SUITE(registry) {
|
||||
RUN_TEST(perl_suppress_drops_weak_builtin_and_method_matches);
|
||||
RUN_TEST(perl_suppress_keeps_high_confidence_and_genuine_calls);
|
||||
RUN_TEST(cross_language_suffix_match_drops_py_vs_js);
|
||||
RUN_TEST(cross_language_config_caller_drops_unique_name_too);
|
||||
RUN_TEST(registry_tie_break_is_independent_of_registration_order);
|
||||
RUN_TEST(cross_language_ref_drops_go_vs_c);
|
||||
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(python_builtin_member_table_matches_builtin_type_methods);
|
||||
RUN_TEST(weak_member_unique_name_exempt_is_python_self_rooted_unique_and_specific_only);
|
||||
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);
|
||||
|
||||
@@ -436,6 +436,30 @@ TEST(json_escape_control_chars) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A truncated escape must never end inside a multibyte UTF-8 sequence: the
|
||||
* callee text of a CALLS edge is escaped into a fixed buffer, and a cut umlaut
|
||||
* persisted bytes SQLite could not decode as text (2026-09-16 probe: rust,
|
||||
* java and typescript stores). "aé" is a, 0xC3, 0xA9. */
|
||||
TEST(json_escape_never_splits_utf8) {
|
||||
char buf[3]; /* room for two payload bytes + NUL: the lead byte would fit, its continuation not
|
||||
*/
|
||||
int len = cbm_json_escape(buf, sizeof(buf), "a\xC3\xA9");
|
||||
ASSERT_STR_EQ(buf, "a");
|
||||
ASSERT_EQ(len, 1);
|
||||
|
||||
char whole[4];
|
||||
len = cbm_json_escape(whole, sizeof(whole), "a\xC3\xA9");
|
||||
ASSERT_STR_EQ(whole, "a\xC3\xA9");
|
||||
ASSERT_EQ(len, 3);
|
||||
|
||||
/* A four-byte sequence cut after two bytes is dropped whole. */
|
||||
char emoji[3];
|
||||
len = cbm_json_escape(emoji, sizeof(emoji), "\xF0\x9F\x98\x80z");
|
||||
ASSERT_STR_EQ(emoji, "");
|
||||
ASSERT_EQ(len, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── SNPRINTF_APPEND tests ────────────────────────────────────── */
|
||||
|
||||
TEST(snprintf_append_basic) {
|
||||
@@ -547,6 +571,7 @@ SUITE(str_util) {
|
||||
RUN_TEST(validate_shell_arg_spaces);
|
||||
/* JSON Escaping */
|
||||
RUN_TEST(json_escape_control_chars);
|
||||
RUN_TEST(json_escape_never_splits_utf8);
|
||||
/* SNPRINTF_APPEND */
|
||||
RUN_TEST(snprintf_append_basic);
|
||||
RUN_TEST(snprintf_append_fills_exactly);
|
||||
|
||||
Reference in New Issue
Block a user