mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge origin/main into fix/permanent-daemon-watch
test_daemon_application.c: the new auto-watch test passes false for the approve_broad parameter that #2017 added to cbm_workspace_grant_add, the same value the existing callers use; the merged tree did not compile without it. Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
b33a645e47
33 files changed
+1421
-64
No files matched your search
@@ -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."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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_*
|
||||
|
||||
@@ -578,6 +578,30 @@ Working in a clone? Use `.` in place of the flake URL, e.g. `nix run .#codebase-
|
||||
You: "Install this MCP server: https://github.com/DeusData/codebase-memory-mcp"
|
||||
```
|
||||
|
||||
### Install via Claude Code plugin
|
||||
|
||||
This repository is also a Claude Code plugin marketplace, so you can find and
|
||||
add the server from inside Claude Code:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add DeusData/codebase-memory-mcp
|
||||
claude plugin install codebase-memory@codebase-memory
|
||||
```
|
||||
|
||||
The plugin registers the MCP server only. It starts the server with
|
||||
`npx -y codebase-memory-mcp@<version>`, pinned to the current release; the first
|
||||
start downloads that release's binary through the npm package, which verifies
|
||||
it against the release checksums. The skill, the three graph agents and the
|
||||
context hooks are not part of the plugin: they come from
|
||||
`codebase-memory-mcp install`.
|
||||
|
||||
Use one or the other. `install` registers the same server for Claude Code, so
|
||||
with both you run it twice, under two tool namespaces
|
||||
(`mcp__codebase-memory-mcp__*` and
|
||||
`mcp__plugin_codebase-memory_codebase-memory-mcp__*`). Choose `install` for the
|
||||
full Claude Code setup; choose the plugin to add just the server without
|
||||
running an installer.
|
||||
|
||||
### Build from Source
|
||||
|
||||
<details>
|
||||
|
||||
+11
-3
@@ -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'")
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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, ®, module_qn, out, "cross");
|
||||
if (ctx.eval_failure)
|
||||
goto cross_cleanup;
|
||||
if (!py_register_lsp_defs(arena, &idx_arena, ®, defs, def_count)) {
|
||||
if (!py_register_lsp_defs(arena, &idx_arena, ®, 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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"codebase-memory-mcp": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "codebase-memory-mcp@0.12.1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-6
@@ -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.
|
||||
|
||||
+2
-4
@@ -8280,12 +8280,10 @@ static int arch_compute_cycles(cbm_store_t *store, const char *project, int64_t
|
||||
*scanned_edges = ecount;
|
||||
scc_graph_t g;
|
||||
if (!scc_build(src, tgt, ecount, &g)) {
|
||||
free(src);
|
||||
free(tgt);
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
return CBM_STORE_OK; /* no edges = no cycles, not an error */
|
||||
}
|
||||
free(src);
|
||||
free(tgt);
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
|
||||
int *comp = malloc((size_t)g.nverts * sizeof(int));
|
||||
if (!comp) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -4165,10 +4165,18 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
|
||||
bind_text(stmt, SKIP_ONE, project);
|
||||
sqlite3_bind_int(stmt, 2, max_edges > 0 ? max_edges + SKIP_ONE : CBM_NOT_FOUND);
|
||||
|
||||
/* Both arrays are STORE-class memory-core blocks: the caller releases them
|
||||
* with cbm_store_free_call_edges, the only free that matches. */
|
||||
int cap = ST_INIT_CAP_16;
|
||||
int n = 0;
|
||||
int64_t *src = malloc((size_t)cap * sizeof(int64_t));
|
||||
int64_t *tgt = malloc((size_t)cap * sizeof(int64_t));
|
||||
int64_t *src = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
|
||||
int64_t *tgt = cbm_alloc(CBM_MEM_CLASS_STORE, (size_t)cap * sizeof(int64_t));
|
||||
if (!src || !tgt) {
|
||||
sqlite3_finalize(stmt);
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
store_set_error(s, "fetch_call_edges: out of memory");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
int scan_rc;
|
||||
while ((scan_rc = sqlite3_step(stmt)) == SQLITE_ROW) {
|
||||
if (max_edges > 0 && n >= max_edges) {
|
||||
@@ -4178,9 +4186,23 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
|
||||
break;
|
||||
}
|
||||
if (n >= cap) {
|
||||
size_t grown_bytes = (size_t)cap * ST_GROWTH * sizeof(int64_t);
|
||||
int64_t *grown_src = cbm_realloc(CBM_MEM_CLASS_STORE, src, grown_bytes);
|
||||
if (grown_src) {
|
||||
src = grown_src;
|
||||
}
|
||||
int64_t *grown_tgt =
|
||||
grown_src ? cbm_realloc(CBM_MEM_CLASS_STORE, tgt, grown_bytes) : NULL;
|
||||
if (grown_tgt) {
|
||||
tgt = grown_tgt;
|
||||
}
|
||||
if (!grown_src || !grown_tgt) {
|
||||
sqlite3_finalize(stmt);
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
store_set_error(s, "fetch_call_edges: out of memory");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
cap *= ST_GROWTH;
|
||||
src = safe_realloc(src, (size_t)cap * sizeof(int64_t));
|
||||
tgt = safe_realloc(tgt, (size_t)cap * sizeof(int64_t));
|
||||
}
|
||||
src[n] = sqlite3_column_int64(stmt, 0);
|
||||
tgt[n] = sqlite3_column_int64(stmt, 1);
|
||||
@@ -4188,8 +4210,7 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
if (scan_rc != SQLITE_DONE && scan_rc != SQLITE_ROW) {
|
||||
free(src);
|
||||
free(tgt);
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
store_set_error_sqlite(s, "fetch_call_edges scan");
|
||||
return CBM_STORE_ERR;
|
||||
}
|
||||
@@ -4199,6 +4220,11 @@ int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edge
|
||||
return CBM_STORE_OK;
|
||||
}
|
||||
|
||||
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt) {
|
||||
cbm_free(CBM_MEM_CLASS_STORE, src);
|
||||
cbm_free(CBM_MEM_CLASS_STORE, tgt);
|
||||
}
|
||||
|
||||
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out,
|
||||
int *count) {
|
||||
bind_id_t b = {source_id};
|
||||
@@ -10647,6 +10673,10 @@ int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *
|
||||
return cbm_leiden(nodes, node_count, edges, edge_count, 1.0, out, out_count);
|
||||
}
|
||||
|
||||
void cbm_leiden_free(cbm_louvain_result_t *result) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result); /* cbm_leiden allocates OTHER */
|
||||
}
|
||||
|
||||
/* ── Architecture: community clusters via Leiden ───────────────── */
|
||||
|
||||
enum {
|
||||
|
||||
+9
-2
@@ -708,12 +708,16 @@ int64_t cbm_store_insert_edge(cbm_store_t *s, const cbm_edge_t *e);
|
||||
int cbm_store_insert_edge_batch(cbm_store_t *s, const cbm_edge_t *edges, int count);
|
||||
|
||||
/* Fetch all CALLS edges among Function/Method nodes for a project as parallel
|
||||
* (source_id, target_id) arrays (caller frees both). For SCC / cycle analysis.
|
||||
* (source_id, target_id) arrays, released together with
|
||||
* cbm_store_free_call_edges. For SCC / cycle analysis.
|
||||
* Stops at max_edges and sets *truncated — never a silent cap. Returns
|
||||
* CBM_STORE_OK (or _ERR); *count is the number returned. */
|
||||
int cbm_store_fetch_call_edges(cbm_store_t *s, const char *project, int max_edges,
|
||||
int64_t **out_src, int64_t **out_tgt, int *count, bool *truncated);
|
||||
|
||||
/* Free the (source_id, target_id) arrays returned by cbm_store_fetch_call_edges. */
|
||||
void cbm_store_free_call_edges(int64_t *src, int64_t *tgt);
|
||||
|
||||
/* Find edges by source node. */
|
||||
int cbm_store_find_edges_by_source(cbm_store_t *s, int64_t source_id, cbm_edge_t **out, int *count);
|
||||
|
||||
@@ -1306,7 +1310,7 @@ typedef struct {
|
||||
* reported community is internally connected. The resolution parameter
|
||||
* controls granularity (higher -> more, smaller communities); 1.0 is standard.
|
||||
* Allocates *out (length *out_count == node_count) in CBM_MEM_CLASS_OTHER;
|
||||
* the caller releases it with cbm_free(CBM_MEM_CLASS_OTHER, *out). */
|
||||
* the caller releases it with cbm_leiden_free(*out). */
|
||||
int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
|
||||
int edge_count, double resolution, cbm_louvain_result_t **out, int *out_count);
|
||||
|
||||
@@ -1314,6 +1318,9 @@ int cbm_leiden(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *e
|
||||
int cbm_louvain(const int64_t *nodes, int node_count, const cbm_louvain_edge_t *edges,
|
||||
int edge_count, cbm_louvain_result_t **out, int *out_count);
|
||||
|
||||
/* Free the result array returned by cbm_leiden / cbm_louvain. */
|
||||
void cbm_leiden_free(cbm_louvain_result_t *result);
|
||||
|
||||
/* ── Memory management helpers ──────────────────────────────────── */
|
||||
|
||||
/* Free heap-allocated strings in a stack-allocated node (does NOT free the node itself). */
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -2857,7 +2857,7 @@ TEST(daemon_application_permanent_workspace_release_still_ends_watch) {
|
||||
bool db_ready = config_ready && project && app_test_create_empty_file(db_path);
|
||||
/* The root is $HOME, so only an explicit sensitive approval allows it. */
|
||||
char grant_error[APP_TEST_PATH_CAP];
|
||||
bool approved = db_ready && cbm_workspace_grant_add(cache, sensitive, sensitive, true,
|
||||
bool approved = db_ready && cbm_workspace_grant_add(cache, sensitive, sensitive, true, false,
|
||||
grant_error, sizeof(grant_error));
|
||||
char grant_path[APP_TEST_PATH_CAP] = {0};
|
||||
bool grant_path_ok =
|
||||
|
||||
@@ -18688,6 +18688,285 @@ TEST(pipeline_python_nested_def_local_after_incremental_issue1548) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A Java record's implicit accessors are GENERATED members (#2295): Method
|
||||
* nodes marked `generated: record`, registered by qualified name only, as a
|
||||
* Python function's locals are. The type-aware resolver binds `p.y()` on a
|
||||
* Point to Point.y; no short-name guess may land on a member the source never
|
||||
* spells. `far` calls y() on a type outside the project, so the project-wide
|
||||
* name registry is its only route to Point.y. `data` calls x(), whose other
|
||||
* spelling is Spring's private field: neither the accessor (a name guess) nor
|
||||
* the field (a Java call never names data) is its target. The record's
|
||||
* hand-written z() and norm() are source members: no `generated` mark.
|
||||
* `org.external.y.Vector` is not in the project, and its package segment `y`
|
||||
* spells the accessor: the import's symbol-name fallback must not bind it
|
||||
* there (no IMPORTS edge), or `new Vector()` in `make` would follow the import
|
||||
* onto the accessor (elasticsearch: `import javax.crypto.spec.PBEKeySpec`
|
||||
* bound a record's `spec()`). `stat` calls, and `refs` passes, the JDK's
|
||||
* Objects.nonNull: no project callable is its target, and Seen's accessor --
|
||||
* the only project callable called nonNull -- must not be taken for it on its
|
||||
* leaf name alone (elasticsearch: `Objects.nonNull(x)` and
|
||||
* `filter(Objects::nonNull)` bound a test record's nonNull()). */
|
||||
static const char jgen_point_java[] =
|
||||
"package com.example;\n"
|
||||
"public record Point(double x, double y, double z) {\n"
|
||||
" public double z() { return z; }\n"
|
||||
" public double norm() { return Math.sqrt(x * x + y * y + z * z); }\n"
|
||||
"}\n";
|
||||
static const char jgen_seen_java[] = "package com.example;\n"
|
||||
"record Seen(boolean nonNull) {}\n";
|
||||
static const char jgen_spring_java[] = "package com.example;\n"
|
||||
"public final class Spring {\n"
|
||||
" private double x;\n"
|
||||
" public void put(double v) { this.x = v; }\n"
|
||||
"}\n";
|
||||
static const char jgen_caller_java[] =
|
||||
"package com.example;\n"
|
||||
"import java.util.List;\n"
|
||||
"import java.util.Objects;\n"
|
||||
"import org.external.y.Vector;\n"
|
||||
"public final class Caller {\n"
|
||||
" public double near(Point p) { return p.y(); }\n"
|
||||
" public double far(Vector v) { return v.y(); }\n"
|
||||
" public double data(Vector v) { return v.x(); }\n"
|
||||
" public Object make() { return new Vector(); }\n"
|
||||
" public boolean stat(Object o) { return Objects.nonNull(o); }\n"
|
||||
" public long refs(List<Object> xs) {\n"
|
||||
" return xs.stream().filter(Objects::nonNull).count();\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
|
||||
/* The id of the one `label` node called `name` in `file_path`; 0 when there is
|
||||
* none or more than one. `generated` (optional) receives whether its
|
||||
* properties carry the `generated: record` mark. */
|
||||
static int64_t jgen_node(cbm_store_t *s, const char *project, const char *name, const char *label,
|
||||
const char *file_path, bool *generated) {
|
||||
cbm_node_t *nodes = NULL;
|
||||
int count = 0;
|
||||
int64_t id = 0;
|
||||
int hits = 0;
|
||||
bool marked = false;
|
||||
if (cbm_store_find_nodes_by_name(s, project, name, &nodes, &count) != CBM_STORE_OK) {
|
||||
return 0;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (nodes[i].label && strcmp(nodes[i].label, label) == 0 && nodes[i].file_path &&
|
||||
strcmp(nodes[i].file_path, file_path) == 0) {
|
||||
hits++;
|
||||
id = nodes[i].id;
|
||||
marked = nodes[i].properties_json &&
|
||||
strstr(nodes[i].properties_json, "\"generated\":\"record\"") != NULL;
|
||||
}
|
||||
}
|
||||
cbm_store_free_nodes(nodes, count);
|
||||
if (generated) {
|
||||
*generated = marked;
|
||||
}
|
||||
return hits == 1 ? id : 0;
|
||||
}
|
||||
|
||||
/* CALLS edges out of `source_id` into `target_id`, or (target_id 0) into any
|
||||
* node called `target_name`. */
|
||||
static int jgen_calls(cbm_store_t *s, int64_t source_id, int64_t target_id,
|
||||
const char *target_name) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int ec = 0;
|
||||
int n = 0;
|
||||
if (cbm_store_find_edges_by_source_type(s, source_id, "CALLS", &edges, &ec) != CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 0; i < ec; i++) {
|
||||
if (target_id) {
|
||||
n += edges[i].target_id == target_id;
|
||||
continue;
|
||||
}
|
||||
cbm_node_t t = {0};
|
||||
if (cbm_store_find_node_by_id(s, edges[i].target_id, &t) == CBM_STORE_OK && t.name &&
|
||||
strcmp(t.name, target_name) == 0) {
|
||||
n++;
|
||||
}
|
||||
cbm_node_free_fields(&t);
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, ec);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Edges of `type` into `target_id`. */
|
||||
static int jgen_edges_into(cbm_store_t *s, int64_t target_id, const char *type) {
|
||||
cbm_edge_t *edges = NULL;
|
||||
int ec = 0;
|
||||
if (cbm_store_find_edges_by_target_type(s, target_id, type, &edges, &ec) != CBM_STORE_OK) {
|
||||
return -1;
|
||||
}
|
||||
if (edges) {
|
||||
cbm_store_free_edges(edges, ec);
|
||||
}
|
||||
return ec;
|
||||
}
|
||||
|
||||
/* One fact of jgen_check: reported, counted, and the check goes on, so a run
|
||||
* shows every fact that does not hold. */
|
||||
#define JGEN_EXPECT(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
printf(" FAIL %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
failed++; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
/* Every fact the fixture pins, on one indexed store; the number that fail. */
|
||||
static int jgen_check(cbm_store_t *s, const char *project) {
|
||||
int failed = 0;
|
||||
bool gen_x = false;
|
||||
bool gen_y = false;
|
||||
bool gen_z = true;
|
||||
bool gen_norm = true;
|
||||
bool gen_nn = false;
|
||||
int64_t acc_nn = jgen_node(s, project, "nonNull", "Method", "Seen.java", &gen_nn);
|
||||
int64_t acc_x = jgen_node(s, project, "x", "Method", "Point.java", &gen_x);
|
||||
int64_t acc_y = jgen_node(s, project, "y", "Method", "Point.java", &gen_y);
|
||||
int64_t own_z = jgen_node(s, project, "z", "Method", "Point.java", &gen_z);
|
||||
int64_t norm = jgen_node(s, project, "norm", "Method", "Point.java", &gen_norm);
|
||||
int64_t near_m = jgen_node(s, project, "near", "Method", "Caller.java", NULL);
|
||||
int64_t far_m = jgen_node(s, project, "far", "Method", "Caller.java", NULL);
|
||||
int64_t data = jgen_node(s, project, "data", "Method", "Caller.java", NULL);
|
||||
int64_t make = jgen_node(s, project, "make", "Method", "Caller.java", NULL);
|
||||
int64_t stat = jgen_node(s, project, "stat", "Method", "Caller.java", NULL);
|
||||
int64_t refs = jgen_node(s, project, "refs", "Method", "Caller.java", NULL);
|
||||
JGEN_EXPECT(acc_x > 0 && acc_y > 0 && own_z > 0 && norm > 0 && acc_nn > 0);
|
||||
JGEN_EXPECT(near_m > 0 && far_m > 0 && data > 0 && make > 0 && stat > 0 && refs > 0);
|
||||
/* The compiler's members are marked; the hand-written ones are not. */
|
||||
JGEN_EXPECT(gen_x);
|
||||
JGEN_EXPECT(gen_y);
|
||||
JGEN_EXPECT(gen_nn);
|
||||
JGEN_EXPECT(!gen_z);
|
||||
JGEN_EXPECT(!gen_norm);
|
||||
/* Bound by qualified name through the receiver's type ... */
|
||||
JGEN_EXPECT(jgen_calls(s, near_m, acc_y, NULL) == 1);
|
||||
/* ... and never by a short-name guess. */
|
||||
JGEN_EXPECT(jgen_calls(s, far_m, acc_y, NULL) == 0);
|
||||
JGEN_EXPECT(jgen_calls(s, far_m, 0, "y") == 0);
|
||||
/* Neither the accessor nor Spring's field `x`. */
|
||||
JGEN_EXPECT(jgen_calls(s, data, 0, "x") == 0);
|
||||
/* No import names a generated member, so nothing follows one onto it. */
|
||||
JGEN_EXPECT(jgen_edges_into(s, acc_y, "IMPORTS") == 0);
|
||||
JGEN_EXPECT(jgen_calls(s, make, 0, "y") == 0);
|
||||
/* The JDK's Objects.nonNull is not Seen's accessor, called or referenced. */
|
||||
JGEN_EXPECT(jgen_calls(s, stat, acc_nn, NULL) == 0);
|
||||
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "CALL_REFERENCE") == 0);
|
||||
JGEN_EXPECT(jgen_edges_into(s, acc_nn, "USAGE") == 0);
|
||||
return failed;
|
||||
}
|
||||
#undef JGEN_EXPECT
|
||||
|
||||
static int jgen_write_fixture(const char *tmp) {
|
||||
write_temp_file(tmp, "Point.java", jgen_point_java);
|
||||
write_temp_file(tmp, "Seen.java", jgen_seen_java);
|
||||
write_temp_file(tmp, "Spring.java", jgen_spring_java);
|
||||
write_temp_file(tmp, "Caller.java", jgen_caller_java);
|
||||
return 0;
|
||||
}
|
||||
|
||||
TEST(pipeline_java_record_accessors_generated_qn_only) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
jgen_write_fixture(tmp);
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/java_gen.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(cbm_pipeline_run(p), 0);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
int rc = jgen_check(s, cbm_pipeline_project_name(p));
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The same fixture through pass_parallel.c: >= 50 files and CBM_WORKERS=4. */
|
||||
TEST(pipeline_java_record_accessors_generated_qn_only_parallel) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_par_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
jgen_write_fixture(tmp);
|
||||
for (int i = 0; i < 52; i++) {
|
||||
char name[64];
|
||||
char body[128];
|
||||
snprintf(name, sizeof(name), "Filler%d.java", i);
|
||||
snprintf(body, sizeof(body),
|
||||
"package com.example;\nclass Filler%d { int f() { return %d; } }\n", i, i);
|
||||
write_temp_file(tmp, name, body);
|
||||
}
|
||||
char *old_workers = getenv("CBM_WORKERS");
|
||||
char *saved = old_workers ? strdup(old_workers) : NULL;
|
||||
cbm_setenv("CBM_WORKERS", "4", 1);
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/java_gen_par.db", tmp);
|
||||
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
int run = p ? cbm_pipeline_run(p) : -1;
|
||||
if (saved) {
|
||||
cbm_setenv("CBM_WORKERS", saved, 1);
|
||||
free(saved);
|
||||
} else {
|
||||
cbm_unsetenv("CBM_WORKERS");
|
||||
}
|
||||
ASSERT_NOT_NULL(p);
|
||||
ASSERT_EQ(run, 0);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
int rc = jgen_check(s, cbm_pipeline_project_name(p));
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p);
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An incremental re-index seeds its registry from the stored graph: the
|
||||
* untouched Point.java's accessors come back from their nodes, and must stay
|
||||
* out of the short-name index exactly as in the full index while the touched
|
||||
* Caller.java is re-resolved. */
|
||||
TEST(pipeline_java_record_accessors_generated_qn_only_after_incremental) {
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp), "/tmp/cbm_java_gen_incr_XXXXXX");
|
||||
if (!cbm_mkdtemp(tmp)) {
|
||||
FAIL("tmpdir");
|
||||
}
|
||||
jgen_write_fixture(tmp);
|
||||
char db_path[512];
|
||||
snprintf(db_path, sizeof(db_path), "%s/java_gen_incr.db", tmp);
|
||||
cbm_pipeline_t *p1 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p1);
|
||||
ASSERT_EQ(cbm_pipeline_run(p1), 0);
|
||||
cbm_pipeline_free(p1);
|
||||
|
||||
char caller_path[512];
|
||||
snprintf(caller_path, sizeof(caller_path), "%s/Caller.java", tmp);
|
||||
ASSERT_EQ(th_append_file(caller_path, "\n// touched: re-resolve Caller.java incrementally\n"),
|
||||
0);
|
||||
|
||||
cbm_pipeline_t *p2 = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
|
||||
ASSERT_NOT_NULL(p2);
|
||||
ASSERT_EQ(cbm_pipeline_run(p2), 0);
|
||||
cbm_store_t *s = cbm_store_open_path(db_path);
|
||||
ASSERT_NOT_NULL(s);
|
||||
int rc = jgen_check(s, cbm_pipeline_project_name(p2));
|
||||
cbm_store_close(s);
|
||||
cbm_pipeline_free(p2);
|
||||
th_rmtree(tmp);
|
||||
ASSERT_EQ(rc, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* As pyn_edge, and the edge's properties also contain `fragment`. */
|
||||
static bool pyn_edge_with(cbm_store_t *s, const char *project, const char *src_rel,
|
||||
const char *tgt_rel, const char *type, const char *fragment) {
|
||||
@@ -29120,6 +29399,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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -1369,6 +1369,126 @@ TEST(adr_validate_keys_empty) {
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── cbm_store_fetch_call_edges / cbm_store_free_call_edges tests ─ */
|
||||
|
||||
TEST(fetch_call_edges_basic) {
|
||||
cbm_store_t *s = setup_arch_test_store();
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
int64_t *src = NULL;
|
||||
int64_t *tgt = NULL;
|
||||
int count = 0;
|
||||
bool truncated = true;
|
||||
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 100, &src, &tgt, &count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
/* setup_arch_test_store() wires 5 CALLS edges among Function nodes. */
|
||||
ASSERT_EQ(count, 5);
|
||||
ASSERT_FALSE(truncated);
|
||||
ASSERT_NOT_NULL(src);
|
||||
ASSERT_NOT_NULL(tgt);
|
||||
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fetch_call_edges_truncated) {
|
||||
cbm_store_t *s = setup_arch_test_store();
|
||||
ASSERT_NOT_NULL(s);
|
||||
|
||||
int64_t *src = NULL;
|
||||
int64_t *tgt = NULL;
|
||||
int count = 0;
|
||||
bool truncated = false;
|
||||
/* Only 5 CALLS edges exist; capping at 2 must set truncated. */
|
||||
ASSERT_EQ(cbm_store_fetch_call_edges(s, "test", 2, &src, &tgt, &count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 2);
|
||||
ASSERT_TRUE(truncated);
|
||||
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
TEST(fetch_call_edges_empty_project) {
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "empty", "/tmp/empty");
|
||||
|
||||
int64_t *src = NULL;
|
||||
int64_t *tgt = NULL;
|
||||
int count = 0;
|
||||
bool truncated = true;
|
||||
ASSERT_EQ(cbm_store_fetch_call_edges(s, "empty", 100, &src, &tgt, &count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
ASSERT_FALSE(truncated);
|
||||
|
||||
/* cbm_store_free_call_edges(NULL, NULL) must not crash. */
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* The two arrays cbm_store_fetch_call_edges hands out are memory-core blocks of
|
||||
* the STORE class, and cbm_store_free_call_edges is what takes them back. Both
|
||||
* halves are measured: the fetch adds exactly the two arrays and the paired free
|
||||
* removes exactly those, so a raw allocation on either side fails. Thirty-six
|
||||
* edges outgrow the initial capacity of 16, so the growth path is covered too. */
|
||||
TEST(fetch_call_edges_memory_core_ownership) {
|
||||
enum { FN = 37 };
|
||||
cbm_store_t *s = cbm_store_open_memory();
|
||||
ASSERT_NOT_NULL(s);
|
||||
cbm_store_upsert_project(s, "grow", "/tmp/grow");
|
||||
int64_t ids[FN];
|
||||
for (int i = 0; i < FN; i++) {
|
||||
char name[32];
|
||||
char qn[64];
|
||||
snprintf(name, sizeof(name), "f%d", i);
|
||||
snprintf(qn, sizeof(qn), "grow.pkg.f%d", i);
|
||||
cbm_node_t n = {.project = "grow",
|
||||
.label = "Function",
|
||||
.name = name,
|
||||
.qualified_name = qn,
|
||||
.file_path = "pkg/f.go"};
|
||||
ids[i] = cbm_store_upsert_node(s, &n);
|
||||
ASSERT_TRUE(ids[i] > 0);
|
||||
}
|
||||
for (int i = 0; i + 1 < FN; i++) {
|
||||
cbm_edge_t e = {
|
||||
.project = "grow", .source_id = ids[i], .target_id = ids[i + 1], .type = "CALLS"};
|
||||
ASSERT_TRUE(cbm_store_insert_edge(s, &e) > 0);
|
||||
}
|
||||
|
||||
size_t bytes = cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE);
|
||||
size_t blocks = cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE);
|
||||
int64_t *src = NULL;
|
||||
int64_t *tgt = NULL;
|
||||
int count = 0;
|
||||
bool truncated = true;
|
||||
ASSERT_EQ(cbm_store_fetch_call_edges(s, "grow", 100, &src, &tgt, &count, &truncated),
|
||||
CBM_STORE_OK);
|
||||
ASSERT_EQ(count, FN - 1);
|
||||
ASSERT_FALSE(truncated);
|
||||
ASSERT_NOT_NULL(src);
|
||||
ASSERT_NOT_NULL(tgt);
|
||||
/* Ordered by (source_id, target_id): growth kept every pair in place. */
|
||||
ASSERT_EQ(src[0], ids[0]);
|
||||
ASSERT_EQ(tgt[0], ids[1]);
|
||||
ASSERT_EQ(src[FN - 2], ids[FN - 2]);
|
||||
ASSERT_EQ(tgt[FN - 2], ids[FN - 1]);
|
||||
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks + 2);
|
||||
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE) >=
|
||||
bytes + 2 * (size_t)count * sizeof(int64_t));
|
||||
|
||||
cbm_store_free_call_edges(src, tgt);
|
||||
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_STORE), bytes);
|
||||
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_STORE), blocks);
|
||||
cbm_store_close(s);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ── Louvain tests ──────────────────────────────────────────────── */
|
||||
|
||||
TEST(louvain_basic) {
|
||||
@@ -1394,7 +1514,7 @@ TEST(louvain_basic) {
|
||||
/* Triangle and pair different */
|
||||
ASSERT_TRUE(comm[1] != comm[4]);
|
||||
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
cbm_leiden_free(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1403,7 +1523,7 @@ TEST(louvain_empty) {
|
||||
int count = 0;
|
||||
ASSERT_EQ(cbm_louvain(NULL, 0, NULL, 0, &result, &count), CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 0);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
cbm_leiden_free(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1414,7 +1534,7 @@ TEST(louvain_single_node) {
|
||||
ASSERT_EQ(cbm_louvain(nodes, 1, NULL, 0, &result, &count), CBM_STORE_OK);
|
||||
ASSERT_EQ(count, 1);
|
||||
ASSERT_EQ(result[0].node_id, 42);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
cbm_leiden_free(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1477,7 +1597,7 @@ TEST(louvain_converges) {
|
||||
}
|
||||
ASSERT_TRUE(same_count >= 8);
|
||||
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
cbm_leiden_free(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1502,7 +1622,8 @@ TEST(leiden_result_memory_core_ownership) {
|
||||
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks + 1);
|
||||
ASSERT_TRUE(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER) >=
|
||||
bytes + 3 * sizeof(*result));
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
/* Released through the exported pair, the way an FFI caller must. */
|
||||
cbm_leiden_free(result);
|
||||
ASSERT_EQ(cbm_mem_class_live_bytes(CBM_MEM_CLASS_OTHER), bytes);
|
||||
ASSERT_EQ(cbm_mem_class_live_blocks(CBM_MEM_CLASS_OTHER), blocks);
|
||||
}
|
||||
@@ -1649,7 +1770,7 @@ TEST(leiden_multilevel_collapses_noise) {
|
||||
}
|
||||
ASSERT_TRUE(same >= SZ - 1);
|
||||
}
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, result);
|
||||
cbm_leiden_free(result);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1681,8 +1802,8 @@ TEST(leiden_resolution_controls_granularity) {
|
||||
ASSERT_TRUE(n_hi > n_lo);
|
||||
ASSERT_TRUE(leiden_all_communities_connected(lo, N, edges, ne));
|
||||
ASSERT_TRUE(leiden_all_communities_connected(hi, N, edges, ne));
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, lo);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, hi);
|
||||
cbm_leiden_free(lo);
|
||||
cbm_leiden_free(hi);
|
||||
PASS();
|
||||
}
|
||||
|
||||
@@ -1948,6 +2069,12 @@ SUITE(store_arch) {
|
||||
RUN_TEST(adr_splice_refuses_unterminated_fence);
|
||||
RUN_TEST(adr_validate_keys_empty);
|
||||
|
||||
/* Call-edge fetch / free */
|
||||
RUN_TEST(fetch_call_edges_basic);
|
||||
RUN_TEST(fetch_call_edges_truncated);
|
||||
RUN_TEST(fetch_call_edges_empty_project);
|
||||
RUN_TEST(fetch_call_edges_memory_core_ownership);
|
||||
|
||||
/* Louvain */
|
||||
RUN_TEST(louvain_basic);
|
||||
RUN_TEST(louvain_empty);
|
||||
|
||||
+37
-2
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user