Merge origin/main into fix/adr-reindex-race

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-30 22:29:41 +01:00
commit 359bd2f3ff
42 files changed
+1803 -143

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>
+9
View File
@@ -245,6 +245,15 @@ root because they are too broad or too sensitive to index as a unit:
- a credential directory at any depth — `.ssh`, `.aws`, `.gnupg`, `.kube`,
`.docker`, `.netrc`, `.git-credentials`, `.password-store`, macOS `Keychains`.
A single top-level directory can still be indexed as one root on purpose, for
example a container or build machine that keeps its project in `/work`, `/app` or
`/src`, by approving that exact path in a terminal:
`codebase-memory-mcp allow-root --approve-broad /work`. The approval is recorded
like any other allowed root, listed as `(approved broad)` by `allow-root --list`,
and covers no other top-level directory; as with any recorded root, indexing is
then confined to the recorded roots. Filesystem, drive and share roots cannot be
approved.
Two limits are worth stating plainly. This constrains *scope*, not
*sensitivity*: inside a root that is allowed, every file the process can read may
be indexed and later returned. And the credential list is a denylist, so it
+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) {
+57 -16
View File
@@ -1789,9 +1789,25 @@ static uint32_t active_lexical_scope_id(const WalkState *state) {
static uint32_t lexical_ancestor_of_kind(const WalkState *state, uint32_t start_id,
bool want_function, bool want_block);
static bool python_default_value_reference(TSNode node) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
/* One step up: on the occurrence cursor when there is one, else with
* ts_node_parent, which descends from the ROOT, so a climb costs O(depth^2). */
static TSNode usage_parent_step(TSTreeCursor *cursor, TSNode current) {
if (cursor) {
return ts_tree_cursor_goto_parent(cursor) ? ts_tree_cursor_current_node(cursor)
: (TSNode){0};
}
usage_slow_parent_fallback_test_note();
return ts_node_parent(current);
}
/* Asked for every Python usage, and a deep expression puts O(depth) parents
* above each one. A tree without errors climbs them on the walk cursor, like
* python_scope_directive_kind; a tree with errors keeps ts_node_parent. */
static bool python_default_value_reference(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
TSTreeCursor *cursor =
ts_node_has_error(ctx->root) ? NULL : reset_occurrence_cursor(state, node);
for (TSNode parent = usage_parent_step(cursor, node); !ts_node_is_null(parent);
parent = usage_parent_step(cursor, parent)) {
const char *kind = ts_node_type(parent);
/* A nested executable scope inside the default owns its own lookups;
* only a direct default expression is evaluated in the declaring
@@ -1812,10 +1828,10 @@ static bool python_default_value_reference(TSNode node) {
return false;
}
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, const WalkState *state,
TSNode node) {
static uint32_t usage_lexical_scope_id_for_node(CBMExtractCtx *ctx, WalkState *state, TSNode node) {
uint32_t scope_id = active_lexical_scope_id(state);
if (!ctx || ctx->language != CBM_LANG_PYTHON || !python_default_value_reference(node)) {
if (!ctx || ctx->language != CBM_LANG_PYTHON ||
!python_default_value_reference(ctx, state, node)) {
return scope_id;
}
uint32_t function_id = lexical_ancestor_of_kind(state, scope_id, true, false);
@@ -1891,8 +1907,8 @@ static bool ensure_lexical_binding_capacity(WalkState *state) {
}
static bool lexical_ancestor_kind(TSNode node, const char *kind) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
for (TSNode parent = usage_parent_step(NULL, node); !ts_node_is_null(parent);
parent = usage_parent_step(NULL, parent)) {
if (strcmp(ts_node_type(parent), kind) == 0) {
return true;
}
@@ -1900,6 +1916,34 @@ static bool lexical_ancestor_kind(TSNode node, const char *kind) {
return false;
}
/* Is `node` a name listed by a Python global/nonlocal statement? The grammar
* makes those names direct children of the statement, so in a tree without
* errors one parent step on the walk cursor answers it. The full climb above
* steps with ts_node_parent, which descends from the ROOT each time: O(depth^2)
* per identifier, asked for every Python identifier, so one deep arithmetic
* expression went cubic. Error recovery can put a name under an ERROR child of
* the statement, so a tree with errors keeps the full climb. */
static CBMPythonDirectiveKind python_scope_directive_kind(CBMExtractCtx *ctx, WalkState *state,
TSNode node) {
if (ts_node_has_error(ctx->root)) {
if (lexical_ancestor_kind(node, "global_statement")) {
return CBM_PYTHON_DIRECTIVE_GLOBAL;
}
return lexical_ancestor_kind(node, "nonlocal_statement") ? CBM_PYTHON_DIRECTIVE_NONLOCAL
: 0;
}
TSNode parent;
const char *field;
if (!occurrence_parent(reset_occurrence_cursor(state, node), node, &parent, &field)) {
return 0;
}
const char *kind = ts_node_type(parent);
if (strcmp(kind, "global_statement") == 0) {
return CBM_PYTHON_DIRECTIVE_GLOBAL;
}
return strcmp(kind, "nonlocal_statement") == 0 ? CBM_PYTHON_DIRECTIVE_NONLOCAL : 0;
}
static TSNode declared_function_name_owner(TSNode node, const CBMLangSpec *spec) {
for (TSNode parent = ts_node_parent(node); !ts_node_is_null(parent);
parent = ts_node_parent(parent)) {
@@ -2246,13 +2290,11 @@ static void record_lexical_binding(CBMExtractCtx *ctx, WalkState *state, TSNode
TSNode function_declaration = declared_function_name_owner(node, spec);
TSNode class_declaration = declared_class_name_owner(node, spec);
if (ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"))) {
CBMPythonDirectiveKind scope_directive =
ctx->language == CBM_LANG_PYTHON ? python_scope_directive_kind(ctx, state, node) : 0;
if (scope_directive) {
uint32_t function_id = lexical_ancestor_of_kind(state, current_id, true, false);
CBMPythonDirectiveKind directive = lexical_ancestor_kind(node, "global_statement")
? CBM_PYTHON_DIRECTIVE_GLOBAL
: CBM_PYTHON_DIRECTIVE_NONLOCAL;
record_python_directive(state, function_id, name, directive);
record_python_directive(state, function_id, name, scope_directive);
return;
}
@@ -2719,8 +2761,7 @@ void handle_usages(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, Wal
}
bool python_scope_directive =
ctx->language == CBM_LANG_PYTHON && (lexical_ancestor_kind(node, "global_statement") ||
lexical_ancestor_kind(node, "nonlocal_statement"));
ctx->language == CBM_LANG_PYTHON && python_scope_directive_kind(ctx, state, node) != 0;
if (python_scope_directive || is_binding_occurrence(ctx, node, spec, state)) {
char *binding_name = reference_name(ctx, node);
if (binding_name && binding_name[0] && !cbm_is_keyword(binding_name, ctx->language)) {
+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.
+109 -40
View File
@@ -392,8 +392,18 @@ enum { WS_LINE_MAX = 4096 };
/* A line may carry a leading '!' meaning "a person explicitly approved this
* sensitive root". The marker is stored rather than inferred so re-classifying
* later (a new credential name is added to the list, say) cannot silently
* upgrade an ordinary grant into a sensitive one. */
* upgrade an ordinary grant into a sensitive one. A leading '^' is the same
* kind of record for a root refused as too broad (allow-root --approve-broad).
* Neither character can begin a canonical absolute path, so a marked line can
* never be misread as an ordinary grant. */
static const char WS_SENSITIVE_MARK = '!';
static const char WS_BROAD_MARK = '^';
typedef enum {
WS_GRANT_ORDINARY = 0,
WS_GRANT_SENSITIVE, /* explicit approval of a CBM_WS_DENY_SENSITIVE root */
WS_GRANT_BROAD, /* explicit approval of a CBM_WS_DENY_TOO_SHALLOW root */
} ws_grant_kind_t;
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
if (!cache_dir || !cache_dir[0] || !out || out_sz == 0) {
@@ -406,7 +416,8 @@ bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz) {
/* Walk the grant file, invoking visit for each entry. visit returns true to stop.
* Returns the number of entries seen. */
static int ws_grant_walk(const char *cache_dir,
bool (*visit)(const char *root, bool sensitive, void *ctx), void *ctx) {
bool (*visit)(const char *root, ws_grant_kind_t kind, void *ctx),
void *ctx) {
char store[WS_LINE_MAX];
if (!cbm_workspace_grant_path(cache_dir, store, sizeof(store))) {
return 0;
@@ -425,13 +436,18 @@ static int ws_grant_walk(const char *cache_dir,
if (len == 0 || line[0] == '#') {
continue;
}
bool sensitive = line[0] == WS_SENSITIVE_MARK;
const char *root = sensitive ? line + 1 : line;
ws_grant_kind_t kind = WS_GRANT_ORDINARY;
if (line[0] == WS_SENSITIVE_MARK) {
kind = WS_GRANT_SENSITIVE;
} else if (line[0] == WS_BROAD_MARK) {
kind = WS_GRANT_BROAD;
}
const char *root = kind == WS_GRANT_ORDINARY ? line : line + 1;
if (!root[0]) {
continue;
}
seen++;
if (visit && visit(root, sensitive, ctx)) {
if (visit && visit(root, kind, ctx)) {
break;
}
}
@@ -443,14 +459,21 @@ typedef struct {
const char *candidate;
bool contained; /* candidate is at or below some granted root */
bool exact_sensitive; /* candidate is itself a grant marked sensitive */
bool exact_broad; /* candidate is itself a grant marked broad */
} ws_match_t;
static bool ws_match_visit(const char *root, bool sensitive, void *ctx) {
static bool ws_match_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
ws_match_t *m = ctx;
if (cbm_path_within_root(root, m->candidate)) {
m->contained = true;
if (sensitive && ws_paths_equal(root, m->candidate)) {
m->exact_sensitive = true;
/* Exact equality, never a prefix: an approval names one root, and must
* not quietly cover every root below it. */
if (kind != WS_GRANT_ORDINARY && ws_paths_equal(root, m->candidate)) {
if (kind == WS_GRANT_SENSITIVE) {
m->exact_sensitive = true;
} else {
m->exact_broad = true;
}
}
}
return false; /* visit every entry: a later one may be the exact match */
@@ -462,10 +485,15 @@ typedef struct {
size_t used;
} ws_list_t;
static bool ws_list_visit(const char *root, bool sensitive, void *ctx) {
static bool ws_list_visit(const char *root, ws_grant_kind_t kind, void *ctx) {
ws_list_t *l = ctx;
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n",
sensitive ? "(approved) " : "", root);
const char *label = "";
if (kind == WS_GRANT_SENSITIVE) {
label = "(approved) ";
} else if (kind == WS_GRANT_BROAD) {
label = "(approved broad) ";
}
int n = snprintf(l->out + l->used, l->out_sz - l->used, "%s%s\n", label, root);
if (n > 0 && (size_t)n < l->out_sz - l->used) {
l->used += (size_t)n;
}
@@ -481,9 +509,49 @@ bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz) {
return ws_grant_walk(cache_dir, ws_list_visit, &l) > 0;
}
/* The allow-root flag that records an explicit approval for a refused verdict,
* or NULL when nothing can lift it (absolute denials). */
static const char *ws_verdict_approval_flag(cbm_ws_verdict_t verdict) {
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
return "--approve-broad";
}
if (cbm_workspace_verdict_is_overridable(verdict)) {
return "--approve-sensitive";
}
return NULL;
}
/* True when a refused verdict carries the explicit approval that lifts it. Each
* flag lifts only its own verdict: --approve-sensitive never widens breadth and
* --approve-broad never widens sensitivity; absolute denials lift for neither.
* Refuses by default and names the flag, rather than asking a question whose
* answer we cannot authenticate. */
static bool ws_grant_verdict_approved(cbm_ws_verdict_t verdict, bool approve_sensitive,
bool approve_broad, char *err, size_t err_sz) {
bool approved = false;
if (verdict == CBM_WS_DENY_TOO_SHALLOW) {
approved = approve_broad;
} else if (cbm_workspace_verdict_is_overridable(verdict)) {
approved = approve_sensitive;
}
if (approved) {
return true;
}
if (err) {
const char *flag = ws_verdict_approval_flag(verdict);
if (flag) {
snprintf(err, err_sz, "%s; re-run with %s if that is intended",
cbm_workspace_verdict_reason(verdict), flag);
} else {
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
}
}
return false;
}
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
const char *canonical_path, bool approve_sensitive, char *err,
size_t err_sz) {
const char *canonical_path, bool approve_sensitive, bool approve_broad,
char *err, size_t err_sz) {
if (err && err_sz) {
err[0] = '\0';
}
@@ -495,33 +563,22 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
}
cbm_ws_verdict_t verdict = cbm_workspace_classify_root(canonical_path, home_dir, cache_dir);
if (verdict != CBM_WS_ALLOW) {
if (!cbm_workspace_verdict_is_overridable(verdict)) {
if (err) {
snprintf(err, err_sz, "%s", cbm_workspace_verdict_reason(verdict));
}
return false;
}
if (!approve_sensitive) {
/* Refuse by default and name the flag, rather than asking a question
* whose answer we cannot authenticate. */
if (err) {
snprintf(err, err_sz, "%s; re-run with --approve-sensitive if that is intended",
cbm_workspace_verdict_reason(verdict));
}
return false;
}
if (verdict != CBM_WS_ALLOW &&
!ws_grant_verdict_approved(verdict, approve_sensitive, approve_broad, err, err_sz)) {
return false;
}
bool sensitive = verdict == CBM_WS_DENY_SENSITIVE;
bool broad = verdict == CBM_WS_DENY_TOO_SHALLOW;
/* Already present is success, not a duplicate error. A sensitive approval
/* Already present is success, not a duplicate error. An explicit approval
* is narrower: an old ordinary grant (or an ordinary ancestor) establishes
* containment, but does not record the exact human-approved exception that
* cbm_workspace_root_allowed requires. Append that exact marked entry once. */
ws_match_t existing = {canonical_path, false, false};
ws_match_t existing = {canonical_path, false, false, false};
(void)ws_grant_walk(cache_dir, ws_match_visit, &existing);
if (existing.contained && (!sensitive || existing.exact_sensitive)) {
if (existing.contained && (!sensitive || existing.exact_sensitive) &&
(!broad || existing.exact_broad)) {
return true;
}
@@ -539,7 +596,13 @@ bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
}
return false;
}
(void)fprintf(f, "%s%s\n", sensitive ? "!" : "", canonical_path);
char mark[2] = {'\0', '\0'};
if (sensitive) {
mark[0] = WS_SENSITIVE_MARK;
} else if (broad) {
mark[0] = WS_BROAD_MARK;
}
(void)fprintf(f, "%s%s\n", mark, canonical_path);
bool ok = fclose(f) == 0;
if (!ok && err) {
snprintf(err, err_sz, "cannot write %s", store);
@@ -560,7 +623,7 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
return false;
}
ws_match_t match = {canonical_path, false, false};
ws_match_t match = {canonical_path, false, false, false};
int grants = ws_grant_walk(cache_dir, ws_match_visit, &match);
/* A configured root behaves as an additional grant so existing
@@ -605,17 +668,23 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir
return true;
}
/* An explicit human approval recorded for exactly this path is the only thing
* that lifts a sensitive refusal. Absolute and shallow refusals cannot be
* lifted at all. */
* that lifts a sensitive or a too-broad refusal, each by its own marker: a
* broad approval says "index this whole tree on purpose", which is not a
* statement about credentials, and the reverse. Exact match, never a prefix,
* so one broad approval cannot cover every root below it — the "/Users"-style
* breadth the depth rule exists to catch. Absolute refusals cannot be lifted. */
if (verdict == CBM_WS_DENY_SENSITIVE && match.exact_sensitive) {
return true;
}
if (verdict == CBM_WS_DENY_TOO_SHALLOW && match.exact_broad) {
return true;
}
if (err) {
if (cbm_workspace_verdict_is_overridable(verdict)) {
const char *flag = ws_verdict_approval_flag(verdict);
if (flag) {
snprintf(err, err_sz,
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root "
"--approve-sensitive %s",
canonical_path, cbm_workspace_verdict_reason(verdict), canonical_path);
"%s: %s. To index it anyway, run: codebase-memory-mcp allow-root %s %s",
canonical_path, cbm_workspace_verdict_reason(verdict), flag, canonical_path);
} else {
snprintf(err, err_sz, "%s: %s", canonical_path, cbm_workspace_verdict_reason(verdict));
}
+13 -7
View File
@@ -25,7 +25,9 @@
typedef enum {
CBM_WS_ALLOW = 0,
/* Fewer path components than the platform minimum — e.g. "/etc", "/home". */
/* Fewer path components than the platform minimum — e.g. "/etc", "/home".
* Lifted only for one exact path by a recorded broad approval
* (allow-root --approve-broad), never by a manifest or a tool call. */
CBM_WS_DENY_TOO_SHALLOW,
/* A filesystem root, a drive root, a UNC share root, or a path that is or
* contains the cbm cache directory. Never overridable. */
@@ -42,8 +44,10 @@ cbm_ws_verdict_t cbm_workspace_classify_root(const char *canonical_path, const c
/* Stable, human-facing reason for a verdict. Never NULL. */
const char *cbm_workspace_verdict_reason(cbm_ws_verdict_t verdict);
/* True when an explicit human action may override this verdict. Absolute denials
* and shallow paths are never overridable; sensitive ones are. */
/* True when a sensitive approval (allow-root --approve-sensitive) may override
* this verdict. Absolute denials never are. A shallow path is not overridable
* in this sense either: only an exact broad approval lifts it, see
* cbm_workspace_grant_add. */
bool cbm_workspace_verdict_is_overridable(cbm_ws_verdict_t verdict);
/* Number of path components below the volume root, ignoring repeated and
@@ -81,11 +85,13 @@ bool cbm_path_within_root(const char *root_path, const char *abs_path);
bool cbm_workspace_grant_path(const char *cache_dir, char *out, size_t out_sz);
/* Record canonical_path as an allowed root. approve_sensitive records an explicit
* human override for a CBM_WS_DENY_SENSITIVE path; absolute denials and shallow
* paths are never grantable. Returns false and fills err on refusal. */
* human override for a CBM_WS_DENY_SENSITIVE path; approve_broad records one for a
* CBM_WS_DENY_TOO_SHALLOW path. Each flag lifts only its own verdict, and only
* for exactly this path. Absolute denials are never grantable. Returns false and
* fills err on refusal. */
bool cbm_workspace_grant_add(const char *cache_dir, const char *home_dir,
const char *canonical_path, bool approve_sensitive, char *err,
size_t err_sz);
const char *canonical_path, bool approve_sensitive, bool approve_broad,
char *err, size_t err_sz);
/* Newline-separated granted roots into out. True when at least one exists. */
bool cbm_workspace_grant_list(const char *cache_dir, char *out, size_t out_sz);
+11 -4
View File
@@ -1179,21 +1179,27 @@ static void print_help(void) {
/* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help).
* Returns -1 if no subcommand matched, otherwise the exit code. */
/* `allow-root [--approve-sensitive] <path>` — record an indexing root.
/* `allow-root [--approve-sensitive | --approve-broad] <path>` — record an
* indexing root.
*
* Enrollment lives here, in a command a person types, and deliberately nowhere
* else: the whole point of the grant store is that neither an indexed repository
* nor a tool caller can widen its own boundary. A confirmation delivered through
* the MCP surface would be answered by the same agent that may have been
* influenced, so it would not be a human decision at all. */
* influenced, so it would not be a human decision at all. --approve-broad is the
* same kind of record for one exact root refused as too broad ("/work", "/app"),
* so it is listed, revocable and seen identically by stdio and daemon sessions. */
static int main_run_allow_root(int argc, char **argv) {
const char *path = NULL;
bool approve_sensitive = false;
bool approve_broad = false;
bool list_only = false;
bool approve_manifest = false;
for (int i = 0; i < argc; i++) {
if (strcmp(argv[i], "--approve-sensitive") == 0) {
approve_sensitive = true;
} else if (strcmp(argv[i], "--approve-broad") == 0) {
approve_broad = true;
} else if (strcmp(argv[i], "--list") == 0) {
list_only = true;
} else if (strcmp(argv[i], "--approve-manifest") == 0) {
@@ -1225,7 +1231,8 @@ static int main_run_allow_root(int argc, char **argv) {
}
if (!path && !list_only) {
(void)fprintf(stderr,
"usage: codebase-memory-mcp allow-root [--approve-sensitive] <path>\n"
"usage: codebase-memory-mcp allow-root [--approve-sensitive | "
"--approve-broad] <path>\n"
" codebase-memory-mcp allow-root --approve-manifest <project>\n"
" codebase-memory-mcp allow-root --list\n");
return EXIT_FAILURE;
@@ -1268,7 +1275,7 @@ static int main_run_allow_root(int argc, char **argv) {
char err[CBM_SZ_1K];
if (!cbm_workspace_grant_add(cache_dir, cbm_workspace_home_dir(), canonical, approve_sensitive,
err, sizeof(err))) {
approve_broad, err, sizeof(err))) {
(void)fprintf(stderr, "refused: %s\n", err[0] ? err : "not an allowable root");
return EXIT_FAILURE;
}
+2 -4
View File
@@ -8301,12 +8301,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
@@ -4186,10 +4186,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) {
@@ -4199,9 +4207,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);
@@ -4209,8 +4231,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;
}
@@ -4220,6 +4241,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};
@@ -10668,6 +10694,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);
@@ -1317,7 +1321,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);
@@ -1325,6 +1329,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
@@ -963,6 +963,59 @@ TEST(repro_python_global_assignment_is_module_binding) {
PASS();
}
/* A global/nonlocal name is a directive, not a read (each name of `global a,
* b`), and the rebinding lands where it says: the module from that point on,
* or the enclosing function. Source 1 is an error tree, whose directive and
* default-value checks take the ts_node_parent fallback. */
TEST(repro_python_scope_directives_bind_the_named_scope) {
static const char *const sources[] = {
"def handler():\n pass\n"
"def outer():\n handler = None\n"
" def inner():\n nonlocal handler\n handler = outer\n"
"def early():\n accept_early(handler)\n"
"def caller():\n def nested():\n global handler, other\n"
" handler = outer\n accept_outer(handler)\n"
"def sibling():\n accept_sibling(handler)\n",
"def handler():\n pass\n"
"def make(handler=handler):\n return handler\n"
"def caller():\n global handler other\n handler = make\n make(handler)\n",
};
static const struct {
int source;
const char *marker, *name;
int count, blocked, shadow;
} sites[] = {
{0, "global handler", "handler", 0, 0, 0},
{0, ", other", "other", 0, 0, 0},
{0, "nonlocal handler", "handler", 0, 0, 0},
{0, "accept_early(handler)", "handler", 1, 0, 0},
{0, "accept_outer(handler)", "handler", 1, 1, 0},
{0, "accept_sibling(handler)", "handler", 1, 1, 0},
{1, "global handler", "handler", 0, 0, 0},
{1, "=handler", "handler", 1, 0, 0},
{1, "make(handler)", "handler", 1, 1, 0},
};
int failures = 0;
for (int i = 0; i < (int)(sizeof(sites) / sizeof(sites[0])); i++) {
const char *source = sources[sites[i].source];
uint32_t start = lb_identifier_offset(source, sites[i].marker, sites[i].name);
ASSERT_NEQ(start, UINT32_MAX);
CBMFileResult *result = lb_extract(source, CBM_LANG_PYTHON, "directives.py");
ASSERT_NOT_NULL(result);
ASSERT_EQ(result->parse_incomplete, sites[i].source);
LBBindingSite got = lb_binding_site(result, NULL, sites[i].name, start);
cbm_free_result(result);
if (got.count != sites[i].count || got.blocked_count != sites[i].blocked ||
got.local_shadow_count != sites[i].shadow) {
printf(" %s: count %d blocked %d shadow %d\n", sites[i].marker, got.count,
got.blocked_count, got.local_shadow_count);
failures++;
}
}
ASSERT_EQ(failures, 0);
PASS();
}
/* Module assignments participate in name lookup too. A later function must
* not raw-name-fallback through a module variable to an older same-named
* callable declaration. */
@@ -1055,6 +1108,7 @@ SUITE(repro_lexical_binding_precision) {
RUN_TEST(repro_objectscript_udl_set_target_binds_method_local);
RUN_TEST(repro_python_default_expression_uses_enclosing_namespace);
RUN_TEST(repro_python_global_assignment_is_module_binding);
RUN_TEST(repro_python_scope_directives_bind_the_named_scope);
RUN_TEST(repro_python_module_assignment_blocks_callable_fallback);
RUN_TEST(repro_go_function_literal_parameter_does_not_leak);
}
+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);
+6 -4
View File
@@ -2643,8 +2643,9 @@ TEST(daemon_application_sensitive_root_blocks_auto_index_but_preserves_controls)
app_wait_for_active_jobs(application, 0);
char grant_error[APP_TEST_PATH_CAP];
bool approved = ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
grant_error, sizeof(grant_error));
bool approved =
ordinary_reaped && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
grant_error, sizeof(grant_error));
bool approved_initialized =
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
@@ -2769,8 +2770,9 @@ TEST(daemon_application_sensitive_root_blocks_watch_but_preserves_controls) {
bool ordinary_released = ordinary_watched && cbm_watcher_watch_count(watcher) == 0;
char grant_error[APP_TEST_PATH_CAP];
bool approved = ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
grant_error, sizeof(grant_error));
bool approved =
ordinary_released && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
grant_error, sizeof(grant_error));
bool approved_initialized =
approved && app_test_initialize_profile(&callbacks, approved_session, sensitive,
CBM_MCP_TOOL_PROFILE_ALL, NULL, NULL);
+44
View File
@@ -10933,6 +10933,49 @@ TEST(extract_rescript_let_bindings_use_the_walk_cursor) {
ASSERT_EQ(big_fallbacks, 0);
PASS();
}
/* Every Python usage asks whether it is a global/nonlocal name and whether it
* sits in a parameter default. Both climbed with ts_node_parent, which
* descends from the ROOT: O(d^2) for a usage d deep, so one deep expression
* went cubic (2.2 s / 17 s / >90 s at 500/1000/2000 terms). Count every such
* root-descending step at N and 2N terms. */
TEST(extract_python_deep_default_parent_climbs_are_linear) {
enum { SMALL = 200 };
uint64_t slow_parents[2];
for (int round = 0; round < 2; round++) {
/* def f(x=x + x + ... + x): return x -- a binary_operator chain. */
int terms = SMALL << round;
size_t capacity = 32U + (size_t)terms * 4U;
char *source = malloc(capacity);
ASSERT_NOT_NULL(source);
int length = snprintf(source, capacity, "def f(x=x");
for (int i = 1; i < terms; i++) {
length += snprintf(source + length, capacity - (size_t)length, " + x");
}
length += snprintf(source + length, capacity - (size_t)length, "):\n return x\n");
cbm_usage_field_lookup_test_reset();
CBMFileResult *result =
cbm_extract_file(source, length, CBM_LANG_PYTHON, "proj", "deep.py", 0, NULL, NULL);
free(source);
ASSERT_NOT_NULL(result);
slow_parents[round] = cbm_usage_slow_parent_fallback_test_count();
/* Anti-vacuous: every default x climbed its chain to the parameter and
* reads the enclosing namespace; only `return x` is shadowed by it. */
int defaults = 0;
for (int i = 0; i < result->usages.count; i++) {
const CBMUsage *usage = &result->usages.items[i];
defaults += usage->ref_name && strcmp(usage->ref_name, "x") == 0 &&
!usage->semantic_reference_local_shadow;
}
cbm_free_result(result);
ASSERT_EQ(defaults, terms);
}
fprintf(stderr, " [python-deep-default] slow_parents(%d)=%llu slow_parents(%d)=%llu\n", SMALL,
(unsigned long long)slow_parents[0], 2 * SMALL, (unsigned long long)slow_parents[1]);
/* Twice the terms may cost about twice the steps; the cubic climb took 4x. */
ASSERT_LTE(slow_parents[1], slow_parents[0] * 3U + 64U);
PASS();
}
#endif
/* ===================================================================
@@ -13455,6 +13498,7 @@ SUITE(extraction) {
RUN_TEST(extract_wide_flat_reference_fields_are_linear);
RUN_TEST(extract_csharp_argument_values_use_the_walk_cursor);
RUN_TEST(extract_rescript_let_bindings_use_the_walk_cursor);
RUN_TEST(extract_python_deep_default_parent_climbs_are_linear);
#endif
/* Perl call-graph noise (#459 follow-up) */
+1 -1
View File
@@ -2046,7 +2046,7 @@ static int issue403_initialize_count_calls(const char *session_root, bool approv
char err[1024];
bool approved =
!approve_sensitive || cbm_workspace_grant_add(cache, cbm_workspace_home_dir(), session_root,
true, err, sizeof(err));
true, false, err, sizeof(err));
cbm_config_t *cfg = approved ? cbm_config_open(cache) : NULL;
cbm_mcp_server_t *srv = cfg ? cbm_mcp_server_new(NULL) : NULL;
int calls = -2;
+282
View File
@@ -19088,6 +19088,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) {
@@ -29520,6 +29799,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
@@ -1385,6 +1385,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) {
@@ -1410,7 +1530,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();
}
@@ -1419,7 +1539,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();
}
@@ -1430,7 +1550,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();
}
@@ -1493,7 +1613,7 @@ TEST(louvain_converges) {
}
ASSERT_TRUE(same_count >= 8);
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1518,7 +1638,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);
}
@@ -1665,7 +1786,7 @@ TEST(leiden_multilevel_collapses_noise) {
}
ASSERT_TRUE(same >= SZ - 1);
}
cbm_free(CBM_MEM_CLASS_OTHER, result);
cbm_leiden_free(result);
PASS();
}
@@ -1697,8 +1818,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();
}
@@ -1965,6 +2086,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
+79 -8
View File
@@ -75,6 +75,74 @@ TEST(ws_posix_top_level_trees_are_too_shallow) {
PASS();
}
/* A recorded broad approval (allow-root --approve-broad) is the only thing that
* lifts CBM_WS_DENY_TOO_SHALLOW, and only for that exact path. Classification
* and cbm_workspace_verdict_is_overridable stay exactly as strict as the test
* above checks; the approval is a grant, so it is listed like any other human
* decision, and it lifts no other verdict. */
TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only) {
char cache[256];
char *created = th_mktempdir("cbm_ws_allow_broad");
ASSERT_NOT_NULL(created);
snprintf(cache, sizeof(cache), "%s", created);
char err[1024];
char listed[4096];
/* Refused out of the box, and the refusal names the command a person runs. */
ASSERT_FALSE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /usr"));
/* Neither an ordinary grant nor a sensitive approval lifts breadth; a broad
* approval lifts neither an absolute nor a sensitive verdict. A refused grant
* records nothing. */
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", false, false, err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "--approve-broad"));
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/usr", true, false, err, sizeof(err)));
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, "/", false, true, err, sizeof(err)));
ASSERT_FALSE(cbm_workspace_grant_add(cache, HOME, HOME, false, true, err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
ASSERT_FALSE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
/* The explicit approval is recorded and marked. */
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
#ifndef _WIN32
/* Containment resolves real paths, so the rest needs a top-level directory
* that exists on every POSIX host: "/usr". A repeated approval is recognized
* rather than appended again. */
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, "/usr", false, true, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_list(cache, listed, sizeof(listed)));
ASSERT_STR_EQ(listed, "(approved broad) /usr\n");
/* It lifts exactly that path. A configured root of "/" puts "/dev" inside the
* declared boundary too, so the only thing still refusing it is that the
* approval names another path. */
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_root_allowed("/usr", HOME, cache, "/", err, sizeof(err)));
ASSERT_FALSE(cbm_workspace_root_allowed("/dev", HOME, cache, "/", err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "allow-root --approve-broad /dev"));
#endif
/* Each marker lifts only its own verdict: a broad mark on a credential
* directory, which grant_add would never write, still does not lift it. */
char secret[1024];
snprintf(secret, sizeof(secret), "%s/.ssh", cache);
ASSERT_EQ(cbm_mkdir(secret), 0);
char store[1024];
ASSERT_TRUE(cbm_workspace_grant_path(cache, store, sizeof(store)));
FILE *f = cbm_fopen(store, "a");
ASSERT_NOT_NULL(f);
(void)fprintf(f, "^%s\n", secret);
ASSERT_EQ(fclose(f), 0);
ASSERT_FALSE(cbm_workspace_root_allowed(secret, HOME, cache, NULL, err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
th_cleanup(cache);
PASS();
}
/* Legitimately shallow project roots must survive: these are the false positives
* a blanket depth rule would cause, which is why Windows counts drive-relative. */
TEST(ws_legitimate_shallow_roots_are_allowed) {
@@ -177,7 +245,7 @@ TEST(ws_sensitive_root_explicit_approval_is_preserved) {
char err[1024];
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
ASSERT_NOT_NULL(strstr(err, "--approve-sensitive"));
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
th_cleanup(root);
@@ -197,14 +265,14 @@ TEST(ws_sensitive_approval_upgrades_existing_ordinary_exact_grant) {
snprintf(cache, sizeof(cache), "%s", created);
char err[1024];
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, root, false, false, err, sizeof(err)));
ASSERT_FALSE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_root_allowed(root, root, cache, NULL, err, sizeof(err)));
/* A repeated explicit approval must recognize the marked exact grant rather
* than append another exception. */
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, root, root, true, false, err, sizeof(err)));
char listed[4096];
char expected[4096];
@@ -232,13 +300,15 @@ TEST(ws_sensitive_approval_adds_exact_exception_under_ordinary_ancestor) {
snprintf(sensitive, sizeof(sensitive), "%s/private-project", ancestor);
ASSERT_EQ(cbm_mkdir(sensitive), 0);
char err[1024];
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, NULL, ancestor, false, false, err, sizeof(err)));
ASSERT_FALSE(cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
ASSERT_TRUE(
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
ASSERT_TRUE(
cbm_workspace_root_allowed(sensitive, sensitive, cache, NULL, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, sensitive, sensitive, true, err, sizeof(err)));
ASSERT_TRUE(
cbm_workspace_grant_add(cache, sensitive, sensitive, true, false, err, sizeof(err)));
char listed[4096];
char expected[4096];
@@ -774,7 +844,7 @@ TEST(ws_manifest_ordinary_entries_unchanged) {
char err[1024];
/* The grant store names a root the sdk is not under. */
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, err, sizeof(err)));
ASSERT_TRUE(cbm_workspace_grant_add(cache, HOME, granted, false, false, err, sizeof(err)));
ws_manifest_write_line(base, sdk);
ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err)));
@@ -918,6 +988,7 @@ SUITE(workspace) {
RUN_TEST(ws_volume_roots_are_absolutely_denied);
RUN_TEST(ws_non_absolute_paths_are_denied);
RUN_TEST(ws_posix_top_level_trees_are_too_shallow);
RUN_TEST(ws_allow_broad_root_lifts_too_shallow_for_an_exact_match_only);
RUN_TEST(ws_legitimate_shallow_roots_are_allowed);
RUN_TEST(ws_home_itself_is_sensitive_but_subdirs_are_fine);
RUN_TEST(ws_credential_directories_are_sensitive_at_any_depth);