Merge origin/main into codex/fix-2816-trace-recursion

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-31 01:00:09 +01:00
commit dd96a98494
36 files changed
+1667 -90

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
{
"name": "codebase-memory",
"description": "The codebase-memory-mcp code knowledge graph server, as a Claude Code plugin.",
"owner": {
"name": "DeusData",
"url": "https://github.com/DeusData"
},
"plugins": [
{
"name": "codebase-memory",
"source": "./plugin",
"description": "Codebase knowledge graph for AI agents: sub-ms queries, 99% fewer tokens."
}
]
}
+2
View File
@@ -33,6 +33,8 @@ Thumbs.db
# MCP config (user-local, generated by install command)
.mcp.json
# ...except the Claude Code plugin's server definition, which is published
!plugin/.mcp.json
# MCP Registry auth tokens
.mcpregistry_*
+24
View File
@@ -578,6 +578,30 @@ Working in a clone? Use `.` in place of the flake URL, e.g. `nix run .#codebase-
You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
```
### Install via Claude Code plugin
This repository is also a Claude Code plugin marketplace, so you can find and
add the server from inside Claude Code:
```bash
claude plugin marketplace add DeusData/codebase-memory-mcp
claude plugin install codebase-memory@codebase-memory
```
The plugin registers the MCP server only. It starts the server with
`npx -y codebase-memory-mcp@<version>`, pinned to the current release; the first
start downloads that release's binary through the npm package, which verifies
it against the release checksums. The skill, the three graph agents and the
context hooks are not part of the plugin: they come from
`codebase-memory-mcp install`.
Use one or the other. `install` registers the same server for Claude Code, so
with both you run it twice, under two tool namespaces
(`mcp__codebase-memory-mcp__*` and
`mcp__plugin_codebase-memory_codebase-memory-mcp__*`). Choose `install` for the
full Claude Code setup; choose the plugin to add just the server without
running an installer.
### Build from Source
<details>
+11 -3
View File
@@ -298,9 +298,10 @@ typedef struct {
* are bound to the owning result's source identity before cross-file use. */
uint32_t test_name_start_byte, test_name_end_byte;
uint32_t test_body_start_byte, test_body_end_byte;
/* Documents only: facts a document definition adds to its node's
* properties, as a NULL-terminated key, value, key, value, ... list (an
* ADR's id, status, date, deciders, title). Keys are fixed literals; the
/* Facts a definition adds to its node's properties, as a NULL-terminated
* key, value, key, value, ... list: a document's (an ADR's id, status,
* date, deciders, title), or CBM_PROP_GENERATED on a member the compiler
* writes and the source never spells. Keys are fixed literals; the
* values are escaped where the properties are written. Tail field:
* zero-init stays valid. */
const char **extra_props;
@@ -321,6 +322,13 @@ typedef struct {
const char *alias_target;
} CBMDefinition;
/* extra_props key of a generated member, one the compiler writes and the
* source never spells: a Java record's implicit accessor carries
* `generated: record`. The node keeps the property, and the pipeline registers
* such a definition by qualified name only, so no short-name guess can land on
* it (pipeline.h cbm_def_props_generated). */
#define CBM_PROP_GENERATED "generated"
/* Argument captured from a call expression */
typedef struct {
const char *expr; // raw expression text ("payload.info", "MY_URL", "'hello'")
+120
View File
@@ -230,6 +230,7 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
const CBMLangSpec *spec);
static TSNode find_class_body(TSNode class_node, CBMLanguage lang);
static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *class_qn);
static void extract_record_accessors(CBMExtractCtx *ctx, TSNode node, const char *class_qn);
static void extract_elixir_call(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec);
// --- Helpers ---
@@ -8317,6 +8318,10 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s
extract_enum_members(ctx, node, class_qn);
}
if (ctx->language == CBM_LANG_JAVA && strcmp(kind, "record_declaration") == 0) {
extract_record_accessors(ctx, node, class_qn);
}
// Extract methods inside the class
int methods_from = ctx->result->defs.count;
extract_class_methods(ctx, node, class_qn, spec);
@@ -9642,6 +9647,121 @@ static void extract_enum_members(CBMExtractCtx *ctx, TSNode node, const char *cl
}
}
/* Whether the record body declares this accessor itself (JLS §8.10.3 lets a
* record override any of them), so the implicit one is not emitted twice. */
static bool record_declares_accessor(CBMExtractCtx *ctx, TSNode node, const char *comp_name) {
TSNode body = find_class_body(node, ctx->language);
if (ts_node_is_null(body)) {
return false;
}
uint32_t mc = ts_node_named_child_count(body);
for (uint32_t mi = 0; mi < mc; mi++) {
TSNode member = ts_node_named_child(body, mi);
if (strcmp(ts_node_type(member), "method_declaration") != 0) {
continue;
}
TSNode mname = ts_node_child_by_field_name(member, TS_FIELD("name"));
if (ts_node_is_null(mname)) {
continue;
}
char *name = cbm_node_text(ctx->arena, mname, ctx->source);
if (!name || strcmp(name, comp_name) != 0) {
continue;
}
/* Same name, no parameters — that is the accessor. */
TSNode params = ts_node_child_by_field_name(member, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return true;
}
/* Comments are named extras, not parameters. */
bool has_parameters = false;
uint32_t pc = ts_node_named_child_count(params);
for (uint32_t pi = 0; pi < pc; pi++) {
if (!ts_node_is_extra(ts_node_named_child(params, pi))) {
has_parameters = true;
break;
}
}
if (!has_parameters) {
return true;
}
}
return false;
}
/* Java records: the compiler writes one accessor per component, so `p.x()`
* calls a method that appears nowhere in the source. Without a definition for
* it the call has nothing to resolve to and falls through to the project-wide
* name registry, which binds it to the first `x` in the tree — typically an
* unrelated class's private field. Emitting the accessors makes the record's
* own API visible, exactly as the language defines it (JLS §8.10.3). */
static void extract_record_accessors(CBMExtractCtx *ctx, TSNode node, const char *class_qn) {
CBMArena *a = ctx->arena;
TSNode params = ts_node_child_by_field_name(node, TS_FIELD("parameters"));
if (ts_node_is_null(params)) {
return;
}
uint32_t pc = ts_node_named_child_count(params);
for (uint32_t pi = 0; pi < pc; pi++) {
TSNode comp = ts_node_named_child(params, pi);
bool spread = strcmp(ts_node_type(comp), "spread_parameter") == 0;
if (!spread && strcmp(ts_node_type(comp), "formal_parameter") != 0) {
continue;
}
TSNode cname = ts_node_child_by_field_name(comp, TS_FIELD("name"));
TSNode ctype = ts_node_child_by_field_name(comp, TS_FIELD("type"));
if (spread) {
/* Java varargs have an unnamed type child and a nested declarator. */
TSNode decl = cbm_find_child_by_kind(comp, "variable_declarator");
if (ts_node_is_null(decl)) {
continue;
}
cname = ts_node_child_by_field_name(decl, TS_FIELD("name"));
uint32_t nc = ts_node_named_child_count(comp);
for (uint32_t ci = 0; ci < nc; ci++) {
TSNode child = ts_node_named_child(comp, ci);
if (!ts_node_is_extra(child) && strcmp(ts_node_type(child), "modifiers") != 0) {
ctype = child;
break;
}
}
}
if (ts_node_is_null(cname)) {
continue;
}
char *comp_name = cbm_node_text(a, cname, ctx->source);
if (!comp_name || !comp_name[0] || record_declares_accessor(ctx, node, comp_name)) {
continue;
}
CBMDefinition adef;
memset(&adef, 0, sizeof(adef));
adef.name = comp_name;
adef.qualified_name = cbm_arena_sprintf(a, "%s.%s", class_qn, comp_name);
adef.label = "Method";
adef.file_path = ctx->rel_path;
adef.start_line = ts_node_start_point(comp).row + TS_LINE_OFFSET;
adef.end_line = ts_node_end_point(comp).row + TS_LINE_OFFSET;
adef.lines = 1;
adef.parent_class = class_qn;
adef.is_exported = true;
if (!ts_node_is_null(ctype)) {
const char *type = cbm_node_text(a, ctype, ctx->source);
adef.return_type = spread && type ? cbm_arena_sprintf(a, "%s[]", type) : type;
}
/* The compiler writes this member and the source never spells it: its
* node says so, and the pipeline registers it by qualified name only,
* so no short-name guess can land on it. */
const char **kv = (const char **)cbm_arena_alloc(a, CBM_SZ_3 * sizeof(char *));
if (kv) {
kv[0] = CBM_PROP_GENERATED;
kv[SKIP_ONE] = "record";
kv[PAIR_LEN] = NULL;
}
adef.extra_props = kv;
cbm_defs_push(&ctx->result->defs, a, adef);
}
}
/* Resolve the identifier node from a destructure pattern child.
* pair_pattern → value field; shorthand/identifier → itself; others → first named child. */
static TSNode destructure_ident(TSNode pat_child) {
+49 -6
View File
@@ -6179,10 +6179,36 @@ static void py_split_field_defs(CBMArena *arena, const char *field_defs, const c
*types_out = types;
}
/* Qualified names of reg->types[0..noted), for a caller that registers into a
* registry it does not finalize until the end (cbm_py_build_cross_registry):
* that registry's own lookup scans every type. qns == NULL probes the
* registry instead. */
typedef struct {
CBMHashTable *qns;
int noted;
} PyKnownTypes;
/* Note the types added since the last call. Run after every add_type, so an
* add that failed silently cannot desync set and registry. If the set cannot
* grow it is dropped and probes fall back to the registry. */
static void py_note_known_types(PyKnownTypes *known, const CBMTypeRegistry *reg) {
for (; known && known->qns && known->noted < reg->type_count; known->noted++) {
const char *qn = reg->types[known->noted].qualified_name;
if (!qn || cbm_ht_has(known->qns, qn))
continue;
cbm_ht_set(known->qns, qn, (void *)(uintptr_t)(known->noted + 1));
if (!cbm_ht_has(known->qns, qn)) {
cbm_ht_free(known->qns);
known->qns = NULL;
}
}
}
/* Build a registry from CBMLSPDef[] supplied by the caller — covers both
* the source file's own defs and cross-file referenced defs. */
* the source file's own defs and cross-file referenced defs. known is NULL
* except for the shared Tier-2 build. */
static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
CBMLSPDef *defs, int def_count) {
CBMLSPDef *defs, int def_count, PyKnownTypes *known) {
/* Pass 1: types only — the method pass probes the registry per Method def
* (receiver auto-registration), which is a LINEAR scan pre-finalize:
* O(methods x types) per file (same quadratic as php_register_lsp_defs;
@@ -6206,6 +6232,7 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
}
py_split_field_defs(arena, d->field_defs, &rt.field_names, &rt.field_types);
cbm_registry_add_type(reg, rt);
py_note_known_types(known, reg);
}
}
/* idx_arena == NULL skips the mid-build finalize: the tier-2 builder
@@ -6259,13 +6286,17 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type; /* borrowed */
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
bool recv_known = known && known->qns
? cbm_ht_has(known->qns, rf.receiver_type)
: cbm_registry_lookup_type(reg, rf.receiver_type) != NULL;
if (!recv_known) {
CBMRegisteredType auto_t;
memset(&auto_t, 0, sizeof(auto_t));
auto_t.qualified_name = rf.receiver_type;
const char *dot = strrchr(d->receiver_type, '.');
auto_t.short_name = dot ? dot + 1 : rf.receiver_type; /* borrowed substring */
cbm_registry_add_type(reg, auto_t);
py_note_known_types(known, reg);
}
}
cbm_registry_add_func(reg, rf);
@@ -6277,7 +6308,7 @@ static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
#ifdef CBM_ENABLE_TEST_SEAMS
bool cbm_py_lsp_test_register_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs,
int def_count) {
return py_register_lsp_defs(arena, NULL, reg, defs, def_count);
return py_register_lsp_defs(arena, NULL, reg, defs, def_count, NULL);
}
#endif
@@ -6774,7 +6805,7 @@ CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, in
py_lsp_init_for_stage(&ctx, arena, source, source_len, &reg, module_qn, out, "cross");
if (ctx.eval_failure)
goto cross_cleanup;
if (!py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count)) {
if (!py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count, NULL)) {
ctx.eval_failure = CBM_LSP_SCOPE_FAILED;
goto cross_cleanup;
}
@@ -6813,15 +6844,27 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
cbm_registry_init(reg, arena);
cbm_python_stdlib_register(reg, arena);
/* The registry is not finalized until the end, so probing it for each
* Method's receiver scanned every type: O(methods x types), single-threaded
* (~54 s of lsp_cross_prepare on a large Python tree). Probe a set of
* the registered type QNs instead; registration order is unchanged. Sized
* for the stdlib seed, not def_count (the mixed all-language count). NULL
* on allocation failure: the registry probe runs as before. */
PyKnownTypes known = {cbm_ht_create((uint32_t)reg->type_count), 0};
py_note_known_types(&known, reg);
/* Filter to Python defs only — defs[] is mixed-language all_defs. */
for (int i = 0; i < def_count; i++) {
CBMLSPDef *d = &defs[i];
if (d->lang != CBM_LANG_PYTHON)
continue;
/* Reuse the existing register fn on a single-def slice (n=1 inline). */
if (!py_register_lsp_defs(arena, NULL, reg, d, 1))
if (!py_register_lsp_defs(arena, NULL, reg, d, 1, &known)) {
cbm_ht_free(known.qns);
return NULL; /* caller owns the shared arena and other registries */
}
}
cbm_ht_free(known.qns);
py_mark_ambiguous_callable_bindings(reg, arena);
cbm_registry_build_type_qn_order(reg, arena);
+9
View File
@@ -0,0 +1,9 @@
{
"name": "codebase-memory",
"version": "0.12.1",
"description": "Codebase knowledge graph for AI agents: sub-ms queries, 99% fewer tokens.",
"author": {
"name": "DeusData",
"url": "https://github.com/DeusData"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"mcpServers": {
"codebase-memory-mcp": {
"type": "stdio",
"command": "npx",
"args": ["-y", "codebase-memory-mcp@0.12.1"]
}
}
}
+3 -6
View File
@@ -20,6 +20,7 @@
#include "daemon/runtime.h"
#include "daemon/version_cohort.h"
#include "foundation/compat.h"
#include "foundation/compat_fs.h"
#include "foundation/platform.h"
#include "foundation/constants.h"
#include "foundation/json_writer.h"
@@ -2718,12 +2719,8 @@ int cbm_remove_zed_mcp_owned(const char *binary_path, const char *config_path) {
/* ── Agent detection ──────────────────────────────────────────── */
static bool dir_exists(const char *path) {
struct stat st;
#ifndef _WIN32
return lstat(path, &st) == 0 && S_ISDIR(st.st_mode);
#else
return stat(path, &st) == 0 && S_ISDIR(st.st_mode);
#endif
cbm_path_info_t info = {0};
return cbm_path_info_utf8(path, &info) == CBM_PATH_INFO_OK && info.is_directory;
}
/* Resolve the Claude Code config dir.
+3 -2
View File
@@ -65,8 +65,9 @@ CBMLanguage cbm_disambiguate_res(const char *path);
CBMLanguage cbm_disambiguate_d(const char *path);
/* Disambiguate .inc files by reading first 4KB of content.
* Returns CBM_LANG_OBJECTSCRIPT_ROUTINE if it looks like an ObjectScript
* include (a "ROUTINE <Uppercase>" header), otherwise CBM_LANG_BITBAKE.
* Returns CBM_LANG_CPP for explicit C-family code, or
* CBM_LANG_OBJECTSCRIPT_ROUTINE for ObjectScript markers / a lone #define.
* BitBake recipe syntax and unknown content retain CBM_LANG_BITBAKE.
* On read failure, defaults to CBM_LANG_BITBAKE. */
CBMLanguage cbm_disambiguate_inc(const char *path);
+137 -25
View File
@@ -1573,38 +1573,150 @@ CBMLanguage cbm_disambiguate_cls(const char *path) {
return lang_cls_text(buf);
}
/* Disambiguate .inc files: shared by BitBake include fragments and
* InterSystems ObjectScript include (macro) files. ObjectScript .inc files are
* predominantly macro definitions ("#define NAME ..." / "#def1arg NAME ...");
* some also carry a "ROUTINE <Name>" header. The macro-preprocessor directives
* are the strongest signal because that is the primary content of an .inc file,
* whereas BitBake uses '#' only for "# comment" lines (always '#' + space).
* We therefore match ObjectScript preprocessor directives ('#' immediately
* followed by 'def'/';'), which BitBake never produces. Defaults to BitBake on
* any doubt (preserves existing behaviour). */
/* A complete leading token, followed by whitespace or punctuation, within a
* single line. In particular #include_extra is not #include. */
static const char *lang_inc_word(const char *line, const char *end, const char *word) {
size_t n = strlen(word);
if ((size_t)(end - line) < n || strncmp(line, word, n) != 0 ||
(line + n < end && (isalnum((unsigned char)line[n]) || line[n] == '_'))) {
return NULL;
}
line += n;
while (line < end && isspace((unsigned char)*line)) {
line++;
}
return line;
}
/* Recipes can embed C in quoted strings or shell/Python tasks. Recognize the
* recipe's own syntax before inspecting those bodies as standalone C code. */
static bool lang_inc_recipe_line(const char *line, const char *end) {
static const char *const words[] = {"require", "include", "inherit", "addtask", "python", NULL};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail > line + strlen(words[i])) {
return true;
}
}
if ((size_t)(end - line) >= SLEN("do_") && strncmp(line, "do_", SLEN("do_")) == 0) {
return true;
}
const char *p = line;
while (p < end && (isalnum((unsigned char)*p) || *p == '_' || *p == ':' || *p == '[' ||
*p == ']' || *p == '-' || *p == '.')) {
p++;
}
if (p == line) {
return false;
}
while (p < end && isspace((unsigned char)*p)) {
p++;
}
return p < end && (*p == '=' || ((*p == '?' || *p == '+' || *p == ':' || *p == '.') &&
p + SKIP_ONE < end && p[SKIP_ONE] == '='));
}
/* Skip only leading whitespace and C comments, keeping block-comment state
* across lines. Recipe strings and bodies are handled before looking for C. */
static const char *lang_inc_code(const char *line, const char *end, bool *block_comment) {
while (line < end) {
if (*block_comment) {
const char *close = strstr(line, "*/");
if (!close || close >= end) {
return end;
}
line = close + SLEN("*/");
*block_comment = false;
continue;
}
if (isspace((unsigned char)*line)) {
line++;
continue;
}
if ((size_t)(end - line) >= SLEN("/*") && strncmp(line, "/*", SLEN("/*")) == 0) {
*block_comment = true;
line += SLEN("/*");
continue;
}
break;
}
return line;
}
static bool lang_inc_c_code(const char *line, const char *end) {
static const char *const words[] = {
"extern", "static", "typedef", "namespace", "template",
"struct", "class", "int", "void", NULL,
};
for (int i = 0; words[i]; i++) {
const char *tail = lang_inc_word(line, end, words[i]);
if (tail && tail < end && *tail != '=') {
return true;
}
}
const char *tail = lang_inc_word(line, end, "if");
return tail && tail < end && *tail == '(';
}
static bool lang_inc_directive(const char *line, const char *end, bool *c_family,
bool *objectscript_define) {
line += SKIP_ONE; /* '#' */
while (line < end && isspace((unsigned char)*line)) {
line++;
}
if (lang_inc_word(line, end, "def1arg") || (line < end && *line == ';')) {
return true; /* Explicit ObjectScript directive. */
}
if (lang_inc_word(line, end, "define")) {
*objectscript_define = true;
}
const char *tail = lang_inc_word(line, end, "include");
if (tail && tail < end && (*tail == '"' || *tail == '<')) {
*c_family = true;
}
return false;
}
/* .inc is shared by C/C++, BitBake and ObjectScript. Select C++ for explicit
* C-family code (including C fragments); do not infer an enclosing include
* context. A lone #define and unknown content retain their existing defaults. */
static CBMLanguage lang_inc_text(const char *buf) {
bool c_family = false;
bool objectscript_define = false;
bool block_comment = false;
const char *line = buf;
while (*line) {
/* ObjectScript include header: a line beginning "ROUTINE <Uppercase>". */
if (strncmp(line, "ROUTINE ", SLEN("ROUTINE ")) == 0 &&
isupper((unsigned char)line[SLEN("ROUTINE ")])) {
const char *end = strchr(line, '\n');
if (!end) {
end = line + strlen(line);
}
const char *code = lang_inc_code(line, end, &block_comment);
line = *end ? end + SKIP_ONE : end;
if (code == end) {
continue;
}
const char *tail = lang_inc_word(code, end, "ROUTINE");
if (tail && tail < end && isupper((unsigned char)*tail)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
/* ObjectScript macro directives — the primary content of .inc files.
* "#define"/"#def1arg" (macro defs) and "#;" (line comment). BitBake's
* only '#' use is "# comment" (hash + space), so these never collide. */
if (strncmp(line, "#define", SLEN("#define")) == 0 ||
strncmp(line, "#def1arg", SLEN("#def1arg")) == 0 ||
strncmp(line, "#;", SLEN("#;")) == 0) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
if (*code == '#') {
if (lang_inc_directive(code, end, &c_family, &objectscript_define)) {
return CBM_LANG_OBJECTSCRIPT_ROUTINE;
}
continue;
}
const char *nl = strchr(line, '\n');
if (!nl) {
break;
if ((size_t)(end - code) >= SLEN("//") && strncmp(code, "//", SLEN("//")) == 0) {
continue;
}
line = nl + SKIP_ONE;
if (!c_family && lang_inc_recipe_line(code, end)) {
return CBM_LANG_BITBAKE;
}
c_family = c_family || lang_inc_c_code(code, end);
}
return CBM_LANG_BITBAKE;
if (c_family) {
return CBM_LANG_CPP;
}
return objectscript_define ? CBM_LANG_OBJECTSCRIPT_ROUTINE : CBM_LANG_BITBAKE;
}
CBMLanguage cbm_disambiguate_inc(const char *path) {
@@ -1775,7 +1887,7 @@ CBMLanguage cbm_language_classify_with(const cbm_userconfig_t *config, const cha
/* ObjectScript UDL, Apex or a Visual Basic 6 class module */
lang = readable ? lang_cls_text(buf) : CBM_LANG_APEX;
} else if (strcmp(ext, ".inc") == 0) {
/* BitBake or an ObjectScript include */
/* C/C++, BitBake or an ObjectScript include */
lang = readable ? lang_inc_text(buf) : CBM_LANG_BITBAKE;
} else if (cfc) {
/* script or tag dialect */
+2 -4
View File
@@ -8280,12 +8280,10 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t
*scanned_edges = ecount;
scc_graph_t g;
if (!scc_build(src, tgt, ecount, &g)) {
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
return CBM_STORE_OK; /* no edges = no cycles, not an error */
}
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
int *comp = malloc((size_t)g.nverts * sizeof(int));
if (!comp) {
+12 -4
View File
@@ -1281,10 +1281,18 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy(
if (strcmp(cand->label, "Function") != 0 && strcmp(cand->label, "Method") != 0) {
continue;
}
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
/* A generated member (a Java record's implicit accessor) is registered
* by qualified name only: it joins through its owner's Class.method
* tail below, never on its leaf name alone -- `Objects.nonNull(x)` is
* no call of some record's nonNull(). Nor does it make a same-named
* callable ambiguous: the leaf fallback sees the graph as if it were
* not there. */
if (!cbm_node_props_generated(cand->properties_json)) {
if (!unique_callable) {
unique_callable = cand;
} else if (unique_callable->id != cand->id) {
callable_ambiguous = true;
}
}
if (!cbm_pipeline_qn_class_method_tail_eq(cand->qualified_name, callee_tail)) {
continue;
+11
View File
@@ -997,6 +997,11 @@ static bool calls_emit_lsp_target(cbm_pipeline_ctx_t *ctx, const CBMCall *call,
if (!target_node || source_node->id == target_node->id) {
return false;
}
/* LSP textual fallbacks can also name data, as with static imports. The
* call is handled: no edge, and no registry fallback after it. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return true;
}
cbm_resolution_t res = {0};
/* Keep the canonical graph QN, including project prefixes added by fallback. */
res.qualified_name = target_node->qualified_name;
@@ -1333,6 +1338,12 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
return calls_emit_route_call(ctx, call, source_node, module_qn, imp_keys, imp_vals,
imp_count, route_mount, lang, evidence);
}
/* A Java call never targets data — see cbm_java_suppress_call_to_data_member.
* The language gate lives here, as with the guards above, and MUST match
* pass_parallel.c exactly or the two resolvers diverge. */
if (cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
return 0;
}
evidence->candidate_qn = target_node->qualified_name;
evidence->calls_emitted |=
emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys,
+8 -3
View File
@@ -509,10 +509,15 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
* (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
* through the same predicate, so the three registries cannot diverge. */
* through the same predicate, so the three registries cannot diverge.
* A generated member is registered by qualified name only, at all three. */
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
const cbm_gbuf_node_t *file_node = cbm_gbuf_find_by_qn(ctx->gbuf, file_qn);
+14 -3
View File
@@ -1811,10 +1811,15 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
return 0;
}
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
* (helpers.c) — see pass_definitions.c for the per-label rationale, and
* for the generated member registered by qualified name only. */
if (cbm_label_is_registry_symbol(def->label)) {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name, def->label,
lang, module_prefix_len);
if (cbm_def_props_generated(def->extra_props)) {
cbm_registry_add_qn_only(ctx->registry, def->qualified_name, def->label);
} else {
cbm_registry_add_lang_in_module(ctx->registry, def->name, def->qualified_name,
def->label, lang, module_prefix_len);
}
(*reg_entries)++;
}
char marked[CBM_SZ_4K];
@@ -3618,6 +3623,12 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
imp_keys, imp_vals, imp_count, route_mount, lang);
continue;
}
if (target_node && source_node->id != target_node->id &&
cbm_java_suppress_call_to_data_member(lang == CBM_LANG_JAVA, target_node->label)) {
/* Same guard as pass_calls.c — a Java call never targets a
* Variable or a Field. */
continue;
}
if (!target_node || source_node->id == target_node->id) {
/* HTTP/ASYNC calls to an EXTERNAL client library (`requests.get(url)`)
* resolve to an unindexed QN (target_node == NULL), but their edge
+9
View File
@@ -2604,6 +2604,15 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
if (import_candidate_is_python_local(ctx->gbuf, cand)) {
continue;
}
/* Nor is a generated member (a Java record's implicit
* accessor, an instance method no import can name). It is
* registered by qualified name only, and a package segment
* that merely spells it (`javax.crypto.spec` -> spec())
* must not bind the import -- or every use of the
* imported name -- to it. */
if (cbm_node_props_generated(cand->properties_json)) {
continue;
}
if (source_file_qn && cand->qualified_name &&
strcmp(cand->qualified_name, source_file_qn) == 0) {
continue; /* self */
+25
View File
@@ -278,6 +278,24 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
const char *qualified_name, const char *label,
CBMLanguage lang, size_t module_prefix_len);
/* Register a symbol for exact (qualified-name) lookups only: it is never
* offered to the short-name strategies (unique_name, suffix_match,
* field_type_hint, fuzzy). For a generated member (cbm_def_props_generated),
* which the source never spells, so only a type-aware resolver that knows its
* owner may bind it. The three graph registration sites route such a symbol
* here instead of cbm_registry_add_lang_in_module. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label);
/* True when a definition's extra_props (key, value, ..., NULL) carry the
* generated-member key (cbm.h CBM_PROP_GENERATED), e.g. a Java record's
* implicit accessor. Pure; unit-tested in test_registry.c. */
bool cbm_def_props_generated(const char *const *extra_props);
/* The stored-node twin of cbm_def_props_generated, for the incremental
* registry seed: true when a node's properties JSON carries that key as a
* string property. Pure; unit-tested in test_registry.c. */
bool cbm_node_props_generated(const char *properties_json);
/* True when code in `caller` can call a symbol defined in `target` by bare
* name: same language, same interop family (JS/TS + script hosts, C/C++/CUDA/
* ObjC, JVM, .NET, ...; the table lives in registry.c), or Swift with the C
@@ -512,6 +530,13 @@ bool cbm_c_member_rule_file(CBMLanguage lang, const char *rel_path);
* a member); NULL when none or several do. */
const char *cbm_registry_unique_field_qn(const cbm_registry_t *r, const char *member_name);
/* A Java call expression never names data: `p.x()` is a method invocation
* whatever `x` spells elsewhere in the project. Drops a CALLS bind whose
* target is a Variable or Field. Java only — Kotlin properties and C function
* pointers are callable names that are not methods. Pure; unit-tested in
* test_registry.c. */
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label);
/* Get the label of a qualified name, or NULL if not found. */
const char *cbm_registry_label_of(const cbm_registry_t *r, const char *qn);
+49 -5
View File
@@ -590,6 +590,39 @@ static char *delta_owner_props(const char *project, const char *name, const char
return out;
}
/* `props` (a JSON object) with a generated member's mark (cbm.h
* CBM_PROP_GENERATED) added, or NULL when `generated` is NULL/empty
* (CBM_MEM_CLASS_OTHER; the caller frees). */
static char *delta_generated_props(const char *props, const char *generated) {
if (!generated || !generated[0]) {
return NULL;
}
size_t pl = props ? strlen(props) : 0;
bool has_members = pl > PAIR_LEN && props[0] == '{' && props[pl - SKIP_ONE] == '}';
size_t cap = pl + PAIR_LEN * strlen(generated) + CBM_SZ_32;
char *out = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, cap);
if (!out) {
return NULL;
}
size_t k = 0;
if (has_members) {
memcpy(out, props, pl - SKIP_ONE);
k = pl - SKIP_ONE;
out[k++] = ',';
} else {
out[k++] = '{';
}
k += (size_t)snprintf(out + k, cap - k, "\"%s\":\"", CBM_PROP_GENERATED);
for (const char *p = generated; *p && k + CBM_SZ_3 < cap; p++) {
if (*p == '"' || *p == '\\') {
out[k++] = '\\';
}
out[k++] = *p;
}
snprintf(out + k, cap - k, "\"}");
return out;
}
int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *gbuf) {
sqlite3 *db = cbm_store_get_db(store);
if (!db) {
@@ -634,7 +667,12 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
* goes by -- a Go method's QN has no receiver, a class-body variable's QN
* no class. A document naming `Order.Reset` binds such a node through
* its owner (doc_links_pdf.c), so a re-resolved document needs it too.
* Only where owner + name differs from the QN (delta_owner_props). */
* Only where owner + name differs from the QN (delta_owner_props).
*
* And a generated member's mark (a Java record's implicit accessor, a
* Method node): the registry seed reads it to register the member by
* qualified name only, as the full build does (cbm_node_props_generated);
* without it the proxy came back into the short-name index. */
sqlite3_stmt *stmt = NULL;
if (sqlite3_prepare_v2(
db,
@@ -646,7 +684,10 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
" start_line, end_line,"
" CASE WHEN label IN ('Method','Variable')"
" AND instr(properties, '\"parent_class\"') > 0"
" THEN json_extract(properties, '$.parent_class') END"
" THEN json_extract(properties, '$.parent_class') END,"
" CASE WHEN label = 'Method'"
" AND instr(properties, '\"" CBM_PROP_GENERATED "\"') > 0"
" THEN json_extract(properties, '$." CBM_PROP_GENERATED "') END"
" FROM nodes"
" WHERE project = ?1 AND label NOT IN"
" ('Macro','Comment','Section','Branch','Commit','Tag')"
@@ -670,13 +711,16 @@ int64_t cbm_delta_preseed(cbm_store_t *store, const char *project, cbm_gbuf_t *g
int start_line = sqlite3_column_int(stmt, 6);
int end_line = sqlite3_column_int(stmt, 7);
const char *parent = (const char *)sqlite3_column_text(stmt, 8);
const char *generated = (const char *)sqlite3_column_text(stmt, 9);
char *owner_props =
parent ? delta_owner_props(project, name, qn, parent, client_props) : NULL;
const char *props = owner_props ? owner_props : (client_props ? client_props : "{}");
char *generated_props = delta_generated_props(props, generated);
/* Pin the gbuf id to the database id: proxies ARE their rows. */
cbm_gbuf_set_next_id(gbuf, id);
int64_t got =
cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line, end_line,
owner_props ? owner_props : (client_props ? client_props : "{}"));
int64_t got = cbm_gbuf_upsert_node(gbuf, label, name, qn, fp ? fp : "", start_line,
end_line, generated_props ? generated_props : props);
cbm_free(CBM_MEM_CLASS_OTHER, generated_props);
cbm_free(CBM_MEM_CLASS_OTHER, owner_props);
if (got != id) {
/* A QN collision inside the preseed set would silently split
+6
View File
@@ -1353,6 +1353,12 @@ static void registry_visitor(const cbm_gbuf_node_t *node, void *userdata) {
if (!incr_label_is_registry_symbol(node->label)) {
return;
}
/* A generated member stays out of the short-name index here as in the
* full index (pass_definitions.c); its node carries the mark. */
if (cbm_node_props_generated(node->properties_json)) {
cbm_registry_add_qn_only(seed->registry, node->qualified_name, node->label);
return;
}
CBMLanguage lang = cbm_registry_synthetic_path_language(node->file_path);
if (lang == CBM_LANG_COUNT && seed->lang_by_path && node->file_path) {
void *v = cbm_ht_get(seed->lang_by_path, node->file_path);
+50
View File
@@ -1370,6 +1370,30 @@ bool cbm_c_member_binds_by_owner(bool is_c, bool is_member_access) {
return is_c && is_member_access;
}
bool cbm_java_suppress_call_to_data_member(bool is_java, const char *target_label) {
/* A Java call expression never names data. `p.x()` is a method invocation
* whatever `x` spells elsewhere in the tree, so a CALLS edge into a
* Variable or a Field is a spelling collision and not a call.
*
* The bind is reached when the type-aware resolver finds no method to
* return: a record's accessors are implicit, so `record Point(double x,
* double y, double z)` declares no `x()` for java_lookup_method to find,
* and every `p.x()` falls through to the short-name registry — which
* matches the first `x` anywhere in the project, typically an unrelated
* class's private field. Measured on a 600-file Java tree (2026-09-22):
* 278 of 2444 CALLS edges landed on fields that way, and the callers of
* one four-field class absorbed every record read in the project.
*
* Java-gated, like the Go guard above, because the veto is only sound
* where no callable name can be anything but a method: Kotlin properties
* hold function values (`val f: () -> Unit; f()`) and C has function
* pointers, so both legitimately call through data. */
if (!is_java || !target_label) {
return false;
}
return strcmp(target_label, "Variable") == 0 || strcmp(target_label, "Field") == 0;
}
/* ── Lifecycle ──────────────────────────────────────────────────── */
cbm_registry_t *cbm_registry_new(void) {
@@ -1629,6 +1653,32 @@ void cbm_registry_add_lang_in_module(cbm_registry_t *r, const char *name,
}
}
/* A generated member (a Java record's implicit accessor) is a definition the
* source never spells: no call names it except through its owner's type. The
* exact map holds it for the LSP targets and the import/same-module lookups,
* as it holds a Python function's locals; the by-name index does not, so no
* unique_name / suffix_match / field_type_hint / fuzzy guess can land on it. */
void cbm_registry_add_qn_only(cbm_registry_t *r, const char *qualified_name, const char *label) {
if (!r || !qualified_name || !label) {
return;
}
(void)registry_add_exact(r, qualified_name, label);
}
bool cbm_def_props_generated(const char *const *extra_props) {
for (const char *const *kv = extra_props; kv && kv[0] && kv[SKIP_ONE]; kv += PAIR_LEN) {
if (strcmp(kv[0], CBM_PROP_GENERATED) == 0) {
return true;
}
}
return false;
}
bool cbm_node_props_generated(const char *properties_json) {
/* The key as the definition passes write it: "generated":"<value>". */
return properties_json && strstr(properties_json, "\"" CBM_PROP_GENERATED "\":\"") != NULL;
}
/* ── Lookup ──────────────────────────────────────────────────────── */
bool cbm_registry_exists(const cbm_registry_t *r, const char *qn) {
+36 -6
View File
@@ -4165,10 +4165,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
bind_text(stmt, SKIP_ONE, project);
sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND);
/* Both arrays are STORE-class memory-core blocks: the caller releases them
* with cbm_store_free_call_edges, the only free that matches. */
int cap = ST_INIT_CAP_16;
int n = 0;
int64_t *src = malloc((size_t)cap * sizeof(int64_t));
int64_t *tgt = malloc((size_t)cap * sizeof(int64_t));
int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
if (!src || !tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
int scan_rc;
while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) {
if (max_edges > 0 && n >= max_edges) {
@@ -4178,9 +4186,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
break;
}
if (n >= cap) {
size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t);
int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes);
if (grown_src) {
src = grown_src;
}
int64_t *grown_tgt =
grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL;
if (grown_tgt) {
tgt = grown_tgt;
}
if (!grown_src || !grown_tgt) {
sqlite3_finalize(stmt);
cbm_store_free_call_edges(src, tgt);
store_set_error(s, "fetch_call_edges: out of memory");
return CBM_STORE_ERR;
}
cap *= ST_GROWTH;
src = safe_realloc(src, (size_t)cap * sizeof(int64_t));
tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t));
}
src[n] = sqlite3_column_int64(stmt, 0);
tgt[n] = sqlite3_column_int64(stmt, 1);
@@ -4188,8 +4210,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
}
sqlite3_finalize(stmt);
if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) {
free(src);
free(tgt);
cbm_store_free_call_edges(src, tgt);
store_set_error_sqlite(s, "fetch_call_edges scan");
return CBM_STORE_ERR;
}
@@ -4199,6 +4220,11 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
return CBM_STORE_OK;
}
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) {
cbm_free(CBM_MEM_CLASS_STORE, src);
cbm_free(CBM_MEM_CLASS_STORE, tgt);
}
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out,
int *count) {
bind_id_t b = {source_id};
@@ -10647,6 +10673,10 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *
return cbm_leiden(nodes, node_count, edges, edge_count, 1.0, out, out_count);
}
void cbm_leiden_free(cbm_louvain_result_t *result) {
cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */
}
/* ── Architecture: community clusters via Leiden ───────────────── */
enum {
+9 -2
View File
@@ -708,12 +708,16 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e);
int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count);
/* Fetch all CALLS edges among Function/Method nodes for a project as parallel
* (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis.
* (source_id, target_id) arrays, released together with
* cbm_store_free_call_edges. For SCC / cycle analysis.
* Stops at max_edges and sets *truncated — never a silent cap. Returns
* CBM_STORE_OK (or _ERR); *count is the number returned. */
int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges,
int64_t **out_src, int64_t **out_tgt, int *count, bool *truncated);
/* Free the (source_id, target_id) arrays returned by cbm_store_fetch_call_edges. */
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt);
/* Find edges by source node. */
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count);
@@ -1306,7 +1310,7 @@ typedef struct {
* reported community is internally connected. The resolution parameter
* controls granularity (higher -> more, smaller communities); 1.0 is standard.
* Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER;
* the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */
* the caller releases it with cbm_leiden_free(*out). */
int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count);
@@ -1314,6 +1318,9 @@ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *e
int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
int edge_count, cbm_louvain_result_t **out, int *out_count);
/* Free the result array returned by cbm_leiden / cbm_louvain. */
void cbm_leiden_free(cbm_louvain_result_t *result);
/* ── Memory management helpers ──────────────────────────────────── */
/* Free heap-allocated strings in a stack-allocated node (does NOT free the node itself). */
+5 -5
View File
@@ -2042,7 +2042,7 @@ static bool attach_missed_graph(yyjson_mut_doc *mdoc, yyjson_mut_val *mroot, cbm
return false;
}
yyjson_doc *mldoc = yyjson_read(mjson, strlen(mjson), 0);
free(mjson);
cbm_layout_free_json(mjson);
if (!mldoc) {
return false;
}
@@ -2164,7 +2164,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
return;
}
cbm_http_replyf(c, 200, g_cors_json, "%s", scoped_json);
free(scoped_json);
cbm_layout_free_json(scoped_json);
return;
}
@@ -2200,13 +2200,13 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
if (linked_count == 0 && missed_graph) {
cbm_store_close(store);
cbm_http_replyf(c, 200, g_cors_json, "%s", primary_json);
free(primary_json);
cbm_layout_free_json(primary_json);
return;
}
/* Parse primary JSON and append missed_graph + linked_projects */
yyjson_doc *pdoc = yyjson_read(primary_json, strlen(primary_json), 0);
free(primary_json);
cbm_layout_free_json(primary_json);
if (!pdoc) {
cbm_store_close(store);
cbm_http_replyf(c, 500, g_cors_json, "{\"error\":\"JSON parse failed\"}");
@@ -2258,7 +2258,7 @@ static void handle_layout(cbm_http_conn_t *c, const cbm_http_req_t *req) {
/* Parse linked project layout */
yyjson_doc *lpdoc = yyjson_read(lp_json, strlen(lp_json), 0);
free(lp_json);
cbm_layout_free_json(lp_json);
if (!lpdoc) {
cbm_store_close(lp_store);
free(linked[li]);
+9 -2
View File
@@ -14,6 +14,7 @@
#include "ui/layout3d.h"
#include "ui/layout_internal.h"
#include "foundation/log.h"
#include "foundation/json_writer.h"
#include "foundation/mem_core.h"
#include "foundation/platform.h"
@@ -899,10 +900,12 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
yyjson_mut_obj_add_val(doc, root, "edges", ea);
yyjson_mut_obj_add_int(doc, root, "total_nodes", r->total_nodes);
/* The output string is an OTHER-class memory-core block, released by
* cbm_layout_free_json; the document keeps its own allocator. */
size_t len = 0;
yyjson_write_err write_err = {0};
char *json =
yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE, NULL, &len, &write_err);
char *json = yyjson_mut_write_opts(doc, YYJSON_WRITE_ALLOW_INVALID_UNICODE,
cbm_json_writer_allocator(), &len, &write_err);
yyjson_mut_doc_free(doc);
if (!json) {
char code[CBM_SZ_32];
@@ -912,3 +915,7 @@ char *cbm_layout_to_json(const cbm_layout_result_t *r) {
}
return json;
}
void cbm_layout_free_json(char *json) {
cbm_free(CBM_MEM_CLASS_OTHER, json);
}
+4 -1
View File
@@ -69,9 +69,12 @@ cbm_layout_result_t *cbm_layout_compute(cbm_store_t *store, const char *project,
/* Release a computed layout result and its fields through their matching allocators. */
void cbm_layout_free(cbm_layout_result_t *result);
/* Serialize layout result to JSON string. Caller must free(). */
/* Serialize layout result to JSON string. Release with cbm_layout_free_json(). */
char *cbm_layout_to_json(const cbm_layout_result_t *result);
/* Free the JSON string returned by cbm_layout_to_json. */
void cbm_layout_free_json(char *json);
/* ── CBM Atlas region level ───────────────────────────────────── */
/* level=regions payload: one body per region (Leiden communities voted per
+229
View File
@@ -35,6 +35,40 @@ static int crc_edge_count(cbm_store_t *store, const char *project, const char *t
return matches;
}
/* Edges of `type` out of `source_name` whose target's qualified name ends with
* `target_qn_suffix`. Two methods in one project can share a short name, and
* then only the qualified name tells them apart. */
static int crc_edge_count_to_qn(cbm_store_t *store, const char *project, const char *type,
const char *source_name, const char *target_qn_suffix) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
if (cbm_store_find_edges_by_type(store, project, type, &edges, &edge_count) != CBM_STORE_OK) {
return -1;
}
int matches = 0;
size_t suffix_len = strlen(target_qn_suffix);
for (int i = 0; i < edge_count; i++) {
cbm_node_t source = {0};
cbm_node_t target = {0};
bool source_ok =
cbm_store_find_node_by_id(store, edges[i].source_id, &source) == CBM_STORE_OK;
bool target_ok =
cbm_store_find_node_by_id(store, edges[i].target_id, &target) == CBM_STORE_OK;
if (source_ok && target_ok && source.name && target.qualified_name &&
strcmp(source.name, source_name) == 0) {
size_t qn_len = strlen(target.qualified_name);
if (qn_len >= suffix_len &&
strcmp(target.qualified_name + qn_len - suffix_len, target_qn_suffix) == 0) {
matches++;
}
}
cbm_node_free_fields(&source);
cbm_node_free_fields(&target);
}
cbm_store_free_edges(edges, edge_count);
return matches;
}
static int crc_global_handler_edge_count(cbm_store_t *store, const char *project,
const char *caller) {
int references =
@@ -423,6 +457,195 @@ TEST(call_reference_go_ambiguous_promoted_method_stays_usage) {
PASS();
}
TEST(call_java_record_accessor_binds_the_record) {
/* A record's accessors are implicit, so `record Point(double x, ...)`
* declares no `x()` in its source. Extraction emits them, and `other.x()`
* binds to the record's own accessor.
*
* Two things must not happen instead. The call must not reach the
* project-wide short-name registry and land on the first `x` in the tree —
* here an unrelated class's private field. And the guard that refuses such
* data binds must not swallow a real method that merely shares the
* spelling: the builder's `x(int)`, declared beside its own `x` field,
* keeps its edge. */
static const RFile files[] = {
{"Point.java", "package com.example;\n"
"public record Point(double x, double y, double z) {\n"
" public double first(Point other) {\n"
" return Maths.abs(other.x()) + new Builder().x(5).made();\n"
" }\n"
"}\n"},
{"Spring.java", "package com.example;\n"
"public final class Spring {\n"
" private double x;\n"
" public void put(double v) { this.x = v; }\n"
"}\n"},
{"Builder.java", "package com.example;\n"
"public final class Builder {\n"
" private int x;\n"
" public Builder x(int v) { this.x = v; return this; }\n"
" public int made() { return x; }\n"
"}\n"},
{"Maths.java", "package com.example;\n"
"public final class Maths {\n"
" public static double abs(double v) { return v < 0 ? -v : v; }\n"
"}\n"}};
RProj project;
cbm_store_t *store = rh_index_files(&project, files, 4);
ASSERT_NOT_NULL(store);
int fabricated_field = crc_edge_count(store, project.project, "CALLS", "first", "Field", "x");
int fabricated_var = crc_edge_count(store, project.project, "CALLS", "first", "Variable", "x");
int genuine = crc_edge_count(store, project.project, "CALLS", "first", "Method", "abs");
int accessor = crc_edge_count_to_qn(store, project.project, "CALLS", "first", ".Point.x");
int builder = crc_edge_count_to_qn(store, project.project, "CALLS", "first", ".Builder.x");
rh_cleanup(&project, store);
ASSERT_EQ(fabricated_field, 0);
ASSERT_EQ(fabricated_var, 0);
ASSERT_EQ(genuine, 1);
ASSERT_EQ(accessor, 1);
ASSERT_EQ(builder, 1);
PASS();
}
/* Static imports can name both an inherited method and a field. Thread's
* activeCount() is external to this project, whereas Owner.activeCount is
* local data; an unresolved import must not turn the field into a callee. */
static int crc_java_static_import_data_call_count(bool parallel) {
RFile files[52] = {{"example/Owner.java", "package example;\n"
"public class Owner extends Thread {\n"
" public static int activeCount;\n"
"}\n"},
{"example/Caller.java", "package example;\n"
"import static example.Owner.activeCount;\n"
"class Caller {\n"
" int caller() { return activeCount(); }\n"
"}\n"}};
char names[50][64];
char sources[50][64];
int count = parallel ? 52 : 2;
for (int i = 2; i < count; i++) {
snprintf(names[i - 2], sizeof(names[i - 2]), "example/Pad%d.java", i);
snprintf(sources[i - 2], sizeof(sources[i - 2]), "package example; class Pad%d {}\n", i);
files[i].name = names[i - 2];
files[i].content = sources[i - 2];
}
RProj project;
cbm_store_t *store = rh_index_files(&project, files, count);
if (!store) {
return -1;
}
int fields = crc_edge_count(store, project.project, "CALLS", "caller", "Field", "activeCount");
int vars = crc_edge_count(store, project.project, "CALLS", "caller", "Variable", "activeCount");
rh_cleanup(&project, store);
return fields < 0 || vars < 0 ? -1 : fields + vars;
}
TEST(call_java_static_import_does_not_call_field_sequential) {
ASSERT_EQ(crc_java_static_import_data_call_count(false), 0);
PASS();
}
TEST(call_java_static_import_does_not_call_field_parallel) {
ASSERT_EQ(crc_java_static_import_data_call_count(true), 0);
PASS();
}
TEST(call_java_record_varargs_accessor_has_array_return) {
static const char *sources[] = {
"record Values(String... xs) {}\n", "record Values(String[]... xs) {}\n",
"record Values(int... xs) {}\n", "record Values(@Deprecated String... xs) {}\n",
"record Values(String... xs) { public String[] xs() { return xs; } }\n"};
static const char *return_types[] = {"String[]", "String[][]", "int[]", "String[]", "String[]"};
for (int fixture = 0; fixture < 5; fixture++) {
CBMFileResult *result =
cbm_extract_file(sources[fixture], (int)strlen(sources[fixture]), CBM_LANG_JAVA,
"repro", "Values.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int accessors = 0;
bool array_return = false;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "xs") == 0 && strcmp(def->label, "Method") == 0) {
accessors++;
array_return =
def->return_type && strcmp(def->return_type, return_types[fixture]) == 0;
}
}
cbm_free_result(result);
ASSERT_EQ(accessors, 1);
ASSERT_TRUE(array_return);
}
PASS();
}
/* Comment extras in an otherwise empty parameter list must not create a
* second, implicit accessor at the record component's source location. */
TEST(call_java_record_commented_accessor_has_only_explicit_span) {
static const char *sources[] = {"record Value(int x) {\n"
" public int x(/* explanation */) {\n"
" return x;\n"
" }\n"
"}\n",
"record Value(int x) {\n"
" public int x(// explanation\n"
" ) {\n"
" return x;\n"
" }\n"
"}\n"};
static const int end_lines[] = {4, 5};
for (int fixture = 0; fixture < 2; fixture++) {
CBMFileResult *result =
cbm_extract_file(sources[fixture], (int)strlen(sources[fixture]), CBM_LANG_JAVA,
"repro", "Value.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int accessors = 0;
int explicit_spans = 0;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "x") == 0 && strcmp(def->label, "Method") == 0) {
accessors++;
if (def->start_line == 2 && def->end_line == end_lines[fixture]) {
explicit_spans++;
}
}
}
cbm_free_result(result);
ASSERT_EQ(accessors, 1);
ASSERT_EQ(explicit_spans, 1);
}
PASS();
}
TEST(call_java_record_commented_overload_keeps_implicit_accessor) {
static const char source[] =
"record Value(int x) {\n"
" public int x(/* explanation */ int scale) { return x * scale; }\n"
"}\n";
CBMFileResult *result = cbm_extract_file(source, (int)strlen(source), CBM_LANG_JAVA, "repro",
"Value.java", 0, NULL, NULL);
ASSERT_NOT_NULL(result);
int methods = 0;
int implicit_spans = 0;
int overload_spans = 0;
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition *def = &result->defs.items[i];
if (strcmp(def->name, "x") == 0 && strcmp(def->label, "Method") == 0) {
methods++;
if (def->start_line == 1 && def->end_line == 1) {
implicit_spans++;
}
if (def->start_line == 2 && def->end_line == 2) {
overload_spans++;
}
}
}
cbm_free_result(result);
ASSERT_EQ(methods, 2);
ASSERT_EQ(implicit_spans, 1);
ASSERT_EQ(overload_spans, 1);
PASS();
}
SUITE(call_reference_contract) {
RUN_TEST(call_reference_typescript_direct_argument_is_exact);
RUN_TEST(call_reference_kotlin_alias_argument_is_exact);
@@ -439,4 +662,10 @@ SUITE(call_reference_contract) {
RUN_TEST(call_reference_python_later_decorated_method_rebinding_stays_usage);
RUN_TEST(call_reference_go_bound_method_argument_is_exact);
RUN_TEST(call_reference_go_ambiguous_promoted_method_stays_usage);
RUN_TEST(call_java_record_accessor_binds_the_record);
RUN_TEST(call_java_record_varargs_accessor_has_array_return);
RUN_TEST(call_java_record_commented_accessor_has_only_explicit_span);
RUN_TEST(call_java_record_commented_overload_keeps_implicit_accessor);
RUN_TEST(call_java_static_import_does_not_call_field_sequential);
RUN_TEST(call_java_static_import_does_not_call_field_parallel);
}
+24
View File
@@ -15281,6 +15281,29 @@ TEST(cli_detect_agents_finds_zed_in_roaming_appdata) {
}
#endif
#ifdef _WIN32
/* #2145: a profile path outside the ANSI code page must stay visible to agent
* detection. The directory is created with th_mkdir_p, which goes through the
* wide-char API: test_mkdirp uses cbm_mkdir, the narrow _mkdir on Windows,
* which would create a code-page-mangled name instead of this one. Cursor is
* detected by its directory alone, so the assertion goes through dir_exists. */
TEST(cli_detect_agents_handles_non_ascii_windows_profile) {
char home[1024];
snprintf(home, sizeof(home), "%s/cbm-用户-profile", cbm_tmpdir());
test_rmdir_r(home);
char cursor_dir[1024];
snprintf(cursor_dir, sizeof(cursor_dir), "%s/.cursor", home);
if (th_mkdir_p(cursor_dir) != 0)
FAIL("could not create non-ASCII Windows profile path");
cbm_detected_agents_t agents = cbm_detect_agents(home);
test_rmdir_r(home);
if (!agents.cursor)
FAIL("agent detection must support non-ASCII Windows profile paths");
PASS();
}
#endif
TEST(cli_detect_agents_finds_antigravity) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-detect-XXXXXX");
@@ -19351,6 +19374,7 @@ SUITE(cli) {
#endif
#ifdef _WIN32
RUN_TEST(cli_detect_agents_finds_zed_in_roaming_appdata);
RUN_TEST(cli_detect_agents_handles_non_ascii_windows_profile);
#endif
RUN_TEST(cli_detect_agents_finds_antigravity);
RUN_TEST(cli_detect_agents_finds_kilocode);
+24
View File
@@ -251,6 +251,29 @@ TEST(extract_c_macros_issue375) {
PASS();
}
TEST(extract_cpp_inc_linkage_and_callback) {
CBMFileResult *r =
extract("#define SAMPLE_CALLBACK(type) type\n"
"extern \"C\" __declspec(dllexport) int sample_export() { return 0; }\n"
"static SAMPLE_CALLBACK(int) sample_callback() { return sample_export(); }\n",
CBM_LANG_CPP, "sample", "runtime/sample.inc");
ASSERT_NOT_NULL(r);
ASSERT_TRUE(has_def(r, "Function", "sample_export"));
ASSERT_TRUE(has_def(r, "Function", "sample_callback"));
ASSERT_FALSE(has_def_any(r, "extern"));
cbm_free_result(r);
/* A statement include has no enclosing function in the file. Keep any
* syntax coverage diagnostic rather than inventing a callable owner. */
r = extract("#ifdef ENABLE_SAMPLE\nif (sample_check()) return 1;\n#endif\n", CBM_LANG_CPP,
"sample", "runtime/guard.inc");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(has_def_any(r, "extern"));
ASSERT_FALSE(has_def(r, "Function", "if"));
cbm_free_result(r);
PASS();
}
TEST(extract_cpp_macros_issue375) {
CBMFileResult *r = extract("#define MAX(a, b) ((a) > (b) ? (a) : (b))\n"
"#define PI 3.14159\n"
@@ -13559,6 +13582,7 @@ SUITE(extraction) {
RUN_TEST(traversal_stack_not_in_result_arena_issue2010);
RUN_TEST(extract_c_macro_hidden_call_survives_preprocessed_pass_issue2010);
RUN_TEST(extract_c_macros_issue375);
RUN_TEST(extract_cpp_inc_linkage_and_callback);
RUN_TEST(extract_cpp_macros_issue375);
RUN_TEST(extract_cpp_functionlike_macro_type_arg_no_false_parse_partial_issue1071);
RUN_TEST(extract_cpp_real_in_body_error_still_flagged_issue1071);
+50
View File
@@ -1457,6 +1457,55 @@ static CBMLanguage classify_content(const char *name, const char *content, size_
typedef CBMLanguage (*path_probe_fn)(const char *path);
/* Exercise the shared classifier and both disk entry points. All sources are
* synthetic; the recipe bodies deliberately contain misleading C-like code. */
TEST(lang_inc_c_family_and_existing_dialects) {
static const struct {
const char *content;
CBMLanguage expected;
} cases[] = {
{"#include \"sample.h\"\nextern \"C\" int sample_export() { return 0; }\n", CBM_LANG_CPP},
{"extern \"C\" __declspec(dllexport) int sample_export() { return 0; }\n", CBM_LANG_CPP},
{"#define SAMPLE_CALLBACK(T) T\nstatic SAMPLE_CALLBACK(int) sample() { return 0; }\n",
CBM_LANG_CPP},
{"/* in-function fragment */\n#ifdef SAMPLE_ENABLED\nif (check()) return 1;\n#endif\n",
CBM_LANG_CPP},
{"int sample(void) { return 0; }\n", CBM_LANG_CPP},
{" \t# include <stdint.h>\r\n", CBM_LANG_CPP},
{"/* example:\nextern \"C\" int example();\n*/\nSUMMARY = \"sample\"\n", CBM_LANG_BITBAKE},
{"// extern \"C\" example only\n", CBM_LANG_BITBAKE},
{"#include_extra \"sample.h\"\n", CBM_LANG_BITBAKE},
{"#define Value 1\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#ifndef SAMPLE\n#define SAMPLE 1\n#endif\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"ROUTINE Sample [Type=INC]\n#define Value 1\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#def1arg Value(%x) %x\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#; ObjectScript comment\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"#include <sample.h>\n#def1arg Value(%x) %x\n", CBM_LANG_OBJECTSCRIPT_ROUTINE},
{"# recipe\nSUMMARY = \"sample\"\nrequire base.inc\n", CBM_LANG_BITBAKE},
{"SRC_URI += \"file://sample.c\"\ndo_compile() {\nif (check) { return; }\n}\n",
CBM_LANG_BITBAKE},
{"python do_generate() {\nif (ready):\n pass\n}\n", CBM_LANG_BITBAKE},
{"do_compile() {\ncat <<'EOF'\n#include <stdio.h>\nstatic int sample;\nEOF\n}\n",
CBM_LANG_BITBAKE},
{"require base.inc\nextern \"C\" example in recipe text\n", CBM_LANG_BITBAKE},
{"", CBM_LANG_BITBAKE},
};
char path[512];
snprintf(path, sizeof(path), "%s/cbm_lang_inc_routing.inc", cbm_tmpdir());
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
size_t len = strlen(cases[i].content);
ASSERT_EQ(classify_content("sample.inc", cases[i].content, len), cases[i].expected);
ASSERT_TRUE(write_exact(path, cases[i].content, len));
CBMLanguage by_probe = cbm_disambiguate_inc(path);
CBMLanguage by_file = cbm_language_for_file("sample.inc", path);
remove(path);
ASSERT_EQ(by_probe, cases[i].expected);
ASSERT_EQ(by_file, cases[i].expected);
}
ASSERT_EQ(cbm_disambiguate_inc(NULL), CBM_LANG_BITBAKE);
PASS();
}
TEST(lang_probe_bytes_follow_the_name) {
ASSERT_EQ(cbm_language_probe_bytes("main.go"), 0);
ASSERT_EQ(cbm_language_probe_bytes("Makefile"), 0);
@@ -1879,6 +1928,7 @@ SUITE(language) {
RUN_TEST(lang_ext_sosl);
RUN_TEST(lang_all_have_names);
RUN_TEST(lang_inc_c_family_and_existing_dialects);
RUN_TEST(lang_probe_bytes_follow_the_name);
RUN_TEST(lang_classify_matches_every_content_rule);
RUN_TEST(lang_classify_unreadable_content_keeps_the_name_default);
+315
View File
@@ -18688,6 +18688,285 @@ TEST(pipeline_python_nested_def_local_after_incremental_issue1548) {
PASS();
}
/* A Java record's implicit accessors are GENERATED members (#2295): Method
* nodes marked `generated: record`, registered by qualified name only, as a
* Python function's locals are. The type-aware resolver binds `p.y()` on a
* Point to Point.y; no short-name guess may land on a member the source never
* spells. `far` calls y() on a type outside the project, so the project-wide
* name registry is its only route to Point.y. `data` calls x(), whose other
* spelling is Spring's private field: neither the accessor (a name guess) nor
* the field (a Java call never names data) is its target. The record's
* hand-written z() and norm() are source members: no `generated` mark.
* `org.external.y.Vector` is not in the project, and its package segment `y`
* spells the accessor: the import's symbol-name fallback must not bind it
* there (no IMPORTS edge), or `new Vector()` in `make` would follow the import
* onto the accessor (elasticsearch: `import javax.crypto.spec.PBEKeySpec`
* bound a record's `spec()`). `stat` calls, and `refs` passes, the JDK's
* Objects.nonNull: no project callable is its target, and Seen's accessor --
* the only project callable called nonNull -- must not be taken for it on its
* leaf name alone (elasticsearch: `Objects.nonNull(x)` and
* `filter(Objects::nonNull)` bound a test record's nonNull()). */
static const char jgen_point_java[] =
"package com.example;\n"
"public record Point(double x, double y, double z) {\n"
" public double z() { return z; }\n"
" public double norm() { return Math.sqrt(x * x + y * y + z * z); }\n"
"}\n";
static const char jgen_seen_java[] = "package com.example;\n"
"record Seen(boolean nonNull) {}\n";
static const char jgen_spring_java[] = "package com.example;\n"
"public final class Spring {\n"
" private double x;\n"
" public void put(double v) { this.x = v; }\n"
"}\n";
static const char jgen_caller_java[] =
"package com.example;\n"
"import java.util.List;\n"
"import java.util.Objects;\n"
"import org.external.y.Vector;\n"
"public final class Caller {\n"
" public double near(Point p) { return p.y(); }\n"
" public double far(Vector v) { return v.y(); }\n"
" public double data(Vector v) { return v.x(); }\n"
" public Object make() { return new Vector(); }\n"
" public boolean stat(Object o) { return Objects.nonNull(o); }\n"
" public long refs(List<Object> xs) {\n"
" return xs.stream().filter(Objects::nonNull).count();\n"
" }\n"
"}\n";
/* The id of the one `label` node called `name` in `file_path`; 0 when there is
* none or more than one. `generated` (optional) receives whether its
* properties carry the `generated: record` mark. */
static int64_t jgen_node(cbm_store_t *s, const char *project, const char *name, const char *label,
const char *file_path, bool *generated) {
cbm_node_t *nodes = NULL;
int count = 0;
int64_t id = 0;
int hits = 0;
bool marked = false;
if (cbm_store_find_nodes_by_name(s, project, name, &nodes, &count) != CBM_STORE_OK) {
return 0;
}
for (int i = 0; i < count; i++) {
if (nodes[i].label && strcmp(nodes[i].label, label) == 0 && nodes[i].file_path &&
strcmp(nodes[i].file_path, file_path) == 0) {
hits++;
id = nodes[i].id;
marked = nodes[i].properties_json &&
strstr(nodes[i].properties_json, "\"generated\":\"record\"") != NULL;
}
}
cbm_store_free_nodes(nodes, count);
if (generated) {
*generated = marked;
}
return hits == 1 ? id : 0;
}
/* CALLS edges out of `source_id` into `target_id`, or (target_id 0) into any
* node called `target_name`. */
static int jgen_calls(cbm_store_t *s, int64_t source_id, int64_t target_id,
const char *target_name) {
cbm_edge_t *edges = NULL;
int ec = 0;
int n = 0;
if (cbm_store_find_edges_by_source_type(s, source_id, "CALLS", &edges, &ec) != CBM_STORE_OK) {
return -1;
}
for (int i = 0; i < ec; i++) {
if (target_id) {
n += edges[i].target_id == target_id;
continue;
}
cbm_node_t t = {0};
if (cbm_store_find_node_by_id(s, edges[i].target_id, &t) == CBM_STORE_OK && t.name &&
strcmp(t.name, target_name) == 0) {
n++;
}
cbm_node_free_fields(&t);
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
return n;
}
/* Edges of `type` into `target_id`. */
static int jgen_edges_into(cbm_store_t *s, int64_t target_id, const char *type) {
cbm_edge_t *edges = NULL;
int ec = 0;
if (cbm_store_find_edges_by_target_type(s, target_id, type, &edges, &ec) != CBM_STORE_OK) {
return -1;
}
if (edges) {
cbm_store_free_edges(edges, ec);
}
return ec;
}
/* One fact of jgen_check: reported, counted, and the check goes on, so a run
* shows every fact that does not hold. */
#define JGEN_EXPECT(cond) \
do { \
if (!(cond)) { \
printf(" FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
failed++; \
} \
} while (0)
/* Every fact the fixture pins, on one indexed store; the number that fail. */
static int jgen_check(cbm_store_t *s, const char *project) {
int failed = 0;
bool gen_x = false;
bool gen_y = false;
bool gen_z = true;
bool gen_norm = true;
bool gen_nn = false;
int64_t acc_nn = jgen_node(s, project, "nonNull", "Method", "Seen.java", &gen_nn);
int64_t acc_x = jgen_node(s, project, "x", "Method", "Point.java", &gen_x);
int64_t acc_y = jgen_node(s, project, "y", "Method", "Point.java", &gen_y);
int64_t own_z = jgen_node(s, project, "z", "Method", "Point.java", &gen_z);
int64_t norm = jgen_node(s, project, "norm", "Method", "Point.java", &gen_norm);
int64_t near_m = jgen_node(s, project, "near", "Method", "Caller.java", NULL);
int64_t far_m = jgen_node(s, project, "far", "Method", "Caller.java", NULL);
int64_t data = jgen_node(s, project, "data", "Method", "Caller.java", NULL);
int64_t make = jgen_node(s, project, "make", "Method", "Caller.java", NULL);
int64_t stat = jgen_node(s, project, "stat", "Method", "Caller.java", NULL);
int64_t refs = jgen_node(s, project, "refs", "Method", "Caller.java", NULL);
JGEN_EXPECT(acc_x > 0 && acc_y > 0 && own_z > 0 && norm > 0 && acc_nn > 0);
JGEN_EXPECT(near_m > 0 && far_m > 0 && data > 0 && make > 0 && stat > 0 && refs > 0);
/* The compiler's members are marked; the hand-written ones are not. */
JGEN_EXPECT(gen_x);
JGEN_EXPECT(gen_y);
JGEN_EXPECT(gen_nn);
JGEN_EXPECT(!gen_z);
JGEN_EXPECT(!gen_norm);
/* Bound by qualified name through the receiver's type ... */
JGEN_EXPECT(jgen_calls(s, near_m, acc_y, NULL) == 1);
/* ... and never by a short-name guess. */
JGEN_EXPECT(jgen_calls(s, far_m, acc_y, NULL) == 0);
JGEN_EXPECT(jgen_calls(s, far_m, 0, "y") == 0);
/* Neither the accessor nor Spring's field `x`. */
JGEN_EXPECT(jgen_calls(s, data, 0, "x") == 0);
/* No import names a generated member, so nothing follows one onto it. */
JGEN_EXPECT(jgen_edges_into(s, acc_y, "IMPORTS") == 0);
JGEN_EXPECT(jgen_calls(s, make, 0, "y") == 0);
/* The JDK's Objects.nonNull is not Seen's accessor, called or referenced. */
JGEN_EXPECT(jgen_calls(s, stat, acc_nn, NULL) == 0);
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "CALL_REFERENCE") == 0);
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "USAGE") == 0);
return failed;
}
#undef JGEN_EXPECT
static int jgen_write_fixture(const char *tmp) {
write_temp_file(tmp, "Point.java", jgen_point_java);
write_temp_file(tmp, "Seen.java", jgen_seen_java);
write_temp_file(tmp, "Spring.java", jgen_spring_java);
write_temp_file(tmp, "Caller.java", jgen_caller_java);
return 0;
}
TEST(pipeline_java_record_accessors_generated_qn_only) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen.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);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* The same fixture through pass_parallel.c: >= 50 files and CBM_WORKERS=4. */
TEST(pipeline_java_record_accessors_generated_qn_only_parallel) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "Filler%d.java", i);
snprintf(body, sizeof(body),
"package com.example;\nclass Filler%d { int f() { return %d; } }\n", i, i);
write_temp_file(tmp, name, body);
}
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
int run = p ? cbm_pipeline_run(p) : -1;
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
ASSERT_NOT_NULL(p);
ASSERT_EQ(run, 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* An incremental re-index seeds its registry from the stored graph: the
* untouched Point.java's accessors come back from their nodes, and must stay
* out of the short-name index exactly as in the full index while the touched
* Caller.java is re-resolved. */
TEST(pipeline_java_record_accessors_generated_qn_only_after_incremental) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_incr_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
jgen_write_fixture(tmp);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/java_gen_incr.db", tmp);
cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p1);
ASSERT_EQ(cbm_pipeline_run(p1), 0);
cbm_pipeline_free(p1);
char caller_path[512];
snprintf(caller_path, sizeof(caller_path), "%s/Caller.java", tmp);
ASSERT_EQ(th_append_file(caller_path, "\n// touched: re-resolve Caller.java incrementally\n"),
0);
cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p2);
ASSERT_EQ(cbm_pipeline_run(p2), 0);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
int rc = jgen_check(s, cbm_pipeline_project_name(p2));
cbm_store_close(s);
cbm_pipeline_free(p2);
th_rmtree(tmp);
ASSERT_EQ(rc, 0);
PASS();
}
/* As pyn_edge, and the edge's properties also contain `fragment`. */
static bool pyn_edge_with(cbm_store_t *s, const char *project, const char *src_rel,
const char *tgt_rel, const char *type, const char *fragment) {
@@ -28880,7 +29159,40 @@ TEST(pipeline_calls_args_overlong_argument_stops_cleanly) {
PASS();
}
/* Discovery must choose the C++ grammar before the pipeline extracts .inc
* symbols; a direct extractor-only test would miss that routing failure. */
TEST(pipeline_inc_c_family_fragments) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "%s/cbm_inc_cpp_XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(tmp));
write_temp_file(tmp, "runtime.inc",
"#define SAMPLE_CALLBACK(T) T\n"
"extern \"C\" int sample_export() { return 0; }\n"
"static SAMPLE_CALLBACK(int) sample_callback() { return sample_export(); }\n");
write_temp_file(tmp, "main.cpp", "#include \"runtime.inc\"\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/graph.db", tmp);
const cbm_index_mode_t modes[] = {CBM_MODE_FAST, CBM_MODE_FULL};
for (size_t i = 0; i < sizeof(modes) / sizeof(modes[0]); i++) {
cbm_pipeline_t *pipeline = cbm_pipeline_new(tmp, db_path, modes[i]);
ASSERT_NOT_NULL(pipeline);
ASSERT_EQ(cbm_pipeline_run(pipeline), 0);
cbm_store_t *store = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(store);
const char *project = cbm_pipeline_project_name(pipeline);
ASSERT_EQ(fixture_node_count(store, project, "runtime.inc", "sample_export", "Function"),
1);
ASSERT_EQ(fixture_node_count(store, project, "runtime.inc", "sample_callback", "Function"),
1);
ASSERT_EQ(named_node_count(store, project, "extern"), 0);
cbm_store_close(store);
cbm_pipeline_free(pipeline);
}
th_rmtree(tmp);
PASS();
}
SUITE(pipeline) {
RUN_TEST(pipeline_inc_c_family_fragments);
RUN_TEST(pipeline_python_module_callers_sequential);
RUN_TEST(pipeline_python_module_callers_parallel);
RUN_TEST(pipeline_python_class_body_calls_sequential);
@@ -29120,6 +29432,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_import_never_binds_a_local_def_issue1548);
RUN_TEST(pipeline_python_nested_defs_parallel_issue1548);
RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only_parallel);
RUN_TEST(pipeline_java_record_accessors_generated_qn_only_after_incremental);
RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548);
RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548);
RUN_TEST(pipeline_python_super_follows_c3_mro);
+140
View File
@@ -751,6 +751,91 @@ TEST(pylsp_fused_self_attr_chain_via_overlay) {
PASS();
}
/* Characterization of the shared Tier-2 build order. Defs register one at a
* time in defs[] order, so a Method seen before its Class auto-registers a
* placeholder receiver type that stays ahead of the Class entry, and a Method
* whose receiver is already known adds no type. Any change to how the build
* probes for known receivers must keep this exact sequence. */
TEST(pylsp_cross_registry_interleaved_order) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *base = cbm_py_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(base);
int tb = base->type_count, fb = base->func_count;
CBMLSPDef defs[] = {
{.qualified_name = "t.m.A.m",
.short_name = "m",
.label = "Method",
.receiver_type = "t.m.A",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
{.qualified_name = "t.m.A",
.short_name = "A",
.label = "Class",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
{.qualified_name = "t.m.B",
.short_name = "B",
.label = "Class",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
{.qualified_name = "t.m.B.f",
.short_name = "f",
.label = "Method",
.receiver_type = "t.m.B",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
{.qualified_name = "t.m.C.g",
.short_name = "g",
.label = "Method",
.receiver_type = "t.m.C",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
/* Receiver auto-registered by C.g just above: no second placeholder. */
{.qualified_name = "t.m.C.h",
.short_name = "h",
.label = "Method",
.receiver_type = "t.m.C",
.def_module_qn = "t.m",
.lang = CBM_LANG_PYTHON},
/* Non-Python defs are skipped by the builder. */
{.qualified_name = "t.m.D",
.short_name = "D",
.label = "Class",
.def_module_qn = "t.m",
.lang = CBM_LANG_GO},
};
CBMTypeRegistry *reg =
cbm_py_build_cross_registry(&arena, defs, (int)(sizeof(defs) / sizeof(defs[0])));
ASSERT_NOT_NULL(reg);
/* Types: placeholder A (from A.m), Class A, Class B, placeholder C. */
ASSERT_EQ(reg->type_count, tb + 4);
ASSERT_TRUE(reg->types[tb].qualified_name == defs[0].receiver_type);
ASSERT_STR_EQ(reg->types[tb].short_name, "A");
ASSERT_TRUE(reg->types[tb + 1].qualified_name == defs[1].qualified_name);
ASSERT_TRUE(reg->types[tb + 2].qualified_name == defs[2].qualified_name);
ASSERT_TRUE(reg->types[tb + 3].qualified_name == defs[4].receiver_type);
ASSERT_STR_EQ(reg->types[tb + 3].short_name, "C");
/* Funcs keep defs[] order. */
ASSERT_EQ(reg->func_count, fb + 4);
ASSERT_STR_EQ(reg->funcs[fb].qualified_name, "t.m.A.m");
ASSERT_STR_EQ(reg->funcs[fb + 1].qualified_name, "t.m.B.f");
ASSERT_STR_EQ(reg->funcs[fb + 2].qualified_name, "t.m.C.g");
ASSERT_STR_EQ(reg->funcs[fb + 2].receiver_type, "t.m.C");
ASSERT_STR_EQ(reg->funcs[fb + 3].qualified_name, "t.m.C.h");
const CBMRegisteredType *a = cbm_registry_lookup_type(reg, "t.m.A");
ASSERT_NOT_NULL(a);
ASSERT_STR_EQ(a->qualified_name, "t.m.A");
ASSERT_NULL(cbm_registry_lookup_type(reg, "t.m.D"));
cbm_arena_destroy(&arena);
PASS();
}
/* Issue #228: a class/static method invoked directly on a CROSS-FILE imported
* class name — ActionRecordX.build_from_text(...) — produced no CALLS edge, so
* the method showed in/out degree 0 and was flagged as dead code. Distinct from
@@ -2448,6 +2533,59 @@ TEST(pylsp_scale_work_is_linear) {
PASS();
}
/* #2413 (O9: no clock): the shared Tier-2 build registers one def at a time
* into a registry it finalizes only at the end, so a receiver probe that scans
* the types registered so far makes it O(methods x types). The registry
* primitives count every candidate they examine (CBM_LSP_WORK): 4x the classes
* must cost at most ~4x the work; the scanning probe lands at ~16x. */
static uint64_t cross_registry_work_for(int n_classes, int *out_types) {
CBMArena arena;
cbm_arena_init(&arena);
int n_defs = n_classes * 3;
CBMLSPDef *defs = (CBMLSPDef *)cbm_arena_calloc(&arena, (size_t)n_defs * sizeof(*defs));
if (!defs) {
cbm_arena_destroy(&arena);
*out_types = -1;
return 0;
}
/* Extraction order: each class followed by its two methods. */
for (int i = 0; i < n_classes; i++) {
CBMLSPDef *cls = &defs[i * 3];
cls->qualified_name = cbm_arena_sprintf(&arena, "scale.mod.Cls%d", i);
cls->short_name = cls->qualified_name + strlen("scale.mod.");
cls->label = "Class";
cls->def_module_qn = "scale.mod";
cls->lang = CBM_LANG_PYTHON;
for (int j = 1; j <= 2; j++) {
CBMLSPDef *m = &defs[i * 3 + j];
m->qualified_name = cbm_arena_sprintf(&arena, "%s.m%d", cls->qualified_name, j);
m->short_name = j == 1 ? "m1" : "m2";
m->label = "Method";
m->receiver_type = cls->qualified_name;
m->def_module_qn = "scale.mod";
m->lang = CBM_LANG_PYTHON;
}
}
(void)cbm_lsp_work_take();
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, defs, n_defs);
uint64_t work = cbm_lsp_work_take();
*out_types = reg ? reg->type_count : -1;
cbm_arena_destroy(&arena);
return work;
}
TEST(pylsp_cross_registry_build_work_is_linear) {
int t1 = 0, t4 = 0;
uint64_t w1 = cross_registry_work_for(1000, &t1);
uint64_t w4 = cross_registry_work_for(4000, &t4);
printf(" cross registry work: 1000=%llu (types=%d) 4000=%llu (types=%d)\n",
(unsigned long long)w1, t1, (unsigned long long)w4, t4);
/* Every class registered exactly once (no duplicate placeholder). */
ASSERT_EQ(t4 - t1, 3000);
ASSERT(w4 <= 5 * w1 + 1000);
PASS();
}
extern const TSLanguage *tree_sitter_python(void);
TEST(pylsp_issue1527_memo_allocation_failure_is_reported) {
@@ -4850,6 +4988,7 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_issue1527_long_binary_chain_is_not_nesting);
RUN_TEST(pylsp_issue1527_long_receiver_chain_is_not_nesting);
RUN_TEST(pylsp_scale_work_is_linear);
RUN_TEST(pylsp_cross_registry_build_work_is_linear);
/* Phase 2 — smoke */
RUN_TEST(pylsp_smoke_empty);
RUN_TEST(pylsp_smoke_one_function);
@@ -4886,6 +5025,7 @@ SUITE(py_lsp) {
RUN_TEST(pylsp_crossfile_receiver_through_typed_field);
RUN_TEST(pylsp_crossfile_typed_field_shared_registry);
RUN_TEST(pylsp_fused_self_attr_chain_via_overlay);
RUN_TEST(pylsp_cross_registry_interleaved_order);
RUN_TEST(pylsp_crossfile_classmethod_on_class_issue228);
RUN_TEST(pylsp_crossfile_inheritance);
RUN_TEST(pylsp_batch_two_files);
+69
View File
@@ -1434,6 +1434,72 @@ TEST(c_member_rule_covers_c_files_and_every_dot_h) {
PASS();
}
TEST(java_call_never_binds_data_member) {
/* A Java call expression never names data, so a CALLS bind that landed on
* a Variable or a Field is a spelling collision. The shape that produces
* it: a record's accessors are implicit, `p.x()` finds no declared `x()`,
* and the short-name registry hands back the first `x` in the tree — an
* unrelated class's private field. */
ASSERT_TRUE(cbm_java_suppress_call_to_data_member(true, "Field"));
ASSERT_TRUE(cbm_java_suppress_call_to_data_member(true, "Variable"));
/* Everything callable is kept, including the constructor's Class target. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Method"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Function"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, "Class"));
/* Other languages have callable data: a Kotlin property or a C function
* pointer is a name that is not a method and is called all the same. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(false, "Field"));
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(false, "Variable"));
/* Degenerate input → nothing to judge. */
ASSERT_FALSE(cbm_java_suppress_call_to_data_member(true, NULL));
PASS();
}
TEST(generated_member_registers_by_qn_only) {
/* A generated member (a Java record's implicit accessor) is found by its
* qualified name, never by a short-name guess. The same member registered
* the ordinary way is exactly what the guess binds. */
cbm_registry_t *r = cbm_registry_new();
cbm_registry_add_qn_only(r, "proj.geo.Point.Point.y", "Method");
ASSERT_TRUE(cbm_registry_exists(r, "proj.geo.Point.Point.y"));
ASSERT_STR_EQ(cbm_registry_label_of(r, "proj.geo.Point.Point.y"), "Method");
const char **qns = NULL;
int n = -1;
ASSERT_EQ(cbm_registry_find_by_name(r, "y", &qns, &n), 0);
ASSERT_EQ(n, 0);
cbm_resolution_t res = cbm_registry_resolve(r, "v.y", "proj.app.Caller", NULL, NULL, 0);
ASSERT_NULL(res.qualified_name);
cbm_registry_add(r, "x", "proj.geo.Point.Point.x", "Method");
res = cbm_registry_resolve(r, "v.x", "proj.app.Caller", NULL, NULL, 0);
ASSERT_STR_EQ(res.qualified_name, "proj.geo.Point.Point.x");
/* Degenerate input registers nothing. */
cbm_registry_add_qn_only(r, NULL, "Method");
cbm_registry_add_qn_only(r, "proj.geo.Point.Point.z", NULL);
cbm_registry_add_qn_only(NULL, "proj.geo.Point.Point.z", "Method");
ASSERT_EQ(cbm_registry_size(r), 2);
cbm_registry_free(r);
PASS();
}
TEST(generated_member_mark_on_defs_and_nodes) {
static const char *const record[] = {"generated", "record", NULL};
static const char *const later[] = {"source", "openapi", "generated", "lombok", NULL};
static const char *const doc[] = {"id", "ADR-7", "status", "accepted", NULL};
static const char *const keyless[] = {"generated", NULL};
ASSERT_TRUE(cbm_def_props_generated(record));
ASSERT_TRUE(cbm_def_props_generated(later));
ASSERT_FALSE(cbm_def_props_generated(doc));
ASSERT_FALSE(cbm_def_props_generated(keyless)); /* a key without a value is no fact */
ASSERT_FALSE(cbm_def_props_generated(NULL));
ASSERT_TRUE(
cbm_node_props_generated("{\"parent_class\":\"p.Point\",\"generated\":\"record\"}"));
ASSERT_FALSE(cbm_node_props_generated("{\"parent_class\":\"p.Point\"}"));
/* The word inside a value is not the key. */
ASSERT_FALSE(cbm_node_props_generated("{\"docstring\":\"code generated by protoc\"}"));
ASSERT_FALSE(cbm_node_props_generated(NULL));
PASS();
}
TEST(dynamic_suppress_drops_weak_method_matches) {
/* #592/#606/#1276: a member call whose receiver the LSP could not type, that
* landed via a WEAK short-name strategy, is generic-resolver noise → drop.
@@ -2597,6 +2663,9 @@ SUITE(registry) {
RUN_TEST(unique_field_qn_needs_exactly_one_field_of_that_name);
RUN_TEST(c_member_access_binds_by_owner_only);
RUN_TEST(c_member_rule_covers_c_files_and_every_dot_h);
RUN_TEST(java_call_never_binds_data_member);
RUN_TEST(generated_member_registers_by_qn_only);
RUN_TEST(generated_member_mark_on_defs_and_nodes);
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);
+135 -8
View File
@@ -1369,6 +1369,126 @@ TEST(adr_validate_keys_empty) {
PASS();
}
/* ── cbm_store_fetch_call_edges / cbm_store_free_call_edges tests ─ */
TEST(fetch_call_edges_basic) {
cbm_store_t *s = setup_arch_test_store();
ASSERT_NOT_NULL(s);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
/* setup_arch_test_store() wires 5 CALLS edges among Function nodes. */
ASSERT_EQ(count, 5);
ASSERT_FALSE(truncated);
ASSERT_NOT_NULL(src);
ASSERT_NOT_NULL(tgt);
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
TEST(fetch_call_edges_truncated) {
cbm_store_t *s = setup_arch_test_store();
ASSERT_NOT_NULL(s);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = false;
/* Only 5 CALLS edges exist; capping at 2 must set truncated. */
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 2, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, 2);
ASSERT_TRUE(truncated);
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
TEST(fetch_call_edges_empty_project) {
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "empty", "/tmp/empty");
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "empty", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, 0);
ASSERT_FALSE(truncated);
/* cbm_store_free_call_edges(NULL, NULL) must not crash. */
cbm_store_free_call_edges(src, tgt);
cbm_store_close(s);
PASS();
}
/* The two arrays cbm_store_fetch_call_edges hands out are memory-core blocks of
* the STORE class, and cbm_store_free_call_edges is what takes them back. Both
* halves are measured: the fetch adds exactly the two arrays and the paired free
* removes exactly those, so a raw allocation on either side fails. Thirty-six
* edges outgrow the initial capacity of 16, so the growth path is covered too. */
TEST(fetch_call_edges_memory_core_ownership) {
enum { FN = 37 };
cbm_store_t *s = cbm_store_open_memory();
ASSERT_NOT_NULL(s);
cbm_store_upsert_project(s, "grow", "/tmp/grow");
int64_t ids[FN];
for (int i = 0; i < FN; i++) {
char name[32];
char qn[64];
snprintf(name, sizeof(name), "f%d", i);
snprintf(qn, sizeof(qn), "grow.pkg.f%d", i);
cbm_node_t n = {.project = "grow",
.label = "Function",
.name = name,
.qualified_name = qn,
.file_path = "pkg/f.go"};
ids[i] = cbm_store_upsert_node(s, &n);
ASSERT_TRUE(ids[i] > 0);
}
for (int i = 0; i + 1 < FN; i++) {
cbm_edge_t e = {
.project = "grow", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
ASSERT_TRUE(cbm_store_insert_edge(s, &e) > 0);
}
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE);
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE);
int64_t *src = NULL;
int64_t *tgt = NULL;
int count = 0;
bool truncated = true;
ASSERT_EQ(cbm_store_fetch_call_edges(s, "grow", 100, &src, &tgt, &count, &truncated),
CBM_STORE_OK);
ASSERT_EQ(count, FN - 1);
ASSERT_FALSE(truncated);
ASSERT_NOT_NULL(src);
ASSERT_NOT_NULL(tgt);
/* Ordered by (source_id, target_id): growth kept every pair in place. */
ASSERT_EQ(src[0], ids[0]);
ASSERT_EQ(tgt[0], ids[1]);
ASSERT_EQ(src[FN - 2], ids[FN - 2]);
ASSERT_EQ(tgt[FN - 2], ids[FN - 1]);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks + 2);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE) >=
bytes + 2 * (size_t)count * sizeof(int64_t));
cbm_store_free_call_edges(src, tgt);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks);
cbm_store_close(s);
PASS();
}
/* ── Louvain tests ──────────────────────────────────────────────── */
TEST(louvain_basic) {
@@ -1394,7 +1514,7 @@ TEST(louvain_basic) {
/* Triangle and pair different */
ASSERT_TRUE(comm[1] != comm[4]);
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1403,7 +1523,7 @@ TEST(louvain_empty) {
int count = 0;
ASSERT_EQ(cbm_louvain(NULL, 0, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 0);
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1414,7 +1534,7 @@ TEST(louvain_single_node) {
ASSERT_EQ(cbm_louvain(nodes, 1, NULL, 0, &result, &count), CBM_STORE_OK);
ASSERT_EQ(count, 1);
ASSERT_EQ(result[0].node_id, 42);
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1477,7 +1597,7 @@ TEST(louvain_converges) {
}
ASSERT_TRUE(same_count >= 8);
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1502,7 +1622,8 @@ TEST(leiden_result_memory_core_ownership) {
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >=
bytes + 3 * sizeof(*result));
cbm_free(CBM_MEM_CLASS_OTHER, result);
/* Released through the exported pair, the way an FFI caller must. */
cbm_leiden_free(result);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
}
@@ -1649,7 +1770,7 @@ TEST(leiden_multilevel_collapses_noise) {
}
ASSERT_TRUE(same >= SZ - 1);
}
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1681,8 +1802,8 @@ TEST(leiden_resolution_controls_granularity) {
ASSERT_TRUE(n_hi > n_lo);
ASSERT_TRUE(leiden_all_communities_connected(lo, N, edges, ne));
ASSERT_TRUE(leiden_all_communities_connected(hi, N, edges, ne));
cbm_free(CBM_MEM_CLASS_OTHER, lo);
cbm_free(CBM_MEM_CLASS_OTHER, hi);
cbm_leiden_free(lo);
cbm_leiden_free(hi);
PASS();
}
@@ -1948,6 +2069,12 @@ SUITE(store_arch) {
RUN_TEST(adr_splice_refuses_unterminated_fence);
RUN_TEST(adr_validate_keys_empty);
/* Call-edge fetch / free */
RUN_TEST(fetch_call_edges_basic);
RUN_TEST(fetch_call_edges_truncated);
RUN_TEST(fetch_call_edges_empty_project);
RUN_TEST(fetch_call_edges_memory_core_ownership);
/* Louvain */
RUN_TEST(louvain_basic);
RUN_TEST(louvain_empty);
+37 -2
View File
@@ -556,7 +556,41 @@ TEST(layout_to_json) {
ASSERT(strstr(json, "\"hello\"") != NULL);
ASSERT(strstr(json, "\"Function\"") != NULL);
free(json);
cbm_layout_free_json(json);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
}
/* cbm_layout_to_json hands out one memory-core block of the OTHER class, and
* cbm_layout_free_json takes exactly that block back. A raw allocation on either
* side of the pair fails the count. */
TEST(layout_json_memory_core_ownership) {
cbm_store_t *store = cbm_store_open_memory();
ASSERT_NOT_NULL(store);
cbm_store_upsert_project(store, "test", "/tmp/test");
cbm_node_t n = {.project = "test",
.label = "Function",
.name = "hello",
.qualified_name = "test::hello",
.file_path = "a.c",
.start_line = 1,
.end_line = 5};
cbm_store_upsert_node(store, &n);
cbm_layout_result_t *r = cbm_layout_compute(store, "test", CBM_LAYOUT_OVERVIEW, NULL, 0, 100);
ASSERT_NOT_NULL(r);
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER);
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER);
char *json = cbm_layout_to_json(r);
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"hello\"") != NULL);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >= bytes + strlen(json) + 1);
cbm_layout_free_json(json);
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
cbm_layout_free(r);
cbm_store_close(store);
PASS();
@@ -777,7 +811,7 @@ TEST(layout_dead_code_classification) {
ASSERT_NOT_NULL(json);
ASSERT(strstr(json, "\"status\":\"dead\"") != NULL);
ASSERT(strstr(json, "\"in_calls\":2") != NULL);
free(json);
cbm_layout_free_json(json);
cbm_layout_free(r);
cbm_store_close(store);
@@ -2748,6 +2782,7 @@ SUITE(ui) {
RUN_TEST(layout_honors_budget_above_default);
RUN_TEST(layout_deterministic);
RUN_TEST(layout_to_json);
RUN_TEST(layout_json_memory_core_ownership);
RUN_TEST(layout_null_inputs);
RUN_TEST(layout_dead_code_classification);
RUN_TEST(layout_coincident_nodes_bounded);
+13 -3
View File
@@ -109,6 +109,8 @@ SURFACES=(
"pkg/chocolatey/tools/chocolateyInstall.ps1|release"
"pkg/scoop/codebase-memory-mcp.json|release"
"pkg/homebrew/Formula/codebase-memory-mcp.rb|release"
"plugin/.claude-plugin/plugin.json|release"
"plugin/.mcp.json|release"
"pkg/aur/PKGBUILD|pin:0.8.1"
"pkg/aur/.SRCINFO|pin:0.8.1"
)
@@ -163,6 +165,12 @@ versions_in() {
# cannot see it, because the version keyword is in the function NAME.
grep -oE 'return "[0-9]+\.[0-9]+\.[0-9]+"' "$path" |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+'
# npm package-spec form: the Claude Code plugin launches the server as
# `npx -y codebase-memory-mcp@X.Y.Z`, and that pin is the version every
# plugin user actually runs. No declaration keyword sits next to it, so
# without this form plugin/.mcp.json reads as a surface with no claim.
grep -oE 'codebase-memory-mcp@[0-9]+\.[0-9]+\.[0-9]+' "$path" |
grep -oE '[0-9]+\.[0-9]+\.[0-9]+$'
} | sort -u
}
@@ -280,7 +288,9 @@ fi
# this contract exists to prevent, and every check above would still pass. The
# sweep pattern is deliberately BROADER than the extractors in section 3: if the
# two shared a regex, a claim form the extractors cannot see would be invisible
# to the sweep as well.
# to the sweep as well. The Claude Code plugin tree (plugin/, .claude-plugin/) is
# swept too: a later plugin file that pins `codebase-memory-mcp@X.Y.Z` (a hook
# command, say) must be registered here, or it would go stale unchecked.
while IFS= read -r path; do
[[ -z "$path" ]] && continue
for entry in "${SURFACES[@]}"; do
@@ -293,8 +303,8 @@ while IFS= read -r path; do
"SURFACES so it stays in step with the release, or to EXEMPT with a" \
"note saying what its number actually is."
done < <(
git grep -IlE '(version|pkgver|releases/(download|tag)/v)[^0-9]{0,16}[0-9]+\.[0-9]+\.[0-9]+|return "[0-9]+\.[0-9]+\.[0-9]+"' \
-- server.json pkg tests/test_version_metadata_contract.sh 2>/dev/null || true
git grep -IlE '(version|pkgver|releases/(download|tag)/v)[^0-9]{0,16}[0-9]+\.[0-9]+\.[0-9]+|return "[0-9]+\.[0-9]+\.[0-9]+"|codebase-memory-mcp@[0-9]+\.[0-9]+\.[0-9]+' \
-- server.json pkg plugin .claude-plugin tests/test_version_metadata_contract.sh 2>/dev/null || true
)
if ((failures > 0)); then