From 7814dacf15e68d8f89951b23c366bc070c43abb1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 16:06:43 +0200 Subject: [PATCH 1/5] fix: size the compress path with size_t and prune the cache from the walk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two loose ends from the bounds work. The decompression side already took size_t so a >2 GiB capacity could not wrap through int, but compression still took int for both the source length and the destination capacity, and the artifact export cast a size_t database size down to reach it. A database past 2 GiB would have handed the encoder a negative length. Both lengths and the bound helper are size_t now, and the function returns int64_t like its decompressing counterpart. The discovery walk now prunes the cache directory by absolute path. A custom CBM_CACHE_DIR may sit inside a repository — tests do it routinely — and walking into it pulls every other project's graph database into this project's file list. This is the narrow form of a concern that was briefly implemented as refusing any root that contained the cache; refusing a whole root was too blunt, and not walking the cache is what the concern actually asks for. The test fails without the prune: a .go file planted under the cache is otherwise discovered, and "cache" is not in the built-in skip list, so the assertion is not vacuous. Signed-off-by: Martin Vogel --- internal/cbm/zstd_store.c | 10 +++++----- internal/cbm/zstd_store.h | 8 +++++--- src/discover/discover.c | 28 ++++++++++++++++++++++++++- src/pipeline/artifact.c | 4 ++-- tests/test_discover.c | 40 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/internal/cbm/zstd_store.c b/internal/cbm/zstd_store.c index 509537419..9a1001040 100644 --- a/internal/cbm/zstd_store.c +++ b/internal/cbm/zstd_store.c @@ -7,12 +7,12 @@ #include #include -int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level) { - size_t rc = ZSTD_compress(dst, (size_t)dstCap, src, (size_t)srcLen, level); +int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level) { + size_t rc = ZSTD_compress(dst, dstCap, src, srcLen, level); if (ZSTD_isError(rc)) { return 0; } - return (int)rc; + return (int64_t)rc; } int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t dstCap) { @@ -31,6 +31,6 @@ size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen) { return (size_t)n; } -size_t cbm_zstd_compress_bound(int inputSize) { - return ZSTD_compressBound((size_t)inputSize); +size_t cbm_zstd_compress_bound(size_t inputSize) { + return ZSTD_compressBound(inputSize); } diff --git a/internal/cbm/zstd_store.h b/internal/cbm/zstd_store.h index ef427df04..42381649f 100644 --- a/internal/cbm/zstd_store.h +++ b/internal/cbm/zstd_store.h @@ -5,8 +5,10 @@ #include // Zstd compression at specified level (1=fast .. 22=best). -// Returns compressed size on success, 0 on error. -int cbm_zstd_compress(const char *src, int srcLen, char *dst, int dstCap, int level); +// srcLen/dstCap are size_t for the same reason decompression's are: a >2 GiB +// database would otherwise wrap through int and hand the encoder a capacity that +// disagrees with the real buffer. Returns compressed size on success, 0 on error. +int64_t cbm_zstd_compress(const char *src, size_t srcLen, char *dst, size_t dstCap, int level); // Zstd decompression. srcLen/dstCap are size_t so a >2 GiB destination capacity // is never truncated (a large DB artifact, or a crafted one, would otherwise @@ -20,6 +22,6 @@ int64_t cbm_zstd_decompress(const char *src, size_t srcLen, char *dst, size_t ds size_t cbm_zstd_frame_content_size(const char *src, size_t srcLen); // Maximum compressed size bound for given input size. -size_t cbm_zstd_compress_bound(int inputSize); +size_t cbm_zstd_compress_bound(size_t inputSize); #endif // CBM_ZSTD_STORE_H diff --git a/src/discover/discover.c b/src/discover/discover.c index 15243d3b6..76896b98b 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -13,6 +13,7 @@ #include "foundation/constants.h" #include "foundation/compat_fs.h" +#include "foundation/workspace.h" #include "foundation/platform.h" #ifdef _WIN32 #include "foundation/win_utf8.h" @@ -562,6 +563,30 @@ static bool is_safety_core_dir(const char *name) { } /* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */ +/* The cache directory holds every indexed project's graph database. When a custom + * CBM_CACHE_DIR sits inside a repository — which happens in tests and is legal in + * production — walking into it would pull other projects' databases into this + * project's file list. Prune it by absolute path. + * + * This is the narrow remedy for a concern that was briefly implemented as + * refusing any root containing the cache: refusing a whole root was too blunt, + * and not walking the cache is what the concern actually asks for. */ +static bool dir_is_cache_tree(const char *abs_path) { + const char *cache = cbm_workspace_cache_dir(); + if (!cache || !cache[0] || !abs_path || !abs_path[0]) { + return false; + } + size_t n = strlen(cache); + while (n > 1 && (cache[n - 1] == '/' || cache[n - 1] == '\\')) { + n--; + } + if (strncmp(abs_path, cache, n) != 0) { + return false; + } + /* Boundary-aware so "x" is not treated as living under "". */ + return abs_path[n] == '\0' || abs_path[n] == '/' || abs_path[n] == '\\'; +} + static bool should_skip_directory(const char *entry_name, const char *rel_path, const cbm_discover_opts_t *opts, const cbm_gitignore_t *gitignore, const cbm_gitignore_t *global_gi, @@ -853,7 +878,8 @@ static void walk_dir_process_entry(cbm_dirent_t *entry, const walk_frame_t *fram } if (S_ISDIR(st.st_mode)) { - if (!should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore, + if (!dir_is_cache_tree(abs_path) && + !should_skip_directory(entry->name, rel_path, opts, gitignore, global_gi, cbmignore, frame->local_gi, frame->local_gi_prefix)) { walk_push_subdir(ws, abs_path, rel_path, frame, out); } else { diff --git a/src/pipeline/artifact.c b/src/pipeline/artifact.c index 66094b4f7..94fcf7263 100644 --- a/src/pipeline/artifact.c +++ b/src/pipeline/artifact.c @@ -589,14 +589,14 @@ int cbm_artifact_export(const char *db_path, const char *repo_path, const char * } /* Compress with zstd */ - size_t bound = cbm_zstd_compress_bound((int)db_size); + size_t bound = cbm_zstd_compress_bound(db_size); char *compressed = malloc(bound); if (!compressed) { free(db_data); return artifact_export_fail("compress", NULL, "alloc_compressed_buffer", 0); } - int clen = cbm_zstd_compress(db_data, (int)db_size, compressed, (int)bound, compression_level); + int64_t clen = cbm_zstd_compress(db_data, db_size, compressed, bound, compression_level); free(db_data); if (clen <= 0) { diff --git a/tests/test_discover.c b/tests/test_discover.c index 57cbefe75..00384a3da 100644 --- a/tests/test_discover.c +++ b/tests/test_discover.c @@ -297,6 +297,45 @@ TEST(pattern_dts_full) { /* ── File discovery (integration) — cross-platform via test_helpers.h ── */ +/* A custom CBM_CACHE_DIR may legitimately sit inside a repository — tests do it + * routinely. Walking into it would pull every other project's graph database into + * this project's file list, so the walk prunes it by absolute path. */ +TEST(discover_prunes_the_cache_tree) { + char *base = th_mktempdir("cbm_disc_cache"); + ASSERT(base != NULL); + + th_write_file(TH_PATH(base, "src/main.go"), "package main\n"); + /* Source-looking files inside the cache must not be discovered. */ + th_write_file(TH_PATH(base, "cache/other_project/leaked.go"), "package leaked\n"); + + const char *saved = getenv("CBM_CACHE_DIR"); + char *saved_copy = saved ? strdup(saved) : NULL; + char cache_dir[1024]; + snprintf(cache_dir, sizeof(cache_dir), "%s/cache", base); + cbm_setenv("CBM_CACHE_DIR", cache_dir, 1); + + cbm_discover_opts_t opts = {0}; + cbm_file_info_t *files = NULL; + int count = 0; + int rc = cbm_discover(base, &opts, &files, &count); + + if (saved_copy) { + cbm_setenv("CBM_CACHE_DIR", saved_copy, 1); + free(saved_copy); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + + ASSERT_EQ(rc, 0); + for (int i = 0; i < count; i++) { + ASSERT(strstr(files[i].rel_path, "leaked.go") == NULL); + } + ASSERT_EQ(count, 1); /* only src/main.go */ + cbm_discover_free(files, count); + th_cleanup(base); + PASS(); +} + TEST(discover_simple) { char *base = th_mktempdir("cbm_disc_simple"); ASSERT(base != NULL); @@ -1359,6 +1398,7 @@ TEST(discover_many_nested_gitignores_do_not_exhaust_matcher_ownership) { /* ── Suite ─────────────────────────────────────────────────────── */ SUITE(discover) { + RUN_TEST(discover_prunes_the_cache_tree); /* Directory skip — always */ RUN_TEST(skip_git); RUN_TEST(skip_node_modules); From bf0dccf1f95a04dee97b1691bbf02c5981e24d34 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 16:18:32 +0200 Subject: [PATCH 2/5] feat(cli): add allow-root to declare an indexing root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grant store was readable but nothing could write to it, so only CBM_ALLOWED_ROOT could declare a root. `allow-root ` records one, `--list` shows what is recorded, and `--approve-sensitive` is required for a home or credential directory. Roots that are refused outright — volume and drive roots, paths too broad to index as a unit — cannot be granted at all. Enrollment is a command a person types and deliberately nothing else. That is the property the whole store exists for: neither an indexed repository nor a tool caller can widen its own boundary. A confirmation delivered through the MCP surface would be answered by the same agent that may have been influenced, so it would not be a human decision. The path is canonicalized before recording, because the policy is defined over resolved paths and a grant stored as a symlink would not match the resolved path the indexer later presents. Classified stateless in the daemon bootstrap: it writes one line of user-level config and reads nothing from a project, so enrolling a root must not depend on daemon state. Without that classification the argument fell through to MCP server mode and exited silently on EOF. Verified end to end: /etc refused as too broad, $HOME refused pending the flag, a project directory recorded, a granted root indexes, and a non-granted root is refused naming the exact command that would allow it. Signed-off-by: Martin Vogel --- src/daemon/bootstrap.c | 4 +++ src/main.c | 79 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/daemon/bootstrap.c b/src/daemon/bootstrap.c index de4656a1f..1fd5d7370 100644 --- a/src/daemon/bootstrap.c +++ b/src/daemon/bootstrap.c @@ -162,6 +162,10 @@ cbm_daemon_process_role_t cbm_daemon_process_role(int argc, char *const argv[]) "install", "uninstall", "update", + /* allow-root writes one line of user-level config and reads nothing from a + * project, so it needs no daemon. Listed here rather than routed through + * the daemon so enrolling a root cannot depend on daemon state. */ + "allow-root", }; /* Stop at the first top-level mode token. Tool names, flag values, and JSON * following `cli` are opaque user input: a search query named "install" diff --git a/src/main.c b/src/main.c index 6a03d0624..8a0ceb646 100644 --- a/src/main.c +++ b/src/main.c @@ -52,6 +52,7 @@ enum { #include "foundation/log.h" #include "foundation/diagnostics.h" #include "foundation/platform.h" +#include "foundation/workspace.h" #include "foundation/compat.h" #include "foundation/compat_fs.h" #include "foundation/compat_thread.h" @@ -921,6 +922,81 @@ static void print_help(void) { /* Try to handle a subcommand (cli/install/uninstall/update/config/--version/--help). * Returns -1 if no subcommand matched, otherwise the exit code. */ +/* `allow-root [--approve-sensitive] ` — record an indexing root. + * + * Enrollment lives here, in a command a person types, and deliberately nowhere + * else: the whole point of the grant store is that neither an indexed repository + * nor a tool caller can widen its own boundary. A confirmation delivered through + * the MCP surface would be answered by the same agent that may have been + * influenced, so it would not be a human decision at all. */ +static int main_run_allow_root(int argc, char **argv) { + const char *path = NULL; + bool approve_sensitive = false; + bool list_only = false; + for (int i = 0; i < argc; i++) { + if (strcmp(argv[i], "--approve-sensitive") == 0) { + approve_sensitive = true; + } else if (strcmp(argv[i], "--list") == 0) { + list_only = true; + } else if (argv[i][0] == '-') { + (void)fprintf(stderr, "error: unknown option: %s\n", argv[i]); + return EXIT_FAILURE; + } else if (!path) { + path = argv[i]; + } else { + (void)fprintf(stderr, "error: only one path may be given\n"); + return EXIT_FAILURE; + } + } + + const char *cache_dir = cbm_workspace_cache_dir(); + if (!cache_dir || !cache_dir[0]) { + (void)fprintf(stderr, "error: cache directory could not be resolved\n"); + return EXIT_FAILURE; + } + + if (list_only || !path) { + char listing[CBM_SZ_8K]; + if (cbm_workspace_grant_list(cache_dir, listing, sizeof(listing))) { + printf("allowed roots:\n%s", listing); + } else { + printf("no allowed roots recorded — indexing is unconfined apart from the " + "always-refused roots (see docs/CONFIGURATION.md)\n"); + } + if (!path && !list_only) { + (void)fprintf(stderr, + "usage: codebase-memory-mcp allow-root [--approve-sensitive] \n" + " codebase-memory-mcp allow-root --list\n"); + return EXIT_FAILURE; + } + return 0; + } + + /* Canonicalize before recording: the policy is defined over resolved paths, + * and a grant stored as a symlink would not match the resolved repo path the + * indexer later presents. */ + char canonical[CBM_PATH_MAX]; + if (!cbm_canonical_path(path, canonical, sizeof(canonical))) { + (void)fprintf(stderr, "error: cannot resolve path: %s\n", path); + return EXIT_FAILURE; + } + if (!cbm_is_dir(canonical)) { + (void)fprintf(stderr, "error: not a directory: %s\n", canonical); + return EXIT_FAILURE; + } + + char err[CBM_SZ_1K]; + if (!cbm_workspace_grant_add(cache_dir, cbm_workspace_home_dir(), canonical, approve_sensitive, + err, sizeof(err))) { + (void)fprintf(stderr, "refused: %s\n", err[0] ? err : "not an allowable root"); + return EXIT_FAILURE; + } + printf("allowed root recorded: %s\n", canonical); + printf("note: with at least one root recorded, indexing is now confined to the " + "recorded roots.\n"); + return 0; +} + static int handle_subcommand(int argc, char **argv, cbm_project_lock_manager_t *project_locks, main_local_maintenance_context_t *maintenance_context) { /* First scan: global flags */ @@ -938,6 +1014,9 @@ static int handle_subcommand(int argc, char **argv, cbm_project_lock_manager_t * print_help(); return 0; } + if (strcmp(argv[i], "allow-root") == 0) { + return main_run_allow_root(argc - i - SKIP_ONE, argv + i + SKIP_ONE); + } if (strcmp(argv[i], "cli") == 0) { cbm_mem_init_with_cap(cbm_mem_ram_fraction_for_total(cbm_system_info().total_ram), cbm_index_worker_memory_budget_bytes()); From 80d52797f61d9a13abb613d8c8fedd9691fe50d2 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 18:00:08 +0200 Subject: [PATCH 3/5] feat(foundation): per-project request manifest with content-keyed approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A repository may ship .cbmpathwhitelist listing outside roots it would like indexed alongside it. The file REQUESTS; it never GRANTS. Both attackers in this project's threat model can write a file inside a repository — a malicious indexed repo, and an agent with write access to the project — so a repo-local file cannot be authoritative without handing them the boundary. Approval is recorded user-level and keyed to a SHA-256 of the manifest bytes, so editing the file lapses approval and it must be granted again. Without the hash a repo approved once could widen itself forever on a later pull. This is direnv's model, and it is the only shape that keeps a checked-in file's ergonomics without making the file a permission slip. The hash covers the raw bytes rather than the parsed entries: a reordering or comment change is still a change the approver has not seen. `allow-root --approve-manifest ` is the human action that grants. Approval refuses outright if any requested entry would not stand on its own as an indexing root, so approving cannot become a route around the breadth policy, and entries are re-classified at use time as well so a stored approval cannot outrank a credential list that has grown since. Entries containing control characters are rejected — the same shape as the newline splitting that let a crafted indexed path inject an extra entry into the scoped file list. Deliberately NOT wired into the root boundary. A project's manifest authorizes outside roots FOR THAT PROJECT, so the question is "may project P pull in tree T", not "may T be indexed standalone", and the boundary check only ever sees one path with no project context to ask that with. A first draft did wire it there by passing the candidate as its own project root, which read a manifest that by definition was not the one requesting it and so authorized nothing — caught by exercising it end to end. The consuming half belongs where project context exists, in discovery walking a project's approved extra roots; cbm_workspace_manifest_allows is the query it will use. Five tests, including the one the design rests on: approval lapses when the manifest content changes. Signed-off-by: Martin Vogel --- src/foundation/workspace.c | 224 +++++++++++++++++++++++++++++++++++++ src/foundation/workspace.h | 49 ++++++++ src/main.c | 24 ++++ tests/test_workspace.c | 111 ++++++++++++++++++ 4 files changed, 408 insertions(+) diff --git a/src/foundation/workspace.c b/src/foundation/workspace.c index 43e1fad38..a85e17822 100644 --- a/src/foundation/workspace.c +++ b/src/foundation/workspace.c @@ -7,6 +7,7 @@ #include "foundation/compat.h" #include "foundation/compat_fs.h" #include "foundation/platform.h" +#include "foundation/sha256.h" #include @@ -511,6 +512,18 @@ bool cbm_workspace_root_allowed(const char *canonical_path, const char *home_dir * then applies to paths that ARE inside the declared root but are still too * broad to index as one unit. */ bool boundary_declared = grants > 0 || configured; + /* No manifest consultation here, deliberately. + * + * A project's manifest authorizes outside roots FOR THAT PROJECT, so the + * question it answers is "may project P pull in tree T", not "may T be indexed + * standalone". This function only ever sees one path, so it has no project + * context to ask that question with — an earlier draft passed the candidate as + * its own project root, which read a manifest that by definition was not the + * one that requested it and so never authorized anything. + * + * The consuming half belongs where the project context exists: discovery + * walking a project's approved extra roots. cbm_workspace_manifest_allows is + * the query that half will use. */ if (boundary_declared && !match.contained && !configured_contains) { if (err) { /* Keep the "outside the allowed root" wording: changing it broke an @@ -564,3 +577,214 @@ const char *cbm_workspace_home_dir(void) { const char *cbm_workspace_cache_dir(void) { return cbm_resolve_cache_dir(); } + +/* ── Per-project request manifest ─────────────────────────────────────────── */ + +bool cbm_workspace_manifest_read(const char *project_root, cbm_ws_manifest_t *out) { + if (!out) { + return false; + } + memset(out, 0, sizeof(*out)); + if (!project_root || !project_root[0]) { + return true; /* nothing to read is not a malformed manifest */ + } + + char path[WS_LINE_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", project_root, CBM_WS_MANIFEST_NAME); + if (n <= 0 || (size_t)n >= sizeof(path)) { + return true; + } + FILE *f = cbm_fopen(path, "rb"); + if (!f) { + return true; + } + + /* Hash the raw bytes, not the parsed entries: a comment or ordering change is + * still a change the approver has not seen, and should lapse approval. */ + char *raw = NULL; + size_t raw_len = 0; + char chunk[WS_LINE_MAX]; + size_t got = 0; + while ((got = fread(chunk, 1, sizeof(chunk), f)) > 0) { + char *grown = realloc(raw, raw_len + got); + if (!grown) { + free(raw); + (void)fclose(f); + return false; + } + raw = grown; + memcpy(raw + raw_len, chunk, got); + raw_len += got; + } + (void)fclose(f); + out->present = true; + cbm_sha256_hex(raw ? raw : "", raw_len, out->digest); + + /* Parse entries out of the same bytes. */ + bool ok = true; + size_t line_start = 0; + for (size_t i = 0; i <= raw_len && ok; i++) { + bool eol = (i == raw_len) || raw[i] == '\n'; + if (!eol) { + continue; + } + size_t len = i - line_start; + while (len > 0 && (raw[line_start + len - 1] == '\r' || raw[line_start + len - 1] == ' ')) { + len--; + } + if (len > 0 && raw[line_start] != '#') { + if (len >= sizeof(out->entries[0]) || out->count >= CBM_WS_MANIFEST_MAX_ENTRIES) { + ok = false; + break; + } + /* A control character in a path is never legitimate and is exactly how + * a crafted entry would try to smuggle a second value past a reader. */ + for (size_t k = 0; k < len; k++) { + unsigned char c = (unsigned char)raw[line_start + k]; + if (c < 0x20) { + ok = false; + break; + } + } + if (!ok) { + break; + } + memcpy(out->entries[out->count], raw + line_start, len); + out->entries[out->count][len] = '\0'; + out->count++; + } + line_start = i + 1; + } + free(raw); + if (!ok) { + memset(out, 0, sizeof(*out)); + } + return ok; +} + +static bool ws_approval_path(const char *cache_dir, char *out, size_t out_sz) { + if (!cache_dir || !cache_dir[0]) { + return false; + } + int n = snprintf(out, out_sz, "%s/approved_manifests", cache_dir); + return n > 0 && (size_t)n < out_sz; +} + +bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *project_root, + const cbm_ws_manifest_t *manifest) { + if (!manifest || !manifest->present || !manifest->digest[0] || !project_root) { + return false; + } + char store[WS_LINE_MAX]; + if (!ws_approval_path(cache_dir, store, sizeof(store))) { + return false; + } + FILE *f = cbm_fopen(store, "r"); + if (!f) { + return false; + } + char line[WS_LINE_MAX]; + bool found = false; + while (!found && fgets(line, (int)sizeof(line), f)) { + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) { + line[--len] = '\0'; + } + char *sep = strchr(line, ' '); + if (!sep) { + continue; + } + *sep = '\0'; + /* Both the digest and the project must match: an approval is for this + * content in this project, not for the content anywhere. */ + found = strcmp(line, manifest->digest) == 0 && ws_paths_equal(sep + 1, project_root); + } + (void)fclose(f); + return found; +} + +bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir, + const char *project_root, char *err, size_t err_sz) { + if (err && err_sz) { + err[0] = '\0'; + } + cbm_ws_manifest_t m; + if (!cbm_workspace_manifest_read(project_root, &m)) { + if (err) { + snprintf(err, err_sz, + "%s is malformed (control characters, an over-long entry, or " + "more than %d entries)", + CBM_WS_MANIFEST_NAME, CBM_WS_MANIFEST_MAX_ENTRIES); + } + return false; + } + if (!m.present) { + if (err) { + snprintf(err, err_sz, "no %s in %s", CBM_WS_MANIFEST_NAME, + project_root ? project_root : "(none)"); + } + return false; + } + + /* Approving a manifest must not become a way around the breadth policy: every + * requested entry has to stand on its own as an indexing root. */ + for (int i = 0; i < m.count; i++) { + cbm_ws_verdict_t v = cbm_workspace_classify_root(m.entries[i], home_dir, cache_dir); + if (v != CBM_WS_ALLOW) { + if (err) { + snprintf(err, err_sz, "requested path %s: %s", m.entries[i], + cbm_workspace_verdict_reason(v)); + } + return false; + } + } + + char store[WS_LINE_MAX]; + if (!ws_approval_path(cache_dir, store, sizeof(store))) { + if (err) { + snprintf(err, err_sz, "cache path too long"); + } + return false; + } + if (cbm_workspace_manifest_is_approved(cache_dir, project_root, &m)) { + return true; + } + FILE *f = cbm_fopen(store, "a"); + if (!f) { + if (err) { + snprintf(err, err_sz, "cannot write %s", store); + } + return false; + } + (void)fprintf(f, "%s %s\n", m.digest, project_root); + bool ok = fclose(f) == 0; + if (!ok && err) { + snprintf(err, err_sz, "cannot write %s", store); + } + return ok; +} + +bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir, + const char *project_root, const char *candidate) { + if (!candidate || !candidate[0]) { + return false; + } + cbm_ws_manifest_t m; + if (!cbm_workspace_manifest_read(project_root, &m) || !m.present) { + return false; + } + if (!cbm_workspace_manifest_is_approved(cache_dir, project_root, &m)) { + return false; + } + for (int i = 0; i < m.count; i++) { + /* Re-classify at use time as well as at approval time: the credential list + * may have grown since, and a stored approval must not outrank it. */ + if (cbm_workspace_classify_root(m.entries[i], home_dir, cache_dir) != CBM_WS_ALLOW) { + continue; + } + if (cbm_path_within_root(m.entries[i], candidate)) { + return true; + } + } + return false; +} diff --git a/src/foundation/workspace.h b/src/foundation/workspace.h index ef1a2e5ef..276079155 100644 --- a/src/foundation/workspace.h +++ b/src/foundation/workspace.h @@ -105,3 +105,52 @@ const char *cbm_workspace_home_dir(void); const char *cbm_workspace_cache_dir(void); #endif /* CBM_FOUNDATION_WORKSPACE_H */ + +/* + * ── Per-project request manifest (.cbmpathwhitelist) ───────────────────────── + * + * A repository may ship a manifest listing outside roots it would like indexed + * alongside it. The file REQUESTS; it never GRANTS. Both attackers in this + * project's threat model can write a file inside a repository — a malicious + * indexed repo, and an agent with project write access — so a repo-local file + * cannot be authoritative without handing them the boundary. + * + * Approval is recorded user-level and keyed to a SHA-256 of the manifest bytes, + * so editing the file (a `git pull` that widens the requests, say) lapses the + * approval and asks again. Without the hash, a repo approved once could widen + * itself forever. This is direnv's model, and it is the only shape that keeps the + * ergonomics of a checked-in file without making the file a permission slip. + */ + +#define CBM_WS_MANIFEST_NAME ".cbmpathwhitelist" + +enum { CBM_WS_MANIFEST_MAX_ENTRIES = 64 }; + +typedef struct { + /* Requested roots, verbatim from the file (already control-char screened). */ + char entries[CBM_WS_MANIFEST_MAX_ENTRIES][1024]; + int count; + /* SHA-256 hex of the raw file bytes; empty when there is no manifest. */ + char digest[65]; + bool present; +} cbm_ws_manifest_t; + +/* Read /.cbmpathwhitelist. Returns false only on a malformed file + * (an entry containing control characters, or more entries than the cap); a + * missing file is success with present=false. */ +bool cbm_workspace_manifest_read(const char *project_root, cbm_ws_manifest_t *out); + +/* True when this exact manifest content has been approved for this project. A + * changed manifest is a different digest and so is not approved. */ +bool cbm_workspace_manifest_is_approved(const char *cache_dir, const char *project_root, + const cbm_ws_manifest_t *manifest); + +/* Record approval for the manifest currently on disk. Refuses when a requested + * entry would not be allowable as a root on its own — approving a manifest must + * not become a way around the breadth policy. */ +bool cbm_workspace_manifest_approve(const char *cache_dir, const char *home_dir, + const char *project_root, char *err, size_t err_sz); + +/* True when candidate is at or below an APPROVED manifest entry of project_root. */ +bool cbm_workspace_manifest_allows(const char *cache_dir, const char *home_dir, + const char *project_root, const char *candidate); diff --git a/src/main.c b/src/main.c index 8a0ceb646..04bbfbc72 100644 --- a/src/main.c +++ b/src/main.c @@ -933,11 +933,14 @@ static int main_run_allow_root(int argc, char **argv) { const char *path = NULL; bool approve_sensitive = false; bool list_only = false; + bool approve_manifest = false; for (int i = 0; i < argc; i++) { if (strcmp(argv[i], "--approve-sensitive") == 0) { approve_sensitive = true; } else if (strcmp(argv[i], "--list") == 0) { list_only = true; + } else if (strcmp(argv[i], "--approve-manifest") == 0) { + approve_manifest = true; } else if (argv[i][0] == '-') { (void)fprintf(stderr, "error: unknown option: %s\n", argv[i]); return EXIT_FAILURE; @@ -966,12 +969,33 @@ static int main_run_allow_root(int argc, char **argv) { if (!path && !list_only) { (void)fprintf(stderr, "usage: codebase-memory-mcp allow-root [--approve-sensitive] \n" + " codebase-memory-mcp allow-root --approve-manifest \n" " codebase-memory-mcp allow-root --list\n"); return EXIT_FAILURE; } return 0; } + if (approve_manifest) { + /* Approve the manifest a project ships, keyed to its current content. The + * file only ever requests; this is the human action that grants. */ + char canonical_project[CBM_PATH_MAX]; + if (!cbm_canonical_path(path, canonical_project, sizeof(canonical_project))) { + (void)fprintf(stderr, "error: cannot resolve path: %s\n", path); + return EXIT_FAILURE; + } + char merr[CBM_SZ_1K]; + if (!cbm_workspace_manifest_approve(cache_dir, cbm_workspace_home_dir(), canonical_project, + merr, sizeof(merr))) { + (void)fprintf(stderr, "refused: %s\n", merr[0] ? merr : "manifest not approvable"); + return EXIT_FAILURE; + } + printf("manifest approved for %s\n", canonical_project); + printf("note: editing %s lapses this approval and it must be granted again.\n", + CBM_WS_MANIFEST_NAME); + return 0; + } + /* Canonicalize before recording: the policy is defined over resolved paths, * and a grant stored as a symlink would not match the resolved repo path the * indexer later presents. */ diff --git a/tests/test_workspace.c b/tests/test_workspace.c index c46f96866..736b4477f 100644 --- a/tests/test_workspace.c +++ b/tests/test_workspace.c @@ -6,7 +6,11 @@ */ #include "../src/foundation/compat.h" #include "test_framework.h" +#include "test_helpers.h" #include "foundation/workspace.h" +#include "foundation/compat_fs.h" +#include +#include static const char *HOME = "/Users/dev"; static const char *CACHE = "/Users/dev/.cache/codebase-memory-mcp"; @@ -155,7 +159,114 @@ TEST(ws_every_verdict_has_a_reason) { PASS(); } +/* ── Per-project request manifest ───────────────────────────────────────── */ + +static void ws_write(const char *path, const char *body) { + FILE *f = cbm_fopen(path, "wb"); + if (f) { + (void)fputs(body, f); + (void)fclose(f); + } +} + +TEST(ws_manifest_absent_is_not_an_error) { + char *base = th_mktempdir("cbm_ws_m0"); + ASSERT(base != NULL); + cbm_ws_manifest_t m; + ASSERT_TRUE(cbm_workspace_manifest_read(base, &m)); + ASSERT_FALSE(m.present); + ASSERT_EQ(m.count, 0); + th_cleanup(base); + PASS(); +} + +TEST(ws_manifest_parses_entries_and_skips_comments) { + char *base = th_mktempdir("cbm_ws_m1"); + ASSERT(base != NULL); + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); + ws_write(path, "# a comment\n/opt/sdk\n\n/srv/protos\n"); + cbm_ws_manifest_t m; + ASSERT_TRUE(cbm_workspace_manifest_read(base, &m)); + ASSERT_TRUE(m.present); + ASSERT_EQ(m.count, 2); + ASSERT_STR_EQ(m.entries[0], "/opt/sdk"); + ASSERT_STR_EQ(m.entries[1], "/srv/protos"); + ASSERT_EQ((int)strlen(m.digest), 64); + th_cleanup(base); + PASS(); +} + +/* A control character is how a crafted entry would smuggle a second value past a + * line reader — the same shape as the newline splitting in the scoped file list. */ +TEST(ws_manifest_rejects_control_characters) { + char *base = th_mktempdir("cbm_ws_m2"); + ASSERT(base != NULL); + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); + ws_write(path, "/opt/sdk\t\x01evil\n"); + cbm_ws_manifest_t m; + ASSERT_FALSE(cbm_workspace_manifest_read(base, &m)); + ASSERT_EQ(m.count, 0); + th_cleanup(base); + PASS(); +} + +/* THE property the design rests on: a manifest grants nothing until a person + * approves it, and editing it lapses that approval rather than inheriting it. */ +TEST(ws_manifest_approval_is_keyed_to_content) { + char *base = th_mktempdir("cbm_ws_m3"); + char *cache = th_mktempdir("cbm_ws_m3c"); + ASSERT(base != NULL); + ASSERT(cache != NULL); + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); + ws_write(path, "/opt/sdk\n"); + + cbm_ws_manifest_t before; + ASSERT_TRUE(cbm_workspace_manifest_read(base, &before)); + /* Unapproved grants nothing. */ + ASSERT_FALSE(cbm_workspace_manifest_is_approved(cache, base, &before)); + + char err[1024]; + ASSERT_TRUE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); + ASSERT_TRUE(cbm_workspace_manifest_is_approved(cache, base, &before)); + + /* Widen the requests, as a `git pull` would. Approval must lapse. */ + ws_write(path, "/opt/sdk\n/srv/protos\n"); + cbm_ws_manifest_t after; + ASSERT_TRUE(cbm_workspace_manifest_read(base, &after)); + ASSERT_TRUE(strcmp(before.digest, after.digest) != 0); + ASSERT_FALSE(cbm_workspace_manifest_is_approved(cache, base, &after)); + + th_cleanup(base); + th_cleanup(cache); + PASS(); +} + +/* Approving a manifest must not become a route around the breadth policy. */ +TEST(ws_manifest_approval_refuses_overbroad_requests) { + char *base = th_mktempdir("cbm_ws_m4"); + char *cache = th_mktempdir("cbm_ws_m4c"); + ASSERT(base != NULL); + ASSERT(cache != NULL); + char path[1024]; + snprintf(path, sizeof(path), "%s/%s", base, CBM_WS_MANIFEST_NAME); + ws_write(path, "/etc\n"); + char err[1024]; + ASSERT_FALSE(cbm_workspace_manifest_approve(cache, HOME, base, err, sizeof(err))); + ASSERT_TRUE(strstr(err, "too broad") != NULL); + th_cleanup(base); + th_cleanup(cache); + PASS(); +} + SUITE(workspace) { + RUN_TEST(ws_manifest_absent_is_not_an_error); + RUN_TEST(ws_manifest_parses_entries_and_skips_comments); + RUN_TEST(ws_manifest_rejects_control_characters); + RUN_TEST(ws_manifest_approval_is_keyed_to_content); + RUN_TEST(ws_manifest_approval_refuses_overbroad_requests); RUN_TEST(ws_depth_counts_components_below_the_volume); RUN_TEST(ws_volume_roots_are_absolutely_denied); RUN_TEST(ws_non_absolute_paths_are_denied); From 91f622315bcc41be2a8805c8b82b3b9674f076bf Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Thu, 6 Aug 2026 18:15:48 +0200 Subject: [PATCH 4/5] fix(test): free the fixture project name before reindexing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three language fixtures assign cbm_project_name_from_path into lp->project each time they open an indexed store. Teardown frees the last one, so a fixture that indexes more than once dropped every earlier heap name — 68 allocations across the run, which the macOS leak lane reports at process exit. Pre-existing rather than introduced here: none of these files or fqn.c are in this branch's diff, and the leak lane passed on the previous PR. Adding a suite shifted what the leak job sees, which is how it surfaced. Fixing it here rather than recording it, since a gate that only stays green by composition accident is not a gate. Freeing before reassigning also makes the ownership obvious at the point it matters, instead of relying on the reader noticing the teardown three functions away. Signed-off-by: Martin Vogel --- tests/test_edge_imports.c | 410 +++++++-------- tests/test_lang_contract.c | 72 +-- tests/test_lsp_resolution_probe.c | 795 +++++++++++++----------------- 3 files changed, 600 insertions(+), 677 deletions(-) diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index caf723267..8f34e95a7 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -68,7 +68,7 @@ typedef struct { } EILangProj; typedef struct { - const char *name; /* relative filename, may include '/' for subdirs */ + const char *name; /* relative filename, may include '/' for subdirs */ const char *content; } EILangFile; @@ -78,7 +78,8 @@ typedef struct { static void ei_to_fwd_slashes(char *p) { for (; *p; p++) { - if (*p == '\\') *p = '/'; + if (*p == '\\') + *p = '/'; } } @@ -86,7 +87,8 @@ static void ei_to_fwd_slashes(char *p) { static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int nfiles) { memset(lp, 0, sizeof(*lp)); snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_ei_XXXXXX"); - if (!cbm_mkdtemp(lp->tmpdir)) return NULL; + if (!cbm_mkdtemp(lp->tmpdir)) + return NULL; ei_to_fwd_slashes(lp->tmpdir); for (int i = 0; i < nfiles; i++) { @@ -100,16 +102,23 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int *slash = '/'; } FILE *f = fopen(path, "wb"); - if (!f) return NULL; + if (!f) + return NULL; fputs(files[i].content, f); fclose(f); } + /* Freed before reassigning: a fixture that indexes more than once would + * otherwise drop the previous heap name on the floor. Teardown frees the + * last one. */ + free(lp->project); lp->project = cbm_project_name_from_path(lp->tmpdir); - if (!lp->project) return NULL; + if (!lp->project) + return NULL; const char *home = getenv("HOME"); - if (!home) home = "/tmp"; + if (!home) + home = "/tmp"; char cache_dir[512]; snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home); cbm_mkdir(cache_dir); @@ -117,12 +126,14 @@ static cbm_store_t *ei_index_files(EILangProj *lp, const EILangFile *files, int unlink(lp->dbpath); lp->srv = cbm_mcp_server_new(NULL); - if (!lp->srv) return NULL; + if (!lp->srv) + return NULL; char args[700]; snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir); char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args); - if (resp) free(resp); + if (resp) + free(resp); return cbm_store_open_path(lp->dbpath); } @@ -211,8 +222,12 @@ static int64_t ei_node_id_for_file_label(cbm_store_t *store, const char *project } static void ei_cleanup(EILangProj *lp, cbm_store_t *store) { - if (store) cbm_store_close(store); - if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } + if (store) + cbm_store_close(store); + if (lp->srv) { + cbm_mcp_server_free(lp->srv); + lp->srv = NULL; + } free(lp->project); lp->project = NULL; th_rmtree(lp->tmpdir); @@ -247,8 +262,8 @@ static int ei_edge_present(const EILangFile *files, int nfiles, const char *edge /* Python: `from .util import helper` — canonical relative import. */ TEST(ei_python_relative_from_import) { static const EILangFile f[] = { - {"util.py", "def helper(x):\n return x + 1\n"}, - {"main.py", "from .util import helper\n\ndef run(y):\n return helper(y)\n"}}; + {"util.py", "def helper(x):\n return x + 1\n"}, + {"main.py", "from .util import helper\n\ndef run(y):\n return helper(y)\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -256,8 +271,8 @@ TEST(ei_python_relative_from_import) { /* Python: bare `import util` (absolute, same directory). */ TEST(ei_python_absolute_import) { static const EILangFile f[] = { - {"util.py", "def compute(x):\n return x * 2\n"}, - {"main.py", "import util\n\ndef run(y):\n return util.compute(y)\n"}}; + {"util.py", "def compute(x):\n return x * 2\n"}, + {"main.py", "import util\n\ndef run(y):\n return util.compute(y)\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -265,8 +280,8 @@ TEST(ei_python_absolute_import) { /* Python: `from util import compute` — named absolute import. */ TEST(ei_python_from_absolute_import) { static const EILangFile f[] = { - {"util.py", "def compute(x):\n return x * 2\n"}, - {"main.py", "from util import compute\n\ndef run(y):\n return compute(y)\n"}}; + {"util.py", "def compute(x):\n return x * 2\n"}, + {"main.py", "from util import compute\n\ndef run(y):\n return compute(y)\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -274,8 +289,9 @@ TEST(ei_python_from_absolute_import) { /* Python: multiple names in one `from` statement. */ TEST(ei_python_from_multi_names) { static const EILangFile f[] = { - {"ops.py", "def add(a, b):\n return a + b\n\ndef mul(a, b):\n return a * b\n"}, - {"client.py","from ops import add, mul\n\ndef run(x, y):\n return add(x, mul(x, y))\n"}}; + {"ops.py", "def add(a, b):\n return a + b\n\ndef mul(a, b):\n return a * b\n"}, + {"client.py", + "from ops import add, mul\n\ndef run(x, y):\n return add(x, mul(x, y))\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -283,8 +299,8 @@ TEST(ei_python_from_multi_names) { /* Python: aliased import `import util as u`. */ TEST(ei_python_aliased_import) { static const EILangFile f[] = { - {"util.py", "def helper(x):\n return x + 1\n"}, - {"main.py", "import util as u\n\ndef run(y):\n return u.helper(y)\n"}}; + {"util.py", "def helper(x):\n return x + 1\n"}, + {"main.py", "import util as u\n\ndef run(y):\n return u.helper(y)\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -293,8 +309,8 @@ TEST(ei_python_aliased_import) { TEST(ei_python_subpackage_import) { static const EILangFile f[] = { {"pkg/__init__.py", ""}, - {"pkg/util.py", "def fn(x):\n return x\n"}, - {"main.py", "from pkg.util import fn\n\ndef run(y):\n return fn(y)\n"}}; + {"pkg/util.py", "def fn(x):\n return x\n"}, + {"main.py", "from pkg.util import fn\n\ndef run(y):\n return fn(y)\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -302,8 +318,8 @@ TEST(ei_python_subpackage_import) { /* Python: wildcard `from util import *`. */ TEST(ei_python_wildcard_import) { static const EILangFile f[] = { - {"util.py", "X = 42\n\ndef helper():\n return X\n"}, - {"main.py", "from util import *\n\ndef run():\n return helper()\n"}}; + {"util.py", "X = 42\n\ndef helper():\n return X\n"}, + {"main.py", "from util import *\n\ndef run():\n return helper()\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -312,8 +328,8 @@ TEST(ei_python_wildcard_import) { TEST(ei_python_package_sibling_import) { static const EILangFile f[] = { {"pkg/__init__.py", ""}, - {"pkg/a.py", "def alpha():\n return 1\n"}, - {"pkg/b.py", "from .a import alpha\n\ndef beta():\n return alpha() + 1\n"}}; + {"pkg/a.py", "def alpha():\n return 1\n"}, + {"pkg/b.py", "from .a import alpha\n\ndef beta():\n return alpha() + 1\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -328,9 +344,9 @@ TEST(ei_python_package_sibling_import) { /* TypeScript: named relative import — the canonical GREEN guard. */ TEST(ei_typescript_named_relative_import) { static const EILangFile f[] = { - {"util.ts", "export function helper(x: number): number { return x + 1; }\n"}, - {"main.ts", "import { helper } from './util';\n\n" - "export function run(y: number): number { return helper(y); }\n"}}; + {"util.ts", "export function helper(x: number): number { return x + 1; }\n"}, + {"main.ts", "import { helper } from './util';\n\n" + "export function run(y: number): number { return helper(y); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -338,9 +354,9 @@ TEST(ei_typescript_named_relative_import) { /* TypeScript: default import `import helper from './util'`. */ TEST(ei_typescript_default_import) { static const EILangFile f[] = { - {"util.ts", "export default function helper(x: number): number { return x + 1; }\n"}, - {"main.ts", "import helper from './util';\n\n" - "export function run(y: number): number { return helper(y); }\n"}}; + {"util.ts", "export default function helper(x: number): number { return x + 1; }\n"}, + {"main.ts", "import helper from './util';\n\n" + "export function run(y: number): number { return helper(y); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -348,10 +364,10 @@ TEST(ei_typescript_default_import) { /* TypeScript: namespace import `import * as util from './util'`. */ TEST(ei_typescript_namespace_import) { static const EILangFile f[] = { - {"util.ts", "export const VALUE = 42;\n" - "export function compute(x: number): number { return x * VALUE; }\n"}, - {"main.ts", "import * as util from './util';\n\n" - "export function run(): number { return util.compute(util.VALUE); }\n"}}; + {"util.ts", "export const VALUE = 42;\n" + "export function compute(x: number): number { return x * VALUE; }\n"}, + {"main.ts", "import * as util from './util';\n\n" + "export function run(): number { return util.compute(util.VALUE); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -359,9 +375,9 @@ TEST(ei_typescript_namespace_import) { /* TypeScript: aliased named import `import { helper as h } from './util'`. */ TEST(ei_typescript_aliased_import) { static const EILangFile f[] = { - {"util.ts", "export function helper(x: number): number { return x + 1; }\n"}, - {"main.ts", "import { helper as h } from './util';\n\n" - "export function run(y: number): number { return h(y); }\n"}}; + {"util.ts", "export function helper(x: number): number { return x + 1; }\n"}, + {"main.ts", "import { helper as h } from './util';\n\n" + "export function run(y: number): number { return h(y); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -369,10 +385,11 @@ TEST(ei_typescript_aliased_import) { /* TypeScript: multi-name import `import { a, b } from './ops'`. */ TEST(ei_typescript_multi_names_import) { static const EILangFile f[] = { - {"ops.ts", "export function add(a: number, b: number): number { return a + b; }\n" - "export function mul(a: number, b: number): number { return a * b; }\n"}, - {"client.ts","import { add, mul } from './ops';\n\n" - "export function run(x: number, y: number): number { return add(x, mul(x, y)); }\n"}}; + {"ops.ts", "export function add(a: number, b: number): number { return a + b; }\n" + "export function mul(a: number, b: number): number { return a * b; }\n"}, + {"client.ts", + "import { add, mul } from './ops';\n\n" + "export function run(x: number, y: number): number { return add(x, mul(x, y)); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -381,8 +398,8 @@ TEST(ei_typescript_multi_names_import) { TEST(ei_typescript_subdir_import) { static const EILangFile f[] = { {"pkg/util.ts", "export function fn(x: number): number { return x; }\n"}, - {"main.ts", "import { fn } from './pkg/util';\n\n" - "export function run(y: number): number { return fn(y); }\n"}}; + {"main.ts", "import { fn } from './pkg/util';\n\n" + "export function run(y: number): number { return fn(y); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -390,7 +407,7 @@ TEST(ei_typescript_subdir_import) { /* TypeScript: re-export `export { fn } from './util'`. */ TEST(ei_typescript_re_export) { static const EILangFile f[] = { - {"util.ts", "export function fn(x: number): number { return x; }\n"}, + {"util.ts", "export function fn(x: number): number { return x; }\n"}, {"index.ts", "export { fn } from './util';\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); @@ -400,8 +417,8 @@ TEST(ei_typescript_re_export) { TEST(ei_typescript_type_import) { static const EILangFile f[] = { {"types.ts", "export interface Config { value: number; }\n"}, - {"main.ts", "import type { Config } from './types';\n\n" - "export function run(c: Config): number { return c.value; }\n"}}; + {"main.ts", "import type { Config } from './types';\n\n" + "export function run(c: Config): number { return c.value; }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -416,10 +433,10 @@ TEST(ei_typescript_type_import) { /* Go: simple same-module cross-package import. */ TEST(ei_go_same_module_import) { static const EILangFile f[] = { - {"go.mod", "module example.com/demo\n\ngo 1.21\n"}, + {"go.mod", "module example.com/demo\n\ngo 1.21\n"}, {"util/util.go", "package util\n\nfunc Helper(x int) int { return x + 1 }\n"}, - {"main.go", "package main\n\nimport \"example.com/demo/util\"\n\n" - "func main() { _ = util.Helper(1) }\n"}}; + {"main.go", "package main\n\nimport \"example.com/demo/util\"\n\n" + "func main() { _ = util.Helper(1) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -427,15 +444,15 @@ TEST(ei_go_same_module_import) { /* Go: grouped import block `import ( "pkg1"; "pkg2" )`. */ TEST(ei_go_grouped_import_block) { static const EILangFile f[] = { - {"go.mod", "module example.com/grp\n\ngo 1.21\n"}, + {"go.mod", "module example.com/grp\n\ngo 1.21\n"}, {"math/math.go", "package math\n\nfunc Add(a, b int) int { return a + b }\n"}, - {"strutil/str.go","package strutil\n\nfunc Join(a, b string) string { return a + b }\n"}, - {"main.go", "package main\n\nimport (\n" - "\t\"example.com/grp/math\"\n" - "\t\"example.com/grp/strutil\"\n)\n\n" - "func main() {\n" - "\t_ = math.Add(1, 2)\n" - "\t_ = strutil.Join(\"a\", \"b\")\n}\n"}}; + {"strutil/str.go", "package strutil\n\nfunc Join(a, b string) string { return a + b }\n"}, + {"main.go", "package main\n\nimport (\n" + "\t\"example.com/grp/math\"\n" + "\t\"example.com/grp/strutil\"\n)\n\n" + "func main() {\n" + "\t_ = math.Add(1, 2)\n" + "\t_ = strutil.Join(\"a\", \"b\")\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 4, "IMPORTS", 1)); PASS(); } @@ -443,10 +460,10 @@ TEST(ei_go_grouped_import_block) { /* Go: aliased import `import util "example.com/demo/util"`. */ TEST(ei_go_aliased_import) { static const EILangFile f[] = { - {"go.mod", "module example.com/alias\n\ngo 1.21\n"}, + {"go.mod", "module example.com/alias\n\ngo 1.21\n"}, {"util/util.go", "package util\n\nfunc Helper(x int) int { return x + 1 }\n"}, - {"main.go", "package main\n\nimport u \"example.com/alias/util\"\n\n" - "func main() { _ = u.Helper(1) }\n"}}; + {"main.go", "package main\n\nimport u \"example.com/alias/util\"\n\n" + "func main() { _ = u.Helper(1) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -454,10 +471,10 @@ TEST(ei_go_aliased_import) { /* Go: dot import `import . "example.com/demo/util"` (members into current ns). */ TEST(ei_go_dot_import) { static const EILangFile f[] = { - {"go.mod", "module example.com/dot\n\ngo 1.21\n"}, + {"go.mod", "module example.com/dot\n\ngo 1.21\n"}, {"util/util.go", "package util\n\nfunc Helper(x int) int { return x + 1 }\n"}, - {"main.go", "package main\n\nimport . \"example.com/dot/util\"\n\n" - "func main() { _ = Helper(1) }\n"}}; + {"main.go", "package main\n\nimport . \"example.com/dot/util\"\n\n" + "func main() { _ = Helper(1) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -465,10 +482,10 @@ TEST(ei_go_dot_import) { /* Go: sub-package path import (multi-level directory). */ TEST(ei_go_subpackage_import) { static const EILangFile f[] = { - {"go.mod", "module example.com/sub\n\ngo 1.21\n"}, - {"pkg/math/ops.go", "package math\n\nfunc Mul(a, b int) int { return a * b }\n"}, - {"main.go", "package main\n\nimport \"example.com/sub/pkg/math\"\n\n" - "func main() { _ = math.Mul(2, 3) }\n"}}; + {"go.mod", "module example.com/sub\n\ngo 1.21\n"}, + {"pkg/math/ops.go", "package math\n\nfunc Mul(a, b int) int { return a * b }\n"}, + {"main.go", "package main\n\nimport \"example.com/sub/pkg/math\"\n\n" + "func main() { _ = math.Mul(2, 3) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -476,9 +493,9 @@ TEST(ei_go_subpackage_import) { /* Go: blank import `import _ "example.com/demo/util"` (side-effects only). */ TEST(ei_go_blank_import) { static const EILangFile f[] = { - {"go.mod", "module example.com/blank\n\ngo 1.21\n"}, + {"go.mod", "module example.com/blank\n\ngo 1.21\n"}, {"util/util.go", "package util\n\nfunc init() {}\n"}, - {"main.go", "package main\n\nimport _ \"example.com/blank/util\"\n\nfunc main() {}\n"}}; + {"main.go", "package main\n\nimport _ \"example.com/blank/util\"\n\nfunc main() {}\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -486,12 +503,12 @@ TEST(ei_go_blank_import) { /* Go: two files importing the same internal package (both should yield edges). */ TEST(ei_go_two_consumers_same_package) { static const EILangFile f[] = { - {"go.mod", "module example.com/two\n\ngo 1.21\n"}, + {"go.mod", "module example.com/two\n\ngo 1.21\n"}, {"util/util.go", "package util\n\nfunc Helper(x int) int { return x + 1 }\n"}, - {"a/a.go", "package a\n\nimport \"example.com/two/util\"\n\n" - "func Run() int { return util.Helper(1) }\n"}, - {"b/b.go", "package b\n\nimport \"example.com/two/util\"\n\n" - "func Run() int { return util.Helper(2) }\n"}}; + {"a/a.go", "package a\n\nimport \"example.com/two/util\"\n\n" + "func Run() int { return util.Helper(1) }\n"}, + {"b/b.go", "package b\n\nimport \"example.com/two/util\"\n\n" + "func Run() int { return util.Helper(2) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 4, "IMPORTS", 2)); PASS(); } @@ -500,22 +517,21 @@ TEST(ei_go_two_consumers_same_package) { * source node. Also exercises angle-bracket include resolution. */ TEST(ei_cpp_header_include_targets_header_file) { static const EILangFixtureFile fixture_files[] = { - {"main.cpp"}, - {"NodeController.h"}, - {"NodeController.cpp"}, - {"SystemController.h"}, - {"SystemController.cpp"}, + {"main.cpp"}, {"NodeController.h"}, {"NodeController.cpp"}, + {"SystemController.h"}, {"SystemController.cpp"}, }; EILangProj lp; - cbm_store_t *store = ei_index_fixture_files(&lp, "tests/fixtures/cpp_include", - fixture_files, - (int)(sizeof(fixture_files) / sizeof(fixture_files[0]))); + cbm_store_t *store = + ei_index_fixture_files(&lp, "tests/fixtures/cpp_include", fixture_files, + (int)(sizeof(fixture_files) / sizeof(fixture_files[0]))); ASSERT_NOT_NULL(store); int64_t main_id = ei_node_id_for_file_label(store, lp.project, "main.cpp", "File"); - int64_t node_source_id = ei_node_id_for_file_label(store, lp.project, "NodeController.cpp", "File"); - int64_t system_source_id = ei_node_id_for_file_label(store, lp.project, "SystemController.cpp", "File"); + int64_t node_source_id = + ei_node_id_for_file_label(store, lp.project, "NodeController.cpp", "File"); + int64_t system_source_id = + ei_node_id_for_file_label(store, lp.project, "SystemController.cpp", "File"); ASSERT_GT(main_id, 0); ASSERT_GT(node_source_id, 0); @@ -531,13 +547,13 @@ TEST(ei_cpp_header_include_targets_header_file) { bool saw_system_header = false; for (int i = 0; i < edge_count; i++) { cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t)); - + /* Pass target directly (no &) because it is already a pointer */ ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK); ASSERT_EQ(edges[i].source_id, main_id); ASSERT_NEQ(edges[i].target_id, node_source_id); ASSERT_NEQ(edges[i].target_id, system_source_id); - + /* Use -> instead of . to access fields on a pointer */ if (target->file_path && strcmp(target->file_path, "NodeController.h") == 0) { saw_node_header = true; @@ -545,7 +561,7 @@ TEST(ei_cpp_header_include_targets_header_file) { if (target->file_path && strcmp(target->file_path, "SystemController.h") == 0) { saw_system_header = true; } - + /* Free the node inside the loop */ cbm_store_free_nodes(target, 1); } @@ -569,7 +585,7 @@ TEST(ei_cpp_header_include_targets_header_file) { /* Rust: `mod a;` file inclusion + `use crate::a::f` cross-module use. */ TEST(ei_rust_mod_plus_use) { static const EILangFile f[] = { - {"a.rs", "pub fn f(x: i32) -> i32 { x + 1 }\n"}, + {"a.rs", "pub fn f(x: i32) -> i32 { x + 1 }\n"}, {"main.rs", "mod a;\nuse crate::a::f;\n\nfn main() { let _ = f(1); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); @@ -586,11 +602,10 @@ TEST(ei_rust_use_crate_path) { /* Rust: grouped use `use crate::ops::{add, mul}`. */ TEST(ei_rust_grouped_use) { - static const EILangFile f[] = { - {"ops.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }\n" - "pub fn mul(a: i32, b: i32) -> i32 { a * b }\n"}, - {"main.rs", "mod ops;\nuse crate::ops::{add, mul};\n\n" - "fn main() { let _ = add(mul(2, 3), 1); }\n"}}; + static const EILangFile f[] = {{"ops.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }\n" + "pub fn mul(a: i32, b: i32) -> i32 { a * b }\n"}, + {"main.rs", "mod ops;\nuse crate::ops::{add, mul};\n\n" + "fn main() { let _ = add(mul(2, 3), 1); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -606,19 +621,17 @@ TEST(ei_rust_aliased_use) { /* Rust: pub re-export `pub use crate::util::helper`. */ TEST(ei_rust_pub_re_export) { - static const EILangFile f[] = { - {"util.rs", "pub fn helper(x: i32) -> i32 { x + 1 }\n"}, - {"lib.rs", "mod util;\npub use crate::util::helper;\n"}}; + static const EILangFile f[] = {{"util.rs", "pub fn helper(x: i32) -> i32 { x + 1 }\n"}, + {"lib.rs", "mod util;\npub use crate::util::helper;\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } /* Rust: struct import `use crate::models::Config`. */ TEST(ei_rust_struct_use) { - static const EILangFile f[] = { - {"models.rs", "pub struct Config { pub value: i32 }\n"}, - {"main.rs", "mod models;\nuse crate::models::Config;\n\n" - "fn make() -> Config { Config { value: 1 } }\n"}}; + static const EILangFile f[] = {{"models.rs", "pub struct Config { pub value: i32 }\n"}, + {"main.rs", "mod models;\nuse crate::models::Config;\n\n" + "fn make() -> Config { Config { value: 1 } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -626,8 +639,8 @@ TEST(ei_rust_struct_use) { /* Rust: glob use `use crate::ops::*`. */ TEST(ei_rust_glob_use) { static const EILangFile f[] = { - {"ops.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }\n" - "pub fn sub(a: i32, b: i32) -> i32 { a - b }\n"}, + {"ops.rs", "pub fn add(a: i32, b: i32) -> i32 { a + b }\n" + "pub fn sub(a: i32, b: i32) -> i32 { a - b }\n"}, {"main.rs", "mod ops;\nuse crate::ops::*;\n\nfn run() -> i32 { add(sub(5, 1), 2) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); @@ -637,8 +650,8 @@ TEST(ei_rust_glob_use) { TEST(ei_rust_trait_use) { static const EILangFile f[] = { {"traits.rs", "pub trait Compute { fn run(&self) -> i32; }\n"}, - {"main.rs", "mod traits;\nuse crate::traits::Compute;\n\n" - "struct Impl;\nimpl Compute for Impl { fn run(&self) -> i32 { 42 } }\n"}}; + {"main.rs", "mod traits;\nuse crate::traits::Compute;\n\n" + "struct Impl;\nimpl Compute for Impl { fn run(&self) -> i32 { 42 } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -653,9 +666,9 @@ TEST(ei_rust_trait_use) { /* Kotlin: `import com.example.Util` — basic cross-file class import. */ TEST(ei_kotlin_basic_class_import) { static const EILangFile f[] = { - {"Util.kt", "package com.example\n\nclass Util {\n fun greet() = \"hello\"\n}\n"}, - {"Main.kt", "package com.example\n\nimport com.example.Util\n\n" - "fun main() { val u = Util(); println(u.greet()) }\n"}}; + {"Util.kt", "package com.example\n\nclass Util {\n fun greet() = \"hello\"\n}\n"}, + {"Main.kt", "package com.example\n\nimport com.example.Util\n\n" + "fun main() { val u = Util(); println(u.greet()) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -663,7 +676,7 @@ TEST(ei_kotlin_basic_class_import) { /* Kotlin: `import com.example.fn` — top-level function import. */ TEST(ei_kotlin_toplevel_function_import) { static const EILangFile f[] = { - {"ops.kt", "package com.example\n\nfun add(a: Int, b: Int): Int = a + b\n"}, + {"ops.kt", "package com.example\n\nfun add(a: Int, b: Int): Int = a + b\n"}, {"main.kt", "package com.example\n\nimport com.example.add\n\n" "fun run(): Int = add(1, 2)\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); @@ -673,31 +686,30 @@ TEST(ei_kotlin_toplevel_function_import) { /* Kotlin: aliased import `import com.example.Util as U`. */ TEST(ei_kotlin_aliased_import) { static const EILangFile f[] = { - {"Util.kt", "package com.example\n\nclass Util {\n fun compute(x: Int) = x + 1\n}\n"}, - {"Main.kt", "package com.example\n\nimport com.example.Util as U\n\n" - "fun run(): Int { val u = U(); return u.compute(5) }\n"}}; + {"Util.kt", "package com.example\n\nclass Util {\n fun compute(x: Int) = x + 1\n}\n"}, + {"Main.kt", "package com.example\n\nimport com.example.Util as U\n\n" + "fun run(): Int { val u = U(); return u.compute(5) }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } /* Kotlin: wildcard import `import com.example.*`. */ TEST(ei_kotlin_wildcard_import) { - static const EILangFile f[] = { - {"ops.kt", "package com.example\n\nfun add(a: Int, b: Int) = a + b\n" - "fun mul(a: Int, b: Int) = a * b\n"}, - {"main.kt", "package com.example\n\nimport com.example.*\n\n" - "fun run() = add(1, mul(2, 3))\n"}}; + static const EILangFile f[] = {{"ops.kt", + "package com.example\n\nfun add(a: Int, b: Int) = a + b\n" + "fun mul(a: Int, b: Int) = a * b\n"}, + {"main.kt", "package com.example\n\nimport com.example.*\n\n" + "fun run() = add(1, mul(2, 3))\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } /* Kotlin: multiple imports in one file. */ TEST(ei_kotlin_multiple_imports) { - static const EILangFile f[] = { - {"A.kt", "package com.x\n\nclass A { fun a() = 1 }\n"}, - {"B.kt", "package com.x\n\nclass B { fun b() = 2 }\n"}, - {"Main.kt", "package com.x\n\nimport com.x.A\nimport com.x.B\n\n" - "fun run(): Int { return A().a() + B().b() }\n"}}; + static const EILangFile f[] = {{"A.kt", "package com.x\n\nclass A { fun a() = 1 }\n"}, + {"B.kt", "package com.x\n\nclass B { fun b() = 2 }\n"}, + {"Main.kt", "package com.x\n\nimport com.x.A\nimport com.x.B\n\n" + "fun run(): Int { return A().a() + B().b() }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -706,8 +718,8 @@ TEST(ei_kotlin_multiple_imports) { TEST(ei_kotlin_object_member_import) { static const EILangFile f[] = { {"Config.kt", "package com.example\n\nobject Config {\n const val DEFAULT = 42\n}\n"}, - {"Main.kt", "package com.example\n\nimport com.example.Config.DEFAULT\n\n" - "fun run() = DEFAULT\n"}}; + {"Main.kt", "package com.example\n\nimport com.example.Config.DEFAULT\n\n" + "fun run() = DEFAULT\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -715,7 +727,7 @@ TEST(ei_kotlin_object_member_import) { /* Kotlin: data class import across packages. */ TEST(ei_kotlin_data_class_import) { static const EILangFile f[] = { - {"model/User.kt", "package com.example.model\n\ndata class User(val name: String)\n"}, + {"model/User.kt", "package com.example.model\n\ndata class User(val name: String)\n"}, {"service/Svc.kt", "package com.example.service\n\nimport com.example.model.User\n\n" "fun greet(u: User) = \"Hello \" + u.name\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); @@ -732,13 +744,13 @@ TEST(ei_kotlin_data_class_import) { /* Java: `import com.example.Util` — basic class import. */ TEST(ei_java_basic_class_import) { static const EILangFile f[] = { - {"Util.java", "package com.example;\npublic class Util {\n" - " public int compute(int x) { return x + 1; }\n}\n"}, - {"Main.java", "package com.example;\nimport com.example.Util;\n" - "public class Main {\n" - " public static void main(String[] args) {\n" - " Util u = new Util();\n System.out.println(u.compute(1));\n" - " }\n}\n"}}; + {"Util.java", "package com.example;\npublic class Util {\n" + " public int compute(int x) { return x + 1; }\n}\n"}, + {"Main.java", "package com.example;\nimport com.example.Util;\n" + "public class Main {\n" + " public static void main(String[] args) {\n" + " Util u = new Util();\n System.out.println(u.compute(1));\n" + " }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -749,9 +761,9 @@ TEST(ei_java_subpackage_import) { {"util/MathOps.java", "package com.example.util;\n" "public class MathOps {\n" " public static int add(int a, int b) { return a + b; }\n}\n"}, - {"Main.java", "package com.example;\nimport com.example.util.MathOps;\n" - "public class Main {\n" - " void run() { int x = MathOps.add(1, 2); }\n}\n"}}; + {"Main.java", "package com.example;\nimport com.example.util.MathOps;\n" + "public class Main {\n" + " void run() { int x = MathOps.add(1, 2); }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -759,10 +771,10 @@ TEST(ei_java_subpackage_import) { /* Java: wildcard import `import com.example.util.*`. */ TEST(ei_java_wildcard_import) { static const EILangFile f[] = { - {"util/Ops.java", "package com.example.util;\n" - "public class Ops { public static int add(int a,int b){return a+b;} }\n"}, - {"Main.java", "package com.example;\nimport com.example.util.*;\n" - "public class Main { void run() { int x = Ops.add(1, 2); } }\n"}}; + {"util/Ops.java", "package com.example.util;\n" + "public class Ops { public static int add(int a,int b){return a+b;} }\n"}, + {"Main.java", "package com.example;\nimport com.example.util.*;\n" + "public class Main { void run() { int x = Ops.add(1, 2); } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -770,10 +782,11 @@ TEST(ei_java_wildcard_import) { /* Java: static import `import static com.example.MathOps.add`. */ TEST(ei_java_static_import) { static const EILangFile f[] = { - {"MathOps.java", "package com.example;\n" - "public class MathOps { public static int add(int a,int b){return a+b;} }\n"}, - {"Main.java", "package com.example;\nimport static com.example.MathOps.add;\n" - "public class Main { void run() { int x = add(1, 2); } }\n"}}; + {"MathOps.java", + "package com.example;\n" + "public class MathOps { public static int add(int a,int b){return a+b;} }\n"}, + {"Main.java", "package com.example;\nimport static com.example.MathOps.add;\n" + "public class Main { void run() { int x = add(1, 2); } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -781,8 +794,8 @@ TEST(ei_java_static_import) { /* Java: multiple imports in one file. */ TEST(ei_java_multiple_imports) { static const EILangFile f[] = { - {"A.java", "package com.x;\npublic class A { public int a() { return 1; } }\n"}, - {"B.java", "package com.x;\npublic class B { public int b() { return 2; } }\n"}, + {"A.java", "package com.x;\npublic class A { public int a() { return 1; } }\n"}, + {"B.java", "package com.x;\npublic class B { public int b() { return 2; } }\n"}, {"Main.java", "package com.x;\nimport com.x.A;\nimport com.x.B;\n" "public class Main { void run() { new A().a(); new B().b(); } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); @@ -793,9 +806,9 @@ TEST(ei_java_multiple_imports) { TEST(ei_java_interface_import) { static const EILangFile f[] = { {"Compute.java", "package com.example;\npublic interface Compute { int run(int x); }\n"}, - {"Impl.java", "package com.example;\nimport com.example.Compute;\n" - "public class Impl implements Compute {\n" - " public int run(int x) { return x + 1; }\n}\n"}}; + {"Impl.java", "package com.example;\nimport com.example.Compute;\n" + "public class Impl implements Compute {\n" + " public int run(int x) { return x + 1; }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -805,8 +818,8 @@ TEST(ei_java_static_wildcard_import) { static const EILangFile f[] = { {"Constants.java", "package com.example;\n" "public class Constants { public static final int MAX = 100; }\n"}, - {"Main.java", "package com.example;\nimport static com.example.Constants.*;\n" - "public class Main { void check() { int x = MAX; } }\n"}}; + {"Main.java", "package com.example;\nimport static com.example.Constants.*;\n" + "public class Main { void check() { int x = MAX; } }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -824,9 +837,9 @@ TEST(ei_csharp_basic_using) { {"Utils.cs", "namespace App.Utils {\n" " public class Helper {\n" " public int Compute(int x) { return x + 1; }\n }\n}\n"}, - {"Main.cs", "using App.Utils;\nnamespace App {\n" - " class Main {\n" - " void Run() { var h = new Helper(); _ = h.Compute(1); }\n }\n}\n"}}; + {"Main.cs", "using App.Utils;\nnamespace App {\n" + " class Main {\n" + " void Run() { var h = new Helper(); _ = h.Compute(1); }\n }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -834,10 +847,11 @@ TEST(ei_csharp_basic_using) { /* C#: aliased using `using H = App.Utils.Helper`. */ TEST(ei_csharp_aliased_using) { static const EILangFile f[] = { - {"Utils.cs", "namespace App.Utils {\n" - " public class Helper { public int Compute(int x) { return x + 1; } }\n}\n"}, - {"Main.cs", "using H = App.Utils.Helper;\nnamespace App {\n" - " class Main { void Run() { var h = new H(); } }\n}\n"}}; + {"Utils.cs", + "namespace App.Utils {\n" + " public class Helper { public int Compute(int x) { return x + 1; } }\n}\n"}, + {"Main.cs", "using H = App.Utils.Helper;\nnamespace App {\n" + " class Main { void Run() { var h = new H(); } }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -848,8 +862,8 @@ TEST(ei_csharp_using_static) { {"MathOps.cs", "namespace App {\n" " public static class MathOps {\n" " public static int Add(int a, int b) { return a + b; }\n }\n}\n"}, - {"Main.cs", "using static App.MathOps;\nnamespace App {\n" - " class Main { void Run() { int x = Add(1, 2); } }\n}\n"}}; + {"Main.cs", "using static App.MathOps;\nnamespace App {\n" + " class Main { void Run() { int x = Add(1, 2); } }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -857,8 +871,8 @@ TEST(ei_csharp_using_static) { /* C#: multiple using directives in one file. */ TEST(ei_csharp_multiple_usings) { static const EILangFile f[] = { - {"A.cs", "namespace Com.X { public class A { public int a() { return 1; } } }\n"}, - {"B.cs", "namespace Com.X { public class B { public int b() { return 2; } } }\n"}, + {"A.cs", "namespace Com.X { public class A { public int a() { return 1; } } }\n"}, + {"B.cs", "namespace Com.X { public class B { public int b() { return 2; } } }\n"}, {"Main.cs", "using Com.X;\nnamespace Com.X {\n" " class Main { void Run() { new A().a(); new B().b(); } }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); @@ -870,9 +884,9 @@ TEST(ei_csharp_interface_using) { static const EILangFile f[] = { {"Interfaces.cs", "namespace App.Contracts {\n" " public interface ICompute { int Run(int x); }\n}\n"}, - {"Impl.cs", "using App.Contracts;\nnamespace App {\n" - " public class Impl : ICompute {\n" - " public int Run(int x) { return x + 1; }\n }\n}\n"}}; + {"Impl.cs", "using App.Contracts;\nnamespace App {\n" + " public class Impl : ICompute {\n" + " public int Run(int x) { return x + 1; }\n }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -880,12 +894,12 @@ TEST(ei_csharp_interface_using) { /* C#: sub-namespace import `using App.Models.Domain`. */ TEST(ei_csharp_subnamespace_using) { static const EILangFile f[] = { - {"models/User.cs", "namespace App.Models.Domain {\n" - " public class User { public string Name { get; set; } }\n}\n"}, - {"service/Svc.cs", "using App.Models.Domain;\nnamespace App.Service {\n" - " public class UserService {\n" - " public string Greet(User u) { return \"Hello \" + u.Name; }\n" - " }\n}\n"}}; + {"models/User.cs", "namespace App.Models.Domain {\n" + " public class User { public string Name { get; set; } }\n}\n"}, + {"service/Svc.cs", "using App.Models.Domain;\nnamespace App.Service {\n" + " public class UserService {\n" + " public string Greet(User u) { return \"Hello \" + u.Name; }\n" + " }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -893,8 +907,8 @@ TEST(ei_csharp_subnamespace_using) { /* C#: file-scoped namespace + using (C# 10 style). */ TEST(ei_csharp_file_scoped_namespace) { static const EILangFile f[] = { - {"Ops.cs", "namespace App.Ops;\npublic static class Ops {\n" - " public static int Add(int a, int b) => a + b;\n}\n"}, + {"Ops.cs", "namespace App.Ops;\npublic static class Ops {\n" + " public static int Add(int a, int b) => a + b;\n}\n"}, {"Main.cs", "using App.Ops;\nnamespace App.Main;\npublic class Main {\n" " public void Run() { int x = Ops.Add(1, 2); }\n}\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); @@ -913,8 +927,8 @@ TEST(ei_php_basic_use) { static const EILangFile f[] = { {"Utils/Helper.php", "compute(1); }\n"}}; + {"main.php", "compute(1); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -924,8 +938,8 @@ TEST(ei_php_aliased_use) { static const EILangFile f[] = { {"Utils/Helper.php", "compute(1); }\n"}}; + {"main.php", "compute(1); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 2, "IMPORTS", 1)); PASS(); } @@ -933,10 +947,13 @@ TEST(ei_php_aliased_use) { /* PHP: grouped use `use App\Utils\{A, B}`. */ TEST(ei_php_grouped_use) { static const EILangFile f[] = { - {"Utils/A.php", "a() + $b->b(); }\n"}}; + {"Utils/A.php", + "a() + $b->b(); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -944,9 +961,10 @@ TEST(ei_php_grouped_use) { /* PHP: `use function App\Utils\compute` — function import. */ TEST(ei_php_function_use) { static const EILangFile f[] = { - {"Utils/funcs.php", "a() + $b->b(); }\n"}}; + {"A.php", "a() + $b->b(); }\n"}}; ASSERT_TRUE(ei_edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -975,11 +994,12 @@ TEST(ei_php_multiple_use_statements) { /* PHP: interface use across files. */ TEST(ei_php_interface_use) { static const EILangFile f[] = { - {"Contracts/Computable.php", "project); lp->project = cbm_project_name_from_path(lp->tmpdir); if (!lp->project) { return NULL; @@ -1198,9 +1202,8 @@ TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767) { " \"@lib\": [\"./src/lib\"],\n" " \"@lib/*\": [\"./src/lib/*\"]\n }\n }\n}\n"}, {"src/lib/thing.ts", "export const Thing = {};\n"}, - {"src/consumer.ts", - "import { ClientC } from '@lib/external-pkg';\n\n" - "export function useClient() {\n return new ClientC();\n}\n"}}; + {"src/consumer.ts", "import { ClientC } from '@lib/external-pkg';\n\n" + "export function useClient() {\n return new ClientC();\n}\n"}}; cbm_store_t *store = lang_index_files(&lp, f, 3); int got = store ? cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") : -1; if (got != 0) { @@ -1221,9 +1224,8 @@ TEST(contract_edge_imports_alias_resolves_real_file_issue767) { " \"@lib\": [\"./src/lib\"],\n" " \"@lib/*\": [\"./src/lib/*\"]\n }\n }\n}\n"}, {"src/lib/thing.ts", "export const Thing = {};\n"}, - {"src/consumer.ts", - "import { Thing } from '@lib/thing';\n\n" - "export function useThing() {\n return Thing;\n}\n"}}; + {"src/consumer.ts", "import { Thing } from '@lib/thing';\n\n" + "export function useThing() {\n return Thing;\n}\n"}}; ASSERT_TRUE(edge_present(f, 3, "IMPORTS", 1)); PASS(); } @@ -1339,17 +1341,17 @@ TEST(contract_edge_no_infra_routes_from_ci_configs_issue999) { TEST(contract_edge_infra_routes_from_deploy_configs_still_minted) { LangProj lp; static const LangFile f[] = { - {"scheduler.yaml", - "jobs:\n" - " - name: nightly-sync\n" - " schedule: \"0 3 * * *\"\n" - " push_endpoint: https://sync.internal.example/api/v1/sync\n"}}; + {"scheduler.yaml", "jobs:\n" + " - name: nightly-sync\n" + " schedule: \"0 3 * * *\"\n" + " push_endpoint: https://sync.internal.example/api/v1/sync\n"}}; cbm_store_t *store = lang_index_files(&lp, f, 1); ASSERT_NOT_NULL(store); int routes = count_infra_routes_matching(store, lp.project, "sync.internal.example"); if (routes < 1) { - fprintf(stderr, " [999] FAIL deploy-config endpoint minted %d infra routes " - "(expected >=1)\n", + fprintf(stderr, + " [999] FAIL deploy-config endpoint minted %d infra routes " + "(expected >=1)\n", routes); } ASSERT_TRUE(routes >= 1); @@ -1392,26 +1394,25 @@ static int calls_edge_targets(cbm_store_t *store, const char *project, const cha * (all resolve on main today and must keep resolving). */ TEST(contract_edge_python_aliased_import_call_resolves_issue988) { LangProj lp; - static const LangFile f[] = { - {"m.py", "def f(x):\n return x + 1\n"}, - {"pkg/__init__.py", ""}, - {"pkg/dm.py", "def h(x):\n return x\n"}, - {"caller_alias.py", "from m import f as g\n" - "\n" - "def use_alias(x):\n" - " return g(x)\n"}, - {"caller_plain.py", "from m import f\n" - "\n" - "def use_plain(x):\n" - " return f(x)\n"}, - {"caller_modalias.py", "import m as mm\n" - "\n" - "def use_modalias(x):\n" - " return mm.f(x)\n"}, - {"caller_dotalias.py", "import pkg.dm as dz\n" - "\n" - "def use_dotalias(x):\n" - " return dz.h(x)\n"}}; + static const LangFile f[] = {{"m.py", "def f(x):\n return x + 1\n"}, + {"pkg/__init__.py", ""}, + {"pkg/dm.py", "def h(x):\n return x\n"}, + {"caller_alias.py", "from m import f as g\n" + "\n" + "def use_alias(x):\n" + " return g(x)\n"}, + {"caller_plain.py", "from m import f\n" + "\n" + "def use_plain(x):\n" + " return f(x)\n"}, + {"caller_modalias.py", "import m as mm\n" + "\n" + "def use_modalias(x):\n" + " return mm.f(x)\n"}, + {"caller_dotalias.py", "import pkg.dm as dz\n" + "\n" + "def use_dotalias(x):\n" + " return dz.h(x)\n"}}; cbm_store_t *store = lang_index_files(&lp, f, 7); ASSERT_TRUE(store != NULL); int alias = calls_edge_between(store, lp.project, ".use_alias", ".m.f"); @@ -1458,8 +1459,9 @@ TEST(contract_edge_commonjs_require_call_resolves_issue871) { * not a definition). */ int shadowed = calls_edge_targets(store, lp.project, "Variable", ".mutations.doThing.doThing"); if (!resolved || shadowed) { - fprintf(stderr, " [871] FAIL resolved=%d shadowed=%d (require binding must resolve to " - "the exported Function, not the local alias Variable)\n", + fprintf(stderr, + " [871] FAIL resolved=%d shadowed=%d (require binding must resolve to " + "the exported Function, not the local alias Variable)\n", resolved, shadowed); } ASSERT_TRUE(resolved); diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index 3345e1ca7..e45043905 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -111,39 +111,49 @@ typedef struct { } LRP_Proj; typedef struct { - const char *name; /* relative filename, may include '/' for subdirs */ + const char *name; /* relative filename, may include '/' for subdirs */ const char *content; } LRP_File; static void lrp_to_fwd_slashes(char *p) { for (; *p; p++) { - if (*p == '\\') *p = '/'; + if (*p == '\\') + *p = '/'; } } static cbm_store_t *lrp_open_indexed(LRP_Proj *lp) { + /* Freed before reassigning: a fixture that indexes more than once would + * otherwise drop the previous heap name on the floor. Teardown frees the + * last one. */ + free(lp->project); lp->project = cbm_project_name_from_path(lp->tmpdir); - if (!lp->project) return NULL; + if (!lp->project) + return NULL; const char *home = getenv("HOME"); - if (!home) home = "/tmp"; + if (!home) + home = "/tmp"; char cache_dir[512]; snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home); cbm_mkdir(cache_dir); snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project); unlink(lp->dbpath); lp->srv = cbm_mcp_server_new(NULL); - if (!lp->srv) return NULL; + if (!lp->srv) + return NULL; char args[700]; snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir); char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args); - if (resp) free(resp); + if (resp) + free(resp); return cbm_store_open_path(lp->dbpath); } static cbm_store_t *lrp_index(LRP_Proj *lp, const LRP_File *files, int nfiles) { memset(lp, 0, sizeof(*lp)); snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_lrp_XXXXXX"); - if (!cbm_mkdtemp(lp->tmpdir)) return NULL; + if (!cbm_mkdtemp(lp->tmpdir)) + return NULL; lrp_to_fwd_slashes(lp->tmpdir); for (int i = 0; i < nfiles; i++) { char path[700]; @@ -155,7 +165,8 @@ static cbm_store_t *lrp_index(LRP_Proj *lp, const LRP_File *files, int nfiles) { *slash = '/'; } FILE *f = fopen(path, "wb"); - if (!f) return NULL; + if (!f) + return NULL; fputs(files[i].content, f); fclose(f); } @@ -163,15 +174,21 @@ static cbm_store_t *lrp_index(LRP_Proj *lp, const LRP_File *files, int nfiles) { } static void lrp_cleanup(LRP_Proj *lp, cbm_store_t *store) { - if (store) cbm_store_close(store); - if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } - free(lp->project); lp->project = NULL; + if (store) + cbm_store_close(store); + if (lp->srv) { + cbm_mcp_server_free(lp->srv); + lp->srv = NULL; + } + free(lp->project); + lp->project = NULL; th_rmtree(lp->tmpdir); unlink(lp->dbpath); char wal[600], shm[600]; snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath); snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); - unlink(wal); unlink(shm); + unlink(wal); + unlink(shm); } /* Returns USAGE edge count (-1 on DB failure). */ @@ -189,7 +206,10 @@ static const char *LRP_ALL_EDGE_TYPES[] = { "OVERRIDE", "TESTS", "TESTS_FILE", "DATA_FLOWS", NULL}; static void lrp_diag(cbm_store_t *store, const char *project, const char *scenario) { - if (!store) { fprintf(stderr, " [LRP] %s: no graph DB\n", scenario); return; } + if (!store) { + fprintf(stderr, " [LRP] %s: no graph DB\n", scenario); + return; + } char line[512] = {0}; for (int i = 0; LRP_ALL_EDGE_TYPES[i]; i++) { int c = cbm_store_count_edges_by_type(store, project, LRP_ALL_EDGE_TYPES[i]); @@ -206,19 +226,21 @@ static void lrp_diag(cbm_store_t *store, const char *project, const char *scenar * expect_green=true: ASSERT_TRUE (green guard). * expect_green=false: still asserts CALLS>=1 (the correct outcome), so RED * until the bug is fixed — same pattern as test_edge_structural.c. */ -static int lrp_assert_calls(const LRP_File *files, int nfiles, int min_calls, - const char *scenario, int expect_green) { +static int lrp_assert_calls(const LRP_File *files, int nfiles, int min_calls, const char *scenario, + int expect_green) { LRP_Proj lp; cbm_store_t *store = lrp_index(&lp, files, nfiles); int got = store ? cbm_store_count_edges_by_type(store, lp.project, "CALLS") : -1; if (got < min_calls) { - fprintf(stderr, " [LRP] %s FAIL calls=%d expected>=%d %s\n", - scenario, got, min_calls, expect_green ? "(GREEN regression)" : "(RED reproduction)"); + fprintf(stderr, " [LRP] %s FAIL calls=%d expected>=%d %s\n", scenario, got, min_calls, + expect_green ? "(GREEN regression)" : "(RED reproduction)"); lrp_diag(store, lp.project, scenario); } else if (!expect_green) { /* Unexpectedly passing — the lsp_cross wiring may have been added. */ - fprintf(stderr, " [LRP] %s UNEXPECTED PASS calls=%d " - "(lsp_cross may now be wired — promote to GREEN)\n", scenario, got); + fprintf(stderr, + " [LRP] %s UNEXPECTED PASS calls=%d " + "(lsp_cross may now be wired — promote to GREEN)\n", + scenario, got); } lrp_cleanup(&lp, store); return got >= min_calls; @@ -333,14 +355,11 @@ TEST(lrp_go_s2_method_dispatch) { /* Counter is defined in counter.go; Run() in runner.go gets a Counter and * calls its Inc() method. The lsp_cross pass sees Counter's type and * resolves the method call to Counter.Inc. */ - static const LRP_File f[] = { - {"counter.go", - "package app\n\ntype Counter struct{ n int }\n\n" - "func (c *Counter) Inc() { c.n++ }\n\n" - "func (c *Counter) Value() int { return c.n }\n"}, - {"runner.go", - "package app\n\nfunc Run(c *Counter) int {\n" - " c.Inc()\n return c.Value()\n}\n"}}; + static const LRP_File f[] = {{"counter.go", "package app\n\ntype Counter struct{ n int }\n\n" + "func (c *Counter) Inc() { c.n++ }\n\n" + "func (c *Counter) Value() int { return c.n }\n"}, + {"runner.go", "package app\n\nfunc Run(c *Counter) int {\n" + " c.Inc()\n return c.Value()\n}\n"}}; /* GREEN: Go lsp_cross resolves *Counter receiver → Inc and Value methods. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S2/method_dispatch", 1)); PASS(); @@ -355,8 +374,7 @@ TEST(lrp_go_s3_constructor) { {"counter.go", "package app\n\ntype Counter struct{ start int }\n\n" "func NewCounter(start int) *Counter {\n return &Counter{start: start}\n}\n"}, - {"main.go", - "package app\n\nfunc Make(n int) *Counter {\n return NewCounter(n)\n}\n"}}; + {"main.go", "package app\n\nfunc Make(n int) *Counter {\n return NewCounter(n)\n}\n"}}; /* GREEN: NewCounter is a top-level function; generic + lsp_cross both see it. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S3/constructor", 1)); PASS(); @@ -368,9 +386,8 @@ TEST(lrp_go_s4_static_call) { * a top-level function via the package alias. */ static const LRP_File f[] = { {"math/ops.go", "package math\n\nfunc Square(x int) int { return x * x }\n"}, - {"main.go", - "package main\n\nimport \"math\"\n\n" - "func Run(n int) int { return math.Square(n) }\n"}}; + {"main.go", "package main\n\nimport \"math\"\n\n" + "func Run(n int) int { return math.Square(n) }\n"}}; /* GREEN: lsp_cross maps import alias "math" → package QN, resolves math.Square. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S4/static_call", 1)); PASS(); @@ -380,15 +397,14 @@ TEST(lrp_go_s4_static_call) { TEST(lrp_go_s5_chained_call) { /* builder.go defines a Builder with two chainable methods; main.go chains them. * The second call (.Build()) is resolved via the return type of .Add() → *Builder. */ - static const LRP_File f[] = { - {"builder.go", - "package app\n\ntype Builder struct{ items []string }\n\n" - "func NewBuilder() *Builder { return &Builder{} }\n\n" - "func (b *Builder) Add(s string) *Builder {\n b.items = append(b.items, s)\n return b\n}\n\n" - "func (b *Builder) Build() []string { return b.items }\n"}, - {"main.go", - "package app\n\nfunc Make() []string {\n" - " return NewBuilder().Add(\"x\").Build()\n}\n"}}; + static const LRP_File f[] = {{"builder.go", + "package app\n\ntype Builder struct{ items []string }\n\n" + "func NewBuilder() *Builder { return &Builder{} }\n\n" + "func (b *Builder) Add(s string) *Builder {\n b.items = " + "append(b.items, s)\n return b\n}\n\n" + "func (b *Builder) Build() []string { return b.items }\n"}, + {"main.go", "package app\n\nfunc Make() []string {\n" + " return NewBuilder().Add(\"x\").Build()\n}\n"}}; /* GREEN: lsp_cross infers *Builder from NewBuilder() return type, then * resolves Add() → *Builder, then resolves Build(). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S5/chained_call", 1)); @@ -398,13 +414,11 @@ TEST(lrp_go_s5_chained_call) { /* S6 — Go embedded-struct method call (Go's "inheritance"). */ TEST(lrp_go_s6_inherited_method) { /* Dog embeds Animal. Calling dog.Speak() should resolve to Animal.Speak. */ - static const LRP_File f[] = { - {"animal.go", - "package zoo\n\ntype Animal struct{ Name string }\n\n" - "func (a *Animal) Speak() string { return a.Name }\n"}, - {"dog.go", - "package zoo\n\ntype Dog struct{ Animal }\n\n" - "func Run(d *Dog) string { return d.Speak() }\n"}}; + static const LRP_File f[] = {{"animal.go", + "package zoo\n\ntype Animal struct{ Name string }\n\n" + "func (a *Animal) Speak() string { return a.Name }\n"}, + {"dog.go", "package zoo\n\ntype Dog struct{ Animal }\n\n" + "func Run(d *Dog) string { return d.Speak() }\n"}}; /* GREEN: Go lsp_cross handles embedded-struct field promotion; Speak() is * resolved to Animal.Speak via the embedded Animal field. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S6/inherited_method", 1)); @@ -419,9 +433,8 @@ TEST(lrp_go_s7_generic_call) { "package app\n\nfunc Map[T, U any](xs []T, fn func(T) U) []U {\n" " out := make([]U, len(xs))\n" " for i, x := range xs {\n out[i] = fn(x)\n }\n return out\n}\n"}, - {"main.go", - "package app\n\nfunc Double(xs []int) []int {\n" - " return Map(xs, func(x int) int { return x * 2 })\n}\n"}}; + {"main.go", "package app\n\nfunc Double(xs []int) []int {\n" + " return Map(xs, func(x int) int { return x * 2 })\n}\n"}}; /* GREEN: Map is a top-level generic function; both generic + lsp_cross resolvers * find it by name regardless of the type parameter instantiation. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S7/generic_call", 1)); @@ -432,12 +445,10 @@ TEST(lrp_go_s7_generic_call) { TEST(lrp_go_s8_field_type_hint) { /* Service has a field `repo *Repo`; its methods call repo.Find(). */ static const LRP_File f[] = { - {"repo.go", - "package app\n\ntype Repo struct{}\n\n" - "func (r *Repo) Find(id int) string { return \"\" }\n"}, - {"service.go", - "package app\n\ntype Service struct{ repo *Repo }\n\n" - "func (s *Service) Get(id int) string { return s.repo.Find(id) }\n"}}; + {"repo.go", "package app\n\ntype Repo struct{}\n\n" + "func (r *Repo) Find(id int) string { return \"\" }\n"}, + {"service.go", "package app\n\ntype Service struct{ repo *Repo }\n\n" + "func (s *Service) Get(id int) string { return s.repo.Find(id) }\n"}}; /* GREEN: lsp_cross sees s.repo field of type *Repo, resolves repo.Find. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "go/S8/field_type_hint", 1)); PASS(); @@ -468,10 +479,9 @@ TEST(lrp_c_s2_funcptr_in_struct) { "typedef struct { int (*compute)(int); } Ops;\n\n" "static int double_it(int x) { return x * 2; }\n\n" "Ops make_ops(void) {\n Ops o;\n o.compute = double_it;\n return o;\n}\n"}, - {"main.c", - "typedef struct { int (*compute)(int); } Ops;\n" - "Ops make_ops(void);\n\n" - "int run(int n) {\n Ops o = make_ops();\n return o.compute(n);\n}\n"}}; + {"main.c", "typedef struct { int (*compute)(int); } Ops;\n" + "Ops make_ops(void);\n\n" + "int run(int n) {\n Ops o = make_ops();\n return o.compute(n);\n}\n"}}; /* Uncertain: C lsp_cross may not track function-pointer fields through a * cross-file struct. Assert the CORRECT outcome; RED if unresolved. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S2/funcptr_in_struct", 1)); @@ -480,16 +490,14 @@ TEST(lrp_c_s2_funcptr_in_struct) { /* S3 — C "constructor" pattern (factory function returns pointer). */ TEST(lrp_c_s3_constructor) { - static const LRP_File f[] = { - {"buf.c", - "#include \n" - "typedef struct { int cap; int *data; } Buf;\n\n" - "Buf *buf_new(int cap) {\n Buf *b = malloc(sizeof(Buf));\n" - " b->cap = cap;\n return b;\n}\n"}, - {"main.c", - "typedef struct { int cap; int *data; } Buf;\n" - "Buf *buf_new(int cap);\n\n" - "Buf *make_buf(int n) { return buf_new(n); }\n"}}; + static const LRP_File f[] = {{"buf.c", + "#include \n" + "typedef struct { int cap; int *data; } Buf;\n\n" + "Buf *buf_new(int cap) {\n Buf *b = malloc(sizeof(Buf));\n" + " b->cap = cap;\n return b;\n}\n"}, + {"main.c", "typedef struct { int cap; int *data; } Buf;\n" + "Buf *buf_new(int cap);\n\n" + "Buf *make_buf(int n) { return buf_new(n); }\n"}}; /* GREEN: buf_new is a plain function; both resolvers find it. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S3/constructor", 1)); PASS(); @@ -520,13 +528,11 @@ TEST(lrp_c_s4_static_local) { /* S5 — C chained dereference call (ptr->field->method). */ TEST(lrp_c_s5_chained_deref) { static const LRP_File f[] = { - {"node.c", - "typedef struct Node { int val; struct Node *next; } Node;\n\n" - "int node_val(const Node *n) { return n->val; }\n"}, - {"main.c", - "typedef struct Node { int val; struct Node *next; } Node;\n" - "int node_val(const Node *n);\n\n" - "int run(Node *head) { return node_val(head->next); }\n"}}; + {"node.c", "typedef struct Node { int val; struct Node *next; } Node;\n\n" + "int node_val(const Node *n) { return n->val; }\n"}, + {"main.c", "typedef struct Node { int val; struct Node *next; } Node;\n" + "int node_val(const Node *n);\n\n" + "int run(Node *head) { return node_val(head->next); }\n"}}; /* GREEN: node_val is a plain function; run->node_val resolves regardless. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S5/chained_deref", 1)); PASS(); @@ -535,14 +541,12 @@ TEST(lrp_c_s5_chained_deref) { /* S6 — C "inheritance" pattern: base struct embedded at start. */ TEST(lrp_c_s6_base_method) { static const LRP_File f[] = { - {"shape.c", - "typedef struct { int color; } Shape;\n\n" - "int shape_color(const Shape *s) { return s->color; }\n"}, - {"circle.c", - "typedef struct { int color; } Shape;\n" - "typedef struct { Shape base; float radius; } Circle;\n" - "int shape_color(const Shape *s);\n\n" - "int run(Circle *c) { return shape_color(&c->base); }\n"}}; + {"shape.c", "typedef struct { int color; } Shape;\n\n" + "int shape_color(const Shape *s) { return s->color; }\n"}, + {"circle.c", "typedef struct { int color; } Shape;\n" + "typedef struct { Shape base; float radius; } Circle;\n" + "int shape_color(const Shape *s);\n\n" + "int run(Circle *c) { return shape_color(&c->base); }\n"}}; /* GREEN: shape_color is a plain function; run->shape_color resolves. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S6/base_method", 1)); PASS(); @@ -556,17 +560,14 @@ TEST(lrp_c_s6_base_method) { * cross-file call shape that c/S1 resolves), so a CALLS edge is genuinely * exercised. */ TEST(lrp_c_s7_generic_callback) { - static const LRP_File f[] = { - {"compare.c", - "int int_cmp(const void *a, const void *b) {\n" - " return *(const int*)a - *(const int*)b;\n}\n"}, - {"sort.c", - "typedef int (*CmpFn)(const void *, const void *);\n" - "int int_cmp(const void *a, const void *b);\n\n" - "int my_sort(int *arr, int n) {\n" - " CmpFn fn = int_cmp;\n" - " int direct = int_cmp(&arr[0], &arr[1]);\n" - " return direct + fn(&arr[0], &arr[1]);\n}\n"}}; + static const LRP_File f[] = {{"compare.c", "int int_cmp(const void *a, const void *b) {\n" + " return *(const int*)a - *(const int*)b;\n}\n"}, + {"sort.c", "typedef int (*CmpFn)(const void *, const void *);\n" + "int int_cmp(const void *a, const void *b);\n\n" + "int my_sort(int *arr, int n) {\n" + " CmpFn fn = int_cmp;\n" + " int direct = int_cmp(&arr[0], &arr[1]);\n" + " return direct + fn(&arr[0], &arr[1]);\n}\n"}}; ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S7/generic_callback", 1)); PASS(); } @@ -574,14 +575,12 @@ TEST(lrp_c_s7_generic_callback) { /* S8 — C field-type-hint: accessing a known struct member's address. */ TEST(lrp_c_s8_field_call) { static const LRP_File f[] = { - {"logger.c", - "typedef struct { int level; } Logger;\n\n" - "void log_msg(const Logger *l, const char *msg) { (void)l; (void)msg; }\n"}, - {"service.c", - "typedef struct { int level; } Logger;\n" - "typedef struct { Logger logger; int id; } Service;\n" - "void log_msg(const Logger *l, const char *msg);\n\n" - "void run(Service *svc, const char *m) { log_msg(&svc->logger, m); }\n"}}; + {"logger.c", "typedef struct { int level; } Logger;\n\n" + "void log_msg(const Logger *l, const char *msg) { (void)l; (void)msg; }\n"}, + {"service.c", "typedef struct { int level; } Logger;\n" + "typedef struct { Logger logger; int id; } Service;\n" + "void log_msg(const Logger *l, const char *msg);\n\n" + "void run(Service *svc, const char *m) { log_msg(&svc->logger, m); }\n"}}; /* GREEN: log_msg is a plain function; run->log_msg resolves. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "c/S8/field_call", 1)); PASS(); @@ -606,13 +605,11 @@ TEST(lrp_cpp_s1_crossfile_call) { /* S2 — C++ method dispatch on object (non-virtual). */ TEST(lrp_cpp_s2_method_dispatch) { static const LRP_File f[] = { - {"counter.cpp", - "class Counter {\npublic:\n" - " int n;\n Counter() : n(0) {}\n" - " void inc() { n++; }\n int val() const { return n; }\n};\n"}, - {"main.cpp", - "class Counter;\n\n" - "int run() {\n Counter c;\n c.inc();\n return c.val();\n}\n"}}; + {"counter.cpp", "class Counter {\npublic:\n" + " int n;\n Counter() : n(0) {}\n" + " void inc() { n++; }\n int val() const { return n; }\n};\n"}, + {"main.cpp", "class Counter;\n\n" + "int run() {\n Counter c;\n c.inc();\n return c.val();\n}\n"}}; /* Uncertain: forward-declaring a class without definition may limit lsp_cross * type inference. Assert the correct outcome. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S2/method_dispatch", 1)); @@ -622,13 +619,11 @@ TEST(lrp_cpp_s2_method_dispatch) { /* S3 — C++ constructor call. */ TEST(lrp_cpp_s3_constructor) { static const LRP_File f[] = { - {"widget.cpp", - "class Widget {\npublic:\n int id;\n" - " Widget(int id) : id(id) {}\n" - " int get_id() const { return id; }\n};\n"}, - {"main.cpp", - "class Widget { public: Widget(int); int get_id() const; };\n\n" - "int run(int n) {\n Widget w(n);\n return w.get_id();\n}\n"}}; + {"widget.cpp", "class Widget {\npublic:\n int id;\n" + " Widget(int id) : id(id) {}\n" + " int get_id() const { return id; }\n};\n"}, + {"main.cpp", "class Widget { public: Widget(int); int get_id() const; };\n\n" + "int run(int n) {\n Widget w(n);\n return w.get_id();\n}\n"}}; /* GREEN: Widget(n) constructor call + get_id() method call. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S3/constructor", 1)); PASS(); @@ -636,13 +631,10 @@ TEST(lrp_cpp_s3_constructor) { /* S4 — C++ static method call (Class::method). */ TEST(lrp_cpp_s4_static_method) { - static const LRP_File f[] = { - {"registry.cpp", - "class Registry {\npublic:\n" - " static int count() { return 42; }\n};\n"}, - {"main.cpp", - "class Registry { public: static int count(); };\n\n" - "int run() { return Registry::count(); }\n"}}; + static const LRP_File f[] = {{"registry.cpp", "class Registry {\npublic:\n" + " static int count() { return 42; }\n};\n"}, + {"main.cpp", "class Registry { public: static int count(); };\n\n" + "int run() { return Registry::count(); }\n"}}; /* REAL BUG: a C++ static qualified call `Registry::count()` is not resolved to * a CALLS edge by the C/C++ lsp_cross (cpp_mode) — diagnostics show calls=0 with * the Registry method present (DEFINES_METHOD=1). The `Class::method()` static @@ -655,13 +647,11 @@ TEST(lrp_cpp_s4_static_method) { /* S5 — C++ chained method call. */ TEST(lrp_cpp_s5_chained) { static const LRP_File f[] = { - {"builder.cpp", - "class Builder {\npublic:\n" - " Builder& add(int x) { (void)x; return *this; }\n" - " int build() { return 1; }\n};\n"}, - {"main.cpp", - "class Builder { public: Builder& add(int); int build(); };\n\n" - "int run() { return Builder().add(1).build(); }\n"}}; + {"builder.cpp", "class Builder {\npublic:\n" + " Builder& add(int x) { (void)x; return *this; }\n" + " int build() { return 1; }\n};\n"}, + {"main.cpp", "class Builder { public: Builder& add(int); int build(); };\n\n" + "int run() { return Builder().add(1).build(); }\n"}}; /* GREEN: Builder() + add() + build() chain; lsp_cross resolves return types. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S5/chained", 1)); PASS(); @@ -670,14 +660,12 @@ TEST(lrp_cpp_s5_chained) { /* S6 — C++ virtual inherited method call. */ TEST(lrp_cpp_s6_virtual_inherited) { static const LRP_File f[] = { - {"shape.cpp", - "class Shape {\npublic:\n virtual int area() const { return 0; }\n};\n"}, - {"circle.cpp", - "class Shape { public: virtual int area() const; };\n\n" - "class Circle : public Shape {\npublic:\n" - " int r;\n Circle(int r) : r(r) {}\n" - " int area() const override { return r * r * 3; }\n" - " int run() const { return Shape::area(); }\n};\n"}}; + {"shape.cpp", "class Shape {\npublic:\n virtual int area() const { return 0; }\n};\n"}, + {"circle.cpp", "class Shape { public: virtual int area() const; };\n\n" + "class Circle : public Shape {\npublic:\n" + " int r;\n Circle(int r) : r(r) {}\n" + " int area() const override { return r * r * 3; }\n" + " int run() const { return Shape::area(); }\n};\n"}}; /* REAL BUG: the explicit base call `Shape::area()` from Circle::run() is not * resolved to a CALLS edge (diagnostics: calls=0, INHERITS=1 present). Same * class as cpp/S4 — the C++ lsp_cross does not resolve `Base::method()` @@ -689,12 +677,10 @@ TEST(lrp_cpp_s6_virtual_inherited) { /* S7 — C++ template function call. */ TEST(lrp_cpp_s7_template) { static const LRP_File f[] = { - {"algo.cpp", - "template\nT clamp(T v, T lo, T hi) {\n" - " if (v < lo) return lo;\n if (v > hi) return hi;\n return v;\n}\n"}, - {"main.cpp", - "template T clamp(T v, T lo, T hi);\n\n" - "int run(int n) { return clamp(n, 0, 100); }\n"}}; + {"algo.cpp", "template\nT clamp(T v, T lo, T hi) {\n" + " if (v < lo) return lo;\n if (v > hi) return hi;\n return v;\n}\n"}, + {"main.cpp", "template T clamp(T v, T lo, T hi);\n\n" + "int run(int n) { return clamp(n, 0, 100); }\n"}}; /* Uncertain: template functions require instantiation to resolve precisely. * The name-based resolver may find clamp regardless; assert CALLS >= 1. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S7/template", 1)); @@ -706,10 +692,9 @@ TEST(lrp_cpp_s8_field_call) { static const LRP_File f[] = { {"logger.cpp", "class Logger {\npublic:\n void log(const char *msg) { (void)msg; }\n};\n"}, - {"service.cpp", - "class Logger { public: void log(const char *); };\n\n" - "class Service {\n Logger logger;\npublic:\n" - " void run(const char *m) { logger.log(m); }\n};\n"}}; + {"service.cpp", "class Logger { public: void log(const char *); };\n\n" + "class Service {\n Logger logger;\npublic:\n" + " void run(const char *m) { logger.log(m); }\n};\n"}}; /* Uncertain: logger.log() — lsp_cross must see logger's type (Logger) from * the Service class definition in the same file. Assert CALLS >= 1. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "cpp/S8/field_call", 1)); @@ -750,16 +735,14 @@ TEST(lrp_rust_s2_method_dispatch) { /* Counter is in counter.rs; run() in runner.rs calls c.inc() and c.value(). * Without lsp_cross, the receiver type Counter is unknown in runner.rs, * so the method calls may not resolve to the correct impl methods. */ - static const LRP_File f[] = { - {"counter.rs", - "pub struct Counter { n: i32 }\n\n" - "impl Counter {\n" - " pub fn new(n: i32) -> Self { Counter { n } }\n" - " pub fn inc(&mut self) { self.n += 1; }\n" - " pub fn value(&self) -> i32 { self.n }\n}\n"}, - {"runner.rs", - "mod counter;\n\nfn run(c: &mut counter::Counter) -> i32 {\n" - " c.inc();\n c.value()\n}\n"}}; + static const LRP_File f[] = {{"counter.rs", "pub struct Counter { n: i32 }\n\n" + "impl Counter {\n" + " pub fn new(n: i32) -> Self { Counter { n } }\n" + " pub fn inc(&mut self) { self.n += 1; }\n" + " pub fn value(&self) -> i32 { self.n }\n}\n"}, + {"runner.rs", + "mod counter;\n\nfn run(c: &mut counter::Counter) -> i32 {\n" + " c.inc();\n c.value()\n}\n"}}; /* RED (expected to fail): lsp_cross not wired for Rust → method dispatch * through typed receiver not resolved by the generic resolver. * Root cause: cbm_pxc_has_cross_lsp returns false for CBM_LANG_RUST. @@ -775,9 +758,8 @@ TEST(lrp_rust_s3_constructor) { "pub struct Point { pub x: f64, pub y: f64 }\n\n" "impl Point {\n pub fn new(x: f64, y: f64) -> Self { Point { x, y } }\n" " pub fn dist(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }\n}\n"}, - {"main.rs", - "mod point;\n\nfn run() -> f64 {\n" - " let p = point::Point::new(3.0, 4.0);\n p.dist()\n}\n"}}; + {"main.rs", "mod point;\n\nfn run() -> f64 {\n" + " let p = point::Point::new(3.0, 4.0);\n p.dist()\n}\n"}}; /* RED: Point::new is a qualified associated function; the name resolver may find * "new" generically, but Point::new in context requires lsp_cross. * p.dist() requires receiver type Point — lsp_cross needed. @@ -789,13 +771,11 @@ TEST(lrp_rust_s3_constructor) { /* S4 — Rust static / associated method call (Type::method). */ TEST(lrp_rust_s4_static_method) { static const LRP_File f[] = { - {"config.rs", - "pub struct Config { pub debug: bool }\n\n" - "impl Config {\n pub fn default() -> Self { Config { debug: false } }\n" - " pub fn is_debug(&self) -> bool { self.debug }\n}\n"}, - {"app.rs", - "mod config;\n\nfn run() -> bool {\n" - " let cfg = config::Config::default();\n cfg.is_debug()\n}\n"}}; + {"config.rs", "pub struct Config { pub debug: bool }\n\n" + "impl Config {\n pub fn default() -> Self { Config { debug: false } }\n" + " pub fn is_debug(&self) -> bool { self.debug }\n}\n"}, + {"app.rs", "mod config;\n\nfn run() -> bool {\n" + " let cfg = config::Config::default();\n cfg.is_debug()\n}\n"}}; /* RED: Config::default() is an associated (static-like) function. * Without lsp_cross the receiver of is_debug() is unknown. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S4/static_method", 0)); @@ -805,15 +785,13 @@ TEST(lrp_rust_s4_static_method) { /* S5 — Rust chained method call. */ TEST(lrp_rust_s5_chained) { static const LRP_File f[] = { - {"builder.rs", - "pub struct Builder { items: Vec }\n\n" - "impl Builder {\n" - " pub fn new() -> Self { Builder { items: vec![] } }\n" - " pub fn add(mut self, x: i32) -> Self { self.items.push(x); self }\n" - " pub fn build(self) -> Vec { self.items }\n}\n"}, - {"main.rs", - "mod builder;\n\nfn run() -> Vec {\n" - " builder::Builder::new().add(1).add(2).build()\n}\n"}}; + {"builder.rs", "pub struct Builder { items: Vec }\n\n" + "impl Builder {\n" + " pub fn new() -> Self { Builder { items: vec![] } }\n" + " pub fn add(mut self, x: i32) -> Self { self.items.push(x); self }\n" + " pub fn build(self) -> Vec { self.items }\n}\n"}, + {"main.rs", "mod builder;\n\nfn run() -> Vec {\n" + " builder::Builder::new().add(1).add(2).build()\n}\n"}}; /* RED: the chain new() → add() → add() → build() requires lsp_cross to track * that each add() returns Self (Builder), which is needed to resolve build(). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "rust/S5/chained", 0)); @@ -825,14 +803,11 @@ TEST(lrp_rust_s6_trait_method) { /* Trait Display is defined in display.rs; Dog implements it in dog.rs. * run() in main.rs calls d.show() on a Dog where show() comes from the trait. */ static const LRP_File f[] = { - {"display.rs", - "pub trait Display {\n fn show(&self) -> String;\n}\n"}, - {"dog.rs", - "mod display;\n\npub struct Dog { pub name: String }\n\n" - "impl display::Display for Dog {\n" - " fn show(&self) -> String { self.name.clone() }\n}\n"}, - {"main.rs", - "mod dog;\n\nfn run(d: &dog::Dog) -> String {\n d.show()\n}\n"}}; + {"display.rs", "pub trait Display {\n fn show(&self) -> String;\n}\n"}, + {"dog.rs", "mod display;\n\npub struct Dog { pub name: String }\n\n" + "impl display::Display for Dog {\n" + " fn show(&self) -> String { self.name.clone() }\n}\n"}, + {"main.rs", "mod dog;\n\nfn run(d: &dog::Dog) -> String {\n d.show()\n}\n"}}; /* RED: d.show() on &Dog requires knowing Dog implements Display and that * show() maps to Dog's impl — needs lsp_cross + trait resolution. */ ASSERT_TRUE(lrp_assert_calls(f, 3, 1, "rust/S6/trait_method", 0)); @@ -842,11 +817,9 @@ TEST(lrp_rust_s6_trait_method) { /* S7 — Rust generic function call. */ TEST(lrp_rust_s7_generic) { static const LRP_File f[] = { - {"algo.rs", - "pub fn max_of(a: T, b: T) -> T {\n" - " if a > b { a } else { b }\n}\n"}, - {"main.rs", - "mod algo;\n\nfn run(x: i32, y: i32) -> i32 {\n algo::max_of(x, y)\n}\n"}}; + {"algo.rs", "pub fn max_of(a: T, b: T) -> T {\n" + " if a > b { a } else { b }\n}\n"}, + {"main.rs", "mod algo;\n\nfn run(x: i32, y: i32) -> i32 {\n algo::max_of(x, y)\n}\n"}}; /* REAL BUG (same root cause as rust/S1): a Rust `::`-qualified cross-file path * `algo::max_of(...)` is not resolved → calls=0. cbm_registry_resolve * (src/pipeline/registry.c:638) splits the callee on '.', not '::', so the @@ -860,13 +833,11 @@ TEST(lrp_rust_s7_generic) { /* S8 — Rust field method call. */ TEST(lrp_rust_s8_field_call) { static const LRP_File f[] = { - {"logger.rs", - "pub struct Logger;\n\nimpl Logger {\n" - " pub fn log(&self, msg: &str) { let _ = msg; }\n}\n"}, - {"service.rs", - "mod logger;\n\npub struct Service { pub logger: logger::Logger }\n\n" - "impl Service {\n" - " pub fn run(&self, msg: &str) { self.logger.log(msg); }\n}\n"}}; + {"logger.rs", "pub struct Logger;\n\nimpl Logger {\n" + " pub fn log(&self, msg: &str) { let _ = msg; }\n}\n"}, + {"service.rs", "mod logger;\n\npub struct Service { pub logger: logger::Logger }\n\n" + "impl Service {\n" + " pub fn run(&self, msg: &str) { self.logger.log(msg); }\n}\n"}}; /* RED: self.logger.log() — lsp_cross must see that self.logger is of type * logger::Logger and resolve log() to Logger::log. * Without lsp_cross the receiver type is unknown. */ @@ -922,14 +893,12 @@ TEST(lrp_python_s1_crossfile_call) { /* S2 — Python method dispatch via known class. */ TEST(lrp_python_s2_method_dispatch) { static const LRP_File f[] = { - {"counter.py", - "class Counter:\n" - " def __init__(self):\n self.n = 0\n\n" - " def inc(self):\n self.n += 1\n\n" - " def value(self):\n return self.n\n"}, - {"main.py", - "from .counter import Counter\n\n\n" - "def run():\n c = Counter()\n c.inc()\n return c.value()\n"}}; + {"counter.py", "class Counter:\n" + " def __init__(self):\n self.n = 0\n\n" + " def inc(self):\n self.n += 1\n\n" + " def value(self):\n return self.n\n"}, + {"main.py", "from .counter import Counter\n\n\n" + "def run():\n c = Counter()\n c.inc()\n return c.value()\n"}}; /* GREEN: py_lsp_cross tracks c = Counter() → type Counter; resolves c.inc() * and c.value() to Counter.inc / Counter.value. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S2/method_dispatch", 1)); @@ -939,11 +908,9 @@ TEST(lrp_python_s2_method_dispatch) { /* S3 — Python constructor call (class instantiation). */ TEST(lrp_python_s3_constructor) { static const LRP_File f[] = { - {"widget.py", - "class Widget:\n def __init__(self, name):\n self.name = name\n\n" - " def label(self):\n return self.name\n"}, - {"main.py", - "from .widget import Widget\n\n\ndef make(name):\n return Widget(name)\n"}}; + {"widget.py", "class Widget:\n def __init__(self, name):\n self.name = name\n\n" + " def label(self):\n return self.name\n"}, + {"main.py", "from .widget import Widget\n\n\ndef make(name):\n return Widget(name)\n"}}; /* GREEN: Widget(name) is a constructor call; py_lsp_cross sees Widget type from * the import and creates a CALLS edge make -> Widget.__init__. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S3/constructor", 1)); @@ -953,13 +920,11 @@ TEST(lrp_python_s3_constructor) { /* S4 — Python class method call (classmethod / staticmethod). */ TEST(lrp_python_s4_class_method) { static const LRP_File f[] = { - {"factory.py", - "class Factory:\n" - " @classmethod\n def create(cls, n):\n return cls()\n\n" - " @staticmethod\n def version():\n return '1.0'\n"}, - {"main.py", - "from .factory import Factory\n\n\ndef run():\n" - " v = Factory.version()\n return v\n"}}; + {"factory.py", "class Factory:\n" + " @classmethod\n def create(cls, n):\n return cls()\n\n" + " @staticmethod\n def version():\n return '1.0'\n"}, + {"main.py", "from .factory import Factory\n\n\ndef run():\n" + " v = Factory.version()\n return v\n"}}; /* GREEN: Factory.version() is a static method call; lsp_cross resolves it. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S4/class_method", 1)); PASS(); @@ -972,9 +937,8 @@ TEST(lrp_python_s5_chained) { "class Builder:\n def __init__(self):\n self.items = []\n\n" " def add(self, x):\n self.items.append(x)\n return self\n\n" " def build(self):\n return list(self.items)\n"}, - {"main.py", - "from .builder import Builder\n\n\ndef make():\n" - " return Builder().add(1).add(2).build()\n"}}; + {"main.py", "from .builder import Builder\n\n\ndef make():\n" + " return Builder().add(1).add(2).build()\n"}}; /* GREEN: Builder() returns Builder; add() returns self (Builder); build() * resolves via return-type chain. py_lsp_cross handles self-returning methods. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S5/chained", 1)); @@ -984,12 +948,10 @@ TEST(lrp_python_s5_chained) { /* S6 — Python inherited method call. */ TEST(lrp_python_s6_inherited_method) { static const LRP_File f[] = { - {"base.py", - "class Base:\n def describe(self):\n return 'base'\n"}, - {"child.py", - "from .base import Base\n\n\nclass Child(Base):\n" - " def extra(self):\n return 'extra'\n\n\n" - "def run(c):\n return c.describe()\n"}}; + {"base.py", "class Base:\n def describe(self):\n return 'base'\n"}, + {"child.py", "from .base import Base\n\n\nclass Child(Base):\n" + " def extra(self):\n return 'extra'\n\n\n" + "def run(c):\n return c.describe()\n"}}; /* Uncertain: c.describe() on a Child — py_lsp_cross must see Child inherits Base * (requires INHERITS edge resolution). Given the Python extraction bug for * base_classes, this may be RED end-to-end even if py_lsp_cross is correct. @@ -1001,12 +963,10 @@ TEST(lrp_python_s6_inherited_method) { /* S7 — Python type-annotated call (PEP 484 type hint). */ TEST(lrp_python_s7_annotated_call) { static const LRP_File f[] = { - {"repo.py", - "class Repo:\n def find(self, id: int) -> str:\n return ''\n"}, - {"service.py", - "from .repo import Repo\n\n\nclass Service:\n" - " def __init__(self, repo: Repo):\n self.repo = repo\n\n" - " def get(self, id: int) -> str:\n return self.repo.find(id)\n"}}; + {"repo.py", "class Repo:\n def find(self, id: int) -> str:\n return ''\n"}, + {"service.py", "from .repo import Repo\n\n\nclass Service:\n" + " def __init__(self, repo: Repo):\n self.repo = repo\n\n" + " def get(self, id: int) -> str:\n return self.repo.find(id)\n"}}; /* GREEN: self.repo is annotated as Repo; py_lsp_cross reads the annotation * and resolves self.repo.find() to Repo.find. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S7/annotated_call", 1)); @@ -1017,12 +977,10 @@ TEST(lrp_python_s7_annotated_call) { TEST(lrp_python_s8_field_type_hint) { /* Service stores a logger field and calls logger.log() in methods. */ static const LRP_File f[] = { - {"logger.py", - "class Logger:\n def log(self, msg: str) -> None:\n pass\n"}, - {"service.py", - "from .logger import Logger\n\n\nclass Service:\n" - " def __init__(self):\n self.logger: Logger = Logger()\n\n" - " def run(self, msg: str):\n self.logger.log(msg)\n"}}; + {"logger.py", "class Logger:\n def log(self, msg: str) -> None:\n pass\n"}, + {"service.py", "from .logger import Logger\n\n\nclass Service:\n" + " def __init__(self):\n self.logger: Logger = Logger()\n\n" + " def run(self, msg: str):\n self.logger.log(msg)\n"}}; /* GREEN: self.logger is annotated as Logger; py_lsp_cross resolves * self.logger.log() to Logger.log. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "python/S8/field_type_hint", 1)); @@ -1068,9 +1026,8 @@ TEST(lrp_python_crossfile_dunder_carrier) { TEST(lrp_ts_s1_crossfile_call) { static const LRP_File f[] = { {"util.ts", "export function format(s: string): string { return s.trim(); }\n"}, - {"main.ts", - "import { format } from './util';\n\n" - "export function run(s: string): string { return format(s); }\n"}}; + {"main.ts", "import { format } from './util';\n\n" + "export function run(s: string): string { return format(s); }\n"}}; ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S1/crossfile_call", 1)); PASS(); } @@ -1081,10 +1038,9 @@ TEST(lrp_ts_s2_method_dispatch) { {"counter.ts", "export class Counter {\n private n = 0;\n" " inc(): void { this.n++; }\n value(): number { return this.n; }\n}\n"}, - {"main.ts", - "import { Counter } from './counter';\n\n" - "export function run(): number {\n" - " const c = new Counter();\n c.inc();\n return c.value();\n}\n"}}; + {"main.ts", "import { Counter } from './counter';\n\n" + "export function run(): number {\n" + " const c = new Counter();\n c.inc();\n return c.value();\n}\n"}}; /* GREEN: ts_lsp_cross resolves c: Counter → inc() and value(). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S2/method_dispatch", 1)); PASS(); @@ -1093,9 +1049,8 @@ TEST(lrp_ts_s2_method_dispatch) { /* S3 — TypeScript constructor call. */ TEST(lrp_ts_s3_constructor) { static const LRP_File f[] = { - {"widget.ts", - "export class Widget {\n constructor(public name: string) {}\n" - " label(): string { return this.name; }\n}\n"}, + {"widget.ts", "export class Widget {\n constructor(public name: string) {}\n" + " label(): string { return this.name; }\n}\n"}, {"main.ts", "import { Widget } from './widget';\n\n" "export function make(name: string): Widget {\n return new Widget(name);\n}\n"}}; @@ -1112,12 +1067,10 @@ TEST(lrp_ts_s3_constructor) { /* S4 — TypeScript static method call. */ TEST(lrp_ts_s4_static_method) { static const LRP_File f[] = { - {"factory.ts", - "export class Factory {\n static version(): string { return '1.0'; }\n" - " static create(): Factory { return new Factory(); }\n}\n"}, - {"main.ts", - "import { Factory } from './factory';\n\n" - "export function run(): string { return Factory.version(); }\n"}}; + {"factory.ts", "export class Factory {\n static version(): string { return '1.0'; }\n" + " static create(): Factory { return new Factory(); }\n}\n"}, + {"main.ts", "import { Factory } from './factory';\n\n" + "export function run(): string { return Factory.version(); }\n"}}; /* GREEN: Factory.version() is a static method; ts_lsp_cross resolves it via * the imported Factory class. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S4/static_method", 1)); @@ -1127,14 +1080,12 @@ TEST(lrp_ts_s4_static_method) { /* S5 — TypeScript chained method call. */ TEST(lrp_ts_s5_chained) { static const LRP_File f[] = { - {"builder.ts", - "export class Builder {\n private items: number[] = [];\n" - " add(x: number): Builder { this.items.push(x); return this; }\n" - " build(): number[] { return this.items; }\n}\n"}, - {"main.ts", - "import { Builder } from './builder';\n\n" - "export function run(): number[] {\n" - " return new Builder().add(1).add(2).build();\n}\n"}}; + {"builder.ts", "export class Builder {\n private items: number[] = [];\n" + " add(x: number): Builder { this.items.push(x); return this; }\n" + " build(): number[] { return this.items; }\n}\n"}, + {"main.ts", "import { Builder } from './builder';\n\n" + "export function run(): number[] {\n" + " return new Builder().add(1).add(2).build();\n}\n"}}; /* GREEN: ts_lsp_cross infers Builder from new Builder(), add() returns Builder, * build() resolves through the chain. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S5/chained", 1)); @@ -1144,8 +1095,7 @@ TEST(lrp_ts_s5_chained) { /* S6 — TypeScript inherited method call. */ TEST(lrp_ts_s6_inherited_method) { static const LRP_File f[] = { - {"base.ts", - "export class Base {\n describe(): string { return 'base'; }\n}\n"}, + {"base.ts", "export class Base {\n describe(): string { return 'base'; }\n}\n"}, {"child.ts", "import { Base } from './base';\n\n" "export class Child extends Base {\n extra(): string { return 'child'; }\n}\n\n" @@ -1175,13 +1125,11 @@ TEST(lrp_ts_s6_inherited_method) { /* S7 — TypeScript generic function call. */ TEST(lrp_ts_s7_generic) { static const LRP_File f[] = { - {"algo.ts", - "export function maxOf(a: T, b: T, cmp: (x: T, y: T) => number): T {\n" - " return cmp(a, b) >= 0 ? a : b;\n}\n"}, - {"main.ts", - "import { maxOf } from './algo';\n\n" - "export function run(a: number, b: number): number {\n" - " return maxOf(a, b, (x, y) => x - y);\n}\n"}}; + {"algo.ts", "export function maxOf(a: T, b: T, cmp: (x: T, y: T) => number): T {\n" + " return cmp(a, b) >= 0 ? a : b;\n}\n"}, + {"main.ts", "import { maxOf } from './algo';\n\n" + "export function run(a: number, b: number): number {\n" + " return maxOf(a, b, (x, y) => x - y);\n}\n"}}; /* GREEN: maxOf is a named import; ts_lsp_cross resolves it regardless of * type parameter instantiation. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S7/generic", 1)); @@ -1193,11 +1141,10 @@ TEST(lrp_ts_s8_field_type_hint) { static const LRP_File f[] = { {"logger.ts", "export class Logger {\n log(msg: string): void { console.log(msg); }\n}\n"}, - {"service.ts", - "import { Logger } from './logger';\n\n" - "export class Service {\n private logger: Logger;\n" - " constructor() { this.logger = new Logger(); }\n" - " run(msg: string): void { this.logger.log(msg); }\n}\n"}}; + {"service.ts", "import { Logger } from './logger';\n\n" + "export class Service {\n private logger: Logger;\n" + " constructor() { this.logger = new Logger(); }\n" + " run(msg: string): void { this.logger.log(msg); }\n}\n"}}; /* GREEN: this.logger is typed as Logger; ts_lsp_cross sees the field type * and resolves this.logger.log() to Logger.log. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "ts/S8/field_type_hint", 1)); @@ -1219,8 +1166,10 @@ TEST(lrp_ts_s8_field_type_hint) { /* S1 — Java cross-file plain method call (same package). */ TEST(lrp_java_s1_crossfile_call) { static const LRP_File f[] = { - {"Util.java", "package app;\n\nclass Util {\n static int square(int x) { return x * x; }\n}\n"}, - {"Main.java", "package app;\n\nclass Main {\n int run(int n) { return Util.square(n); }\n}\n"}}; + {"Util.java", + "package app;\n\nclass Util {\n static int square(int x) { return x * x; }\n}\n"}, + {"Main.java", + "package app;\n\nclass Main {\n int run(int n) { return Util.square(n); }\n}\n"}}; /* GREEN: Util.square is resolved by the name-based resolver (class.method name match). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S1/crossfile_call", 1)); PASS(); @@ -1232,10 +1181,9 @@ TEST(lrp_java_s2_method_dispatch) { {"Counter.java", "package app;\n\nclass Counter {\n private int n = 0;\n" " public void inc() { n++; }\n public int value() { return n; }\n}\n"}, - {"Runner.java", - "package app;\n\nclass Runner {\n" - " public int run() {\n Counter c = new Counter();\n" - " c.inc();\n return c.value();\n }\n}\n"}}; + {"Runner.java", "package app;\n\nclass Runner {\n" + " public int run() {\n Counter c = new Counter();\n" + " c.inc();\n return c.value();\n }\n}\n"}}; /* RED: c.inc() / c.value() on Counter — without java_lsp_cross the * receiver type (Counter) is not known; the name-based resolver may find * "inc" and "value" by name, but won't guarantee the correct attribution. @@ -1247,13 +1195,11 @@ TEST(lrp_java_s2_method_dispatch) { /* S3 — Java constructor call (new T()). */ TEST(lrp_java_s3_constructor) { static const LRP_File f[] = { - {"Widget.java", - "package app;\n\nclass Widget {\n private String name;\n" - " public Widget(String name) { this.name = name; }\n" - " public String label() { return name; }\n}\n"}, - {"Factory.java", - "package app;\n\nclass Factory {\n" - " public Widget make(String name) { return new Widget(name); }\n}\n"}}; + {"Widget.java", "package app;\n\nclass Widget {\n private String name;\n" + " public Widget(String name) { this.name = name; }\n" + " public String label() { return name; }\n}\n"}, + {"Factory.java", "package app;\n\nclass Factory {\n" + " public Widget make(String name) { return new Widget(name); }\n}\n"}}; /* REAL BUG: `new Widget(name)` yields NO CALLS edge (diagnostics: calls=0, * DEFINES_METHOD=3 present). ROOT CAUSE: java_call_types (lang_specs.c) is * {"method_invocation"} only — it does NOT include @@ -1267,13 +1213,11 @@ TEST(lrp_java_s3_constructor) { /* S4 — Java static method call (Class.staticMethod). */ TEST(lrp_java_s4_static_method) { static const LRP_File f[] = { - {"MathUtil.java", - "package app;\n\nclass MathUtil {\n" - " public static int clamp(int v, int lo, int hi) {\n" - " return v < lo ? lo : v > hi ? hi : v;\n }\n}\n"}, - {"App.java", - "package app;\n\nclass App {\n" - " public int run(int n) { return MathUtil.clamp(n, 0, 100); }\n}\n"}}; + {"MathUtil.java", "package app;\n\nclass MathUtil {\n" + " public static int clamp(int v, int lo, int hi) {\n" + " return v < lo ? lo : v > hi ? hi : v;\n }\n}\n"}, + {"App.java", "package app;\n\nclass App {\n" + " public int run(int n) { return MathUtil.clamp(n, 0, 100); }\n}\n"}}; /* Uncertain: MathUtil.clamp — the name resolver may find "clamp" via name match. * Assert CALLS >= 1; may be GREEN even without lsp_cross if name is unique. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S4/static_method", 0)); @@ -1283,15 +1227,13 @@ TEST(lrp_java_s4_static_method) { /* S5 — Java chained method call. */ TEST(lrp_java_s5_chained) { static const LRP_File f[] = { - {"Builder.java", - "package app;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n" - "class Builder {\n private List items = new ArrayList<>();\n" - " public Builder add(int x) { items.add(x); return this; }\n" - " public List build() { return items; }\n}\n"}, - {"Main.java", - "package app;\n\nimport java.util.List;\n\n" - "class Main {\n public List run() {\n" - " return new Builder().add(1).add(2).build();\n }\n}\n"}}; + {"Builder.java", "package app;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n" + "class Builder {\n private List items = new ArrayList<>();\n" + " public Builder add(int x) { items.add(x); return this; }\n" + " public List build() { return items; }\n}\n"}, + {"Main.java", "package app;\n\nimport java.util.List;\n\n" + "class Main {\n public List run() {\n" + " return new Builder().add(1).add(2).build();\n }\n}\n"}}; /* RED: chained calls new Builder().add(1).add(2).build() require type tracking. * Without lsp_cross the intermediate types are unknown. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S5/chained", 0)); @@ -1300,12 +1242,11 @@ TEST(lrp_java_s5_chained) { /* S6 — Java inherited method call (subclass calls base method). */ TEST(lrp_java_s6_inherited_method) { - static const LRP_File f[] = { - {"Animal.java", - "package zoo;\n\nclass Animal {\n public String describe() { return \"animal\"; }\n}\n"}, - {"Dog.java", - "package zoo;\n\nclass Dog extends Animal {\n" - " public String run() { return describe(); }\n}\n"}}; + static const LRP_File f[] = {{"Animal.java", "package zoo;\n\nclass Animal {\n public " + "String describe() { return \"animal\"; }\n}\n"}, + {"Dog.java", + "package zoo;\n\nclass Dog extends Animal {\n" + " public String run() { return describe(); }\n}\n"}}; /* Uncertain: describe() in Dog.run() — the name-based resolver may find it * if "describe" is unique in the project. Assert CALLS >= 1. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S6/inherited_method", 0)); @@ -1315,15 +1256,13 @@ TEST(lrp_java_s6_inherited_method) { /* S7 — Java generic method call. */ TEST(lrp_java_s7_generic) { static const LRP_File f[] = { - {"Util.java", - "package app;\n\nimport java.util.List;\n\n" - "class Util {\n public static T first(List list) {\n" - " return list.isEmpty() ? null : list.get(0);\n }\n}\n"}, - {"App.java", - "package app;\n\nimport java.util.List;\nimport java.util.ArrayList;\n\n" - "class App {\n public String run() {\n" - " List xs = new ArrayList<>();\n" - " xs.add(\"hello\");\n return Util.first(xs);\n }\n}\n"}}; + {"Util.java", "package app;\n\nimport java.util.List;\n\n" + "class Util {\n public static T first(List list) {\n" + " return list.isEmpty() ? null : list.get(0);\n }\n}\n"}, + {"App.java", "package app;\n\nimport java.util.List;\nimport java.util.ArrayList;\n\n" + "class App {\n public String run() {\n" + " List xs = new ArrayList<>();\n" + " xs.add(\"hello\");\n return Util.first(xs);\n }\n}\n"}}; /* Uncertain: Util.first is a generic static; name resolver may find it. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "java/S7/generic", 0)); PASS(); @@ -1332,9 +1271,8 @@ TEST(lrp_java_s7_generic) { /* S8 — Java field-type-hint (this.field.method()). */ TEST(lrp_java_s8_field_type_hint) { static const LRP_File f[] = { - {"Logger.java", - "package app;\n\nclass Logger {\n" - " public void log(String msg) { System.out.println(msg); }\n}\n"}, + {"Logger.java", "package app;\n\nclass Logger {\n" + " public void log(String msg) { System.out.println(msg); }\n}\n"}, {"Service.java", "package app;\n\nclass Service {\n private Logger logger = new Logger();\n" " public void run(String msg) { logger.log(msg); }\n}\n"}}; @@ -1363,9 +1301,8 @@ TEST(lrp_java_s8_field_type_hint) { * link this Kotlin cross-file call. Fix = add Kotlin cross-file LSP * (internal/cbm/lsp/kotlin_lsp.c) and wire it in pass_lsp_cross.c. */ TEST(lrp_kotlin_s1_crossfile_call) { - static const LRP_File f[] = { - {"Util.kt", "fun double(x: Int): Int = x * 2\n"}, - {"Main.kt", "fun run(n: Int): Int = double(n)\n"}}; + static const LRP_File f[] = {{"Util.kt", "fun double(x: Int): Int = x * 2\n"}, + {"Main.kt", "fun run(n: Int): Int = double(n)\n"}}; ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S1/crossfile_call", 1)); PASS(); } @@ -1373,11 +1310,9 @@ TEST(lrp_kotlin_s1_crossfile_call) { /* S2 — Kotlin method dispatch on typed receiver. */ TEST(lrp_kotlin_s2_method_dispatch) { static const LRP_File f[] = { - {"Counter.kt", - "class Counter {\n private var n = 0\n" - " fun inc() { n++ }\n fun value(): Int = n\n}\n"}, - {"Runner.kt", - "fun run(c: Counter): Int {\n c.inc()\n return c.value()\n}\n"}}; + {"Counter.kt", "class Counter {\n private var n = 0\n" + " fun inc() { n++ }\n fun value(): Int = n\n}\n"}, + {"Runner.kt", "fun run(c: Counter): Int {\n c.inc()\n return c.value()\n}\n"}}; /* RED: c.inc() / c.value() require knowing c: Counter and resolving * methods through the class definition in Counter.kt. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S2/method_dispatch", 0)); @@ -1387,10 +1322,8 @@ TEST(lrp_kotlin_s2_method_dispatch) { /* S3 — Kotlin constructor call. */ TEST(lrp_kotlin_s3_constructor) { static const LRP_File f[] = { - {"Widget.kt", - "class Widget(val name: String) {\n fun label(): String = name\n}\n"}, - {"Main.kt", - "fun make(name: String): Widget = Widget(name)\n"}}; + {"Widget.kt", "class Widget(val name: String) {\n fun label(): String = name\n}\n"}, + {"Main.kt", "fun make(name: String): Widget = Widget(name)\n"}}; /* RED: Widget(name) is a constructor call; without cross-file LSP the type * Widget from Widget.kt is not in the per-file resolver scope. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S3/constructor", 0)); @@ -1403,8 +1336,7 @@ TEST(lrp_kotlin_s4_companion_static) { {"Config.kt", "class Config(val debug: Boolean) {\n" " companion object {\n fun default(): Config = Config(false)\n }\n}\n"}, - {"App.kt", - "fun run(): Config = Config.default()\n"}}; + {"App.kt", "fun run(): Config = Config.default()\n"}}; /* RED: Config.default() is a companion-object call; requires cross-file type * knowledge of Config and its companion object. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S4/companion_static", 0)); @@ -1414,12 +1346,10 @@ TEST(lrp_kotlin_s4_companion_static) { /* S5 — Kotlin chained method call. */ TEST(lrp_kotlin_s5_chained) { static const LRP_File f[] = { - {"Builder.kt", - "class Builder {\n private val items = mutableListOf()\n" - " fun add(x: Int): Builder { items.add(x); return this }\n" - " fun build(): List = items.toList()\n}\n"}, - {"Main.kt", - "fun run(): List = Builder().add(1).add(2).build()\n"}}; + {"Builder.kt", "class Builder {\n private val items = mutableListOf()\n" + " fun add(x: Int): Builder { items.add(x); return this }\n" + " fun build(): List = items.toList()\n}\n"}, + {"Main.kt", "fun run(): List = Builder().add(1).add(2).build()\n"}}; /* RED: Builder() → add() → build() chain requires type tracking across files. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S5/chained", 0)); PASS(); @@ -1428,10 +1358,8 @@ TEST(lrp_kotlin_s5_chained) { /* S6 — Kotlin inherited method call (open class). */ TEST(lrp_kotlin_s6_inherited_method) { static const LRP_File f[] = { - {"Base.kt", - "open class Base {\n open fun describe(): String = \"base\"\n}\n"}, - {"Child.kt", - "class Child : Base() {\n fun run(): String = describe()\n}\n"}}; + {"Base.kt", "open class Base {\n open fun describe(): String = \"base\"\n}\n"}, + {"Child.kt", "class Child : Base() {\n fun run(): String = describe()\n}\n"}}; /* RED: describe() in Child.run() requires knowing Child extends Base. * Kotlin extraction bug (`:` supertype not parsed) compounds this. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S6/inherited_method", 0)); @@ -1441,10 +1369,8 @@ TEST(lrp_kotlin_s6_inherited_method) { /* S7 — Kotlin generic function call. */ TEST(lrp_kotlin_s7_generic) { static const LRP_File f[] = { - {"Algo.kt", - "fun > maxOf(a: T, b: T): T = if (a > b) a else b\n"}, - {"Main.kt", - "fun run(a: Int, b: Int): Int = maxOf(a, b)\n"}}; + {"Algo.kt", "fun > maxOf(a: T, b: T): T = if (a > b) a else b\n"}, + {"Main.kt", "fun run(a: Int, b: Int): Int = maxOf(a, b)\n"}}; /* Uncertain: maxOf is a top-level generic function; name resolver may find * it. Assert CALLS >= 1; may be GREEN via name resolver. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S7/generic", 0)); @@ -1454,11 +1380,9 @@ TEST(lrp_kotlin_s7_generic) { /* S8 — Kotlin field-type-hint (this.field.method()). */ TEST(lrp_kotlin_s8_field_type_hint) { static const LRP_File f[] = { - {"Logger.kt", - "class Logger {\n fun log(msg: String): Unit { println(msg) }\n}\n"}, - {"Service.kt", - "class Service {\n private val logger = Logger()\n" - " fun run(msg: String) { logger.log(msg) }\n}\n"}}; + {"Logger.kt", "class Logger {\n fun log(msg: String): Unit { println(msg) }\n}\n"}, + {"Service.kt", "class Service {\n private val logger = Logger()\n" + " fun run(msg: String) { logger.log(msg) }\n}\n"}}; /* RED: logger.log() — without cross-file LSP the type of logger (Logger) * is not known; name resolver may find "log" by name alone. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "kotlin/S8/field_type_hint", 0)); @@ -1481,12 +1405,10 @@ TEST(lrp_kotlin_s8_field_type_hint) { /* S1 — C# cross-file plain static call. */ TEST(lrp_csharp_s1_crossfile_call) { static const LRP_File f[] = { - {"Util.cs", - "namespace App {\n class Util {\n" - " public static int Square(int x) { return x * x; }\n }\n}\n"}, - {"Main.cs", - "namespace App {\n class Main {\n" - " public int Run(int n) { return Util.Square(n); }\n }\n}\n"}}; + {"Util.cs", "namespace App {\n class Util {\n" + " public static int Square(int x) { return x * x; }\n }\n}\n"}, + {"Main.cs", "namespace App {\n class Main {\n" + " public int Run(int n) { return Util.Square(n); }\n }\n}\n"}}; /* GREEN: Util.Square found by name-based resolver (class.method pattern). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S1/crossfile_call", 1)); PASS(); @@ -1495,9 +1417,9 @@ TEST(lrp_csharp_s1_crossfile_call) { /* S2 — C# method dispatch on typed instance. */ TEST(lrp_csharp_s2_method_dispatch) { static const LRP_File f[] = { - {"Counter.cs", - "namespace App {\n class Counter {\n private int n = 0;\n" - " public void Inc() { n++; }\n public int Value() { return n; }\n }\n}\n"}, + {"Counter.cs", "namespace App {\n class Counter {\n private int n = 0;\n" + " public void Inc() { n++; }\n public int Value() { return n; " + "}\n }\n}\n"}, {"Runner.cs", "namespace App {\n class Runner {\n" " public int Run() {\n var c = new Counter();\n" @@ -1511,10 +1433,9 @@ TEST(lrp_csharp_s2_method_dispatch) { /* S3 — C# constructor call (new T()). */ TEST(lrp_csharp_s3_constructor) { static const LRP_File f[] = { - {"Widget.cs", - "namespace App {\n class Widget {\n public string Name;\n" - " public Widget(string name) { Name = name; }\n" - " public string Label() { return Name; }\n }\n}\n"}, + {"Widget.cs", "namespace App {\n class Widget {\n public string Name;\n" + " public Widget(string name) { Name = name; }\n" + " public string Label() { return Name; }\n }\n}\n"}, {"Factory.cs", "namespace App {\n class Factory {\n" " public Widget Make(string name) { return new Widget(name); }\n }\n}\n"}}; @@ -1546,11 +1467,10 @@ TEST(lrp_csharp_s4_static_method) { /* S5 — C# chained method call. */ TEST(lrp_csharp_s5_chained) { static const LRP_File f[] = { - {"Builder.cs", - "namespace App {\n using System.Collections.Generic;\n" - " class Builder {\n private List items = new List();\n" - " public Builder Add(int x) { items.Add(x); return this; }\n" - " public List Build() { return items; }\n }\n}\n"}, + {"Builder.cs", "namespace App {\n using System.Collections.Generic;\n" + " class Builder {\n private List items = new List();\n" + " public Builder Add(int x) { items.Add(x); return this; }\n" + " public List Build() { return items; }\n }\n}\n"}, {"Main.cs", "namespace App {\n using System.Collections.Generic;\n" " class Main {\n public List Run() {\n" @@ -1563,12 +1483,10 @@ TEST(lrp_csharp_s5_chained) { /* S6 — C# inherited method call. */ TEST(lrp_csharp_s6_inherited_method) { static const LRP_File f[] = { - {"Base.cs", - "namespace App {\n class Base {\n" - " public virtual string Describe() { return \"base\"; }\n }\n}\n"}, - {"Child.cs", - "namespace App {\n class Child : Base {\n" - " public string Run() { return Describe(); }\n }\n}\n"}}; + {"Base.cs", "namespace App {\n class Base {\n" + " public virtual string Describe() { return \"base\"; }\n }\n}\n"}, + {"Child.cs", "namespace App {\n class Child : Base {\n" + " public string Run() { return Describe(); }\n }\n}\n"}}; /* Uncertain: Describe() in Child.Run() — name resolver may find it if unique. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S6/inherited_method", 0)); PASS(); @@ -1582,12 +1500,11 @@ TEST(lrp_csharp_s7_generic) { " class Container {\n" " public static T First(List xs) {\n" " return xs.Count > 0 ? xs[0] : default;\n }\n }\n}\n"}, - {"App.cs", - "namespace App {\n using System.Collections.Generic;\n" - " class AppEntry {\n" - " public string Run() {\n" - " var xs = new List { \"hello\" };\n" - " return Container.First(xs);\n }\n }\n}\n"}}; + {"App.cs", "namespace App {\n using System.Collections.Generic;\n" + " class AppEntry {\n" + " public string Run() {\n" + " var xs = new List { \"hello\" };\n" + " return Container.First(xs);\n }\n }\n}\n"}}; /* Uncertain: Container.First is a static generic method. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S7/generic", 0)); PASS(); @@ -1596,13 +1513,11 @@ TEST(lrp_csharp_s7_generic) { /* S8 — C# field-type-hint (this.field.method()). */ TEST(lrp_csharp_s8_field_type_hint) { static const LRP_File f[] = { - {"Logger.cs", - "namespace App {\n class Logger {\n" - " public void Log(string msg) { }\n }\n}\n"}, - {"Service.cs", - "namespace App {\n class Service {\n" - " private Logger logger = new Logger();\n" - " public void Run(string msg) { logger.Log(msg); }\n }\n}\n"}}; + {"Logger.cs", "namespace App {\n class Logger {\n" + " public void Log(string msg) { }\n }\n}\n"}, + {"Service.cs", "namespace App {\n class Service {\n" + " private Logger logger = new Logger();\n" + " public void Run(string msg) { logger.Log(msg); }\n }\n}\n"}}; /* RED: logger.Log() — without cs_lsp_cross the receiver type Logger is not * tracked from the field declaration. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "csharp/S8/field_type_hint", 0)); @@ -1627,15 +1542,13 @@ TEST(lrp_php_s1_crossfile_call) { /* S2 — PHP method dispatch via $this (cross-file). */ TEST(lrp_php_s2_method_dispatch) { - static const LRP_File f[] = { - {"Counter.php", - "n++; }\n" - " public function value() { return $this->n; }\n}\n"}, - {"main.php", - "inc();\n return $c->value();\n}\n"}}; + static const LRP_File f[] = {{"Counter.php", + "n++; }\n" + " public function value() { return $this->n; }\n}\n"}, + {"main.php", "inc();\n return $c->value();\n}\n"}}; /* GREEN: php_lsp_cross tracks $c = new Counter() → type Counter; * resolves $c->inc() / $c->value(). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S2/method_dispatch", 1)); @@ -1645,13 +1558,11 @@ TEST(lrp_php_s2_method_dispatch) { /* S3 — PHP constructor (new T()). */ TEST(lrp_php_s3_constructor) { static const LRP_File f[] = { - {"Widget.php", - "name = $name; }\n" - " public function label() { return $this->name; }\n}\n"}, - {"factory.php", - "name = $name; }\n" + " public function label() { return $this->name; }\n}\n"}, + {"factory.php", "items[] = $x; return $this; }\n" - " public function build() { return $this->items; }\n}\n"}, - {"main.php", - "add(1)->add(2)->build(); }\n"}}; + {"Builder.php", "items[] = $x; return $this; }\n" + " public function build() { return $this->items; }\n}\n"}, + {"main.php", "add(1)->add(2)->build(); }\n"}}; /* GREEN: (new Builder())->add()->add()->build() — php_lsp_cross tracks * self-returning methods (return $this). */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S5/chained", 1)); @@ -1695,11 +1603,9 @@ TEST(lrp_php_s5_chained) { /* S6 — PHP inherited method call. */ TEST(lrp_php_s6_inherited_method) { static const LRP_File f[] = { - {"Base.php", - "describe(); }\n}\n"}}; + {"Base.php", "describe(); }\n}\n"}}; /* Uncertain: $this->describe() in Child::run() — php_lsp_cross must see * Child extends Base. PHP extraction bug (base_classes not populated) * may block this end-to-end. Assert CALLS >= 1. */ @@ -1716,12 +1622,11 @@ TEST(lrp_php_s7_interface_call) { "logger = $logger;\n }\n" - " public function run($msg) { $this->logger->log($msg); }\n}\n"}}; + {"Service.php", "logger = $logger;\n }\n" + " public function run($msg) { $this->logger->log($msg); }\n}\n"}}; /* GREEN: $this->logger is type-hinted as LoggerInterface; php_lsp_cross * resolves $this->logger->log() to LoggerInterface::log. */ ASSERT_TRUE(lrp_assert_calls(f, 3, 1, "php/S7/interface_call", 1)); @@ -1733,13 +1638,12 @@ TEST(lrp_php_s8_field_type_hint) { static const LRP_File f[] = { {"Mailer.php", "mailer = $mailer;\n }\n" - " public function notify($to, $msg) {\n" - " $this->mailer->send($to, $msg);\n }\n}\n"}}; + {"Notifier.php", "mailer = $mailer;\n }\n" + " public function notify($to, $msg) {\n" + " $this->mailer->send($to, $msg);\n }\n}\n"}}; /* GREEN: $this->mailer is type-hinted as Mailer in constructor parameter; * php_lsp_cross resolves $this->mailer->send() to Mailer::send. */ ASSERT_TRUE(lrp_assert_calls(f, 2, 1, "php/S8/field_type_hint", 1)); @@ -1757,8 +1661,7 @@ TEST(lrp_php_s8_field_type_hint) { TEST(lrp_go_usage_struct_literal) { static const LRP_File f[] = { {"types.go", "package app\n\ntype Config struct{ Port int }\n"}, - {"main.go", - "package app\n\nfunc Make() Config { return Config{Port: 8080} }\n"}}; + {"main.go", "package app\n\nfunc Make() Config { return Config{Port: 8080} }\n"}}; LRP_Proj lp; int n = lrp_usage(&lp, f, 2); /* GREEN: USAGE edge from Make to Config type (struct literal). */ @@ -1773,9 +1676,8 @@ TEST(lrp_go_usage_struct_literal) { TEST(lrp_ts_usage_type_param) { static const LRP_File f[] = { {"types.ts", "export interface Config { timeout: number; }\n"}, - {"main.ts", - "import { Config } from './types';\n\n" - "export function run(cfg: Config): number { return cfg.timeout; }\n"}}; + {"main.ts", "import { Config } from './types';\n\n" + "export function run(cfg: Config): number { return cfg.timeout; }\n"}}; LRP_Proj lp; cbm_store_t *store = lrp_index(&lp, f, 2); int exact = store ? lrp_exact_edge_by_name(store, lp.project, "USAGE", "run", "Config") : -1; @@ -1818,8 +1720,7 @@ TEST(lrp_tsx_usage_import_type_with_decoy) { TEST(lrp_python_usage_instantiation) { static const LRP_File f[] = { {"model.py", "class User:\n def __init__(self, name):\n self.name = name\n"}, - {"main.py", - "from .model import User\n\n\ndef create(name):\n return User(name)\n"}}; + {"main.py", "from .model import User\n\n\ndef create(name):\n return User(name)\n"}}; LRP_Proj lp; cbm_store_t *store = lrp_index(&lp, f, 2); int exact = store ? lrp_exact_calls_by_name(store, lp.project, "create", "User") : -1; From bb4fca52c09a36ba34a39495e658f19b398c153f Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Fri, 7 Aug 2026 04:41:50 +0200 Subject: [PATCH 5/5] fix(discover): resolve the cache directory once per walk, not per directory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cbm_resolve_cache_dir() returns a pointer to a static thread-local buffer. The cache prune called it for every directory the walk visited, so it rewrote a buffer other code was entitled to still be holding — an aliasing bug, not a cosmetic inefficiency. The Windows guards job caught it as two REDs: a daemon cold-storm regression and a following "index did not run" setup failure. Both are consistent with the cache path being rewritten underneath a live reader during indexing. Resolve once at the discovery entry point into a thread-local copy and compare against that, so the prune costs one resolver call per walk rather than one per directory, and holds no pointer into shared storage. Signed-off-by: Martin Vogel --- src/discover/discover.c | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/discover/discover.c b/src/discover/discover.c index 76896b98b..fbcd9bc8d 100644 --- a/src/discover/discover.c +++ b/src/discover/discover.c @@ -563,6 +563,19 @@ static bool is_safety_core_dir(const char *name) { } /* Check if a directory entry should be skipped (hardcoded dirs + gitignore). */ +/* Snapshot of the cache directory for the current walk. + * + * cbm_resolve_cache_dir() returns a pointer to a static thread-local buffer, so + * calling it once per directory — as an earlier version of this prune did — + * rewrites a buffer other code may still be holding. Resolve once at the entry + * point and compare against the copy. */ +static _Thread_local char g_walk_cache_dir[CBM_SZ_4K]; + +static void walk_cache_dir_snapshot(void) { + const char *cache = cbm_workspace_cache_dir(); + snprintf(g_walk_cache_dir, sizeof(g_walk_cache_dir), "%s", cache ? cache : ""); +} + /* The cache directory holds every indexed project's graph database. When a custom * CBM_CACHE_DIR sits inside a repository — which happens in tests and is legal in * production — walking into it would pull other projects' databases into this @@ -572,8 +585,8 @@ static bool is_safety_core_dir(const char *name) { * refusing any root containing the cache: refusing a whole root was too blunt, * and not walking the cache is what the concern actually asks for. */ static bool dir_is_cache_tree(const char *abs_path) { - const char *cache = cbm_workspace_cache_dir(); - if (!cache || !cache[0] || !abs_path || !abs_path[0]) { + const char *cache = g_walk_cache_dir; + if (!cache[0] || !abs_path || !abs_path[0]) { return false; } size_t n = strlen(cache); @@ -1190,6 +1203,7 @@ static cbm_discover_status_t discover_impl(const char *repo_path, const cbm_disc .collect_excluded = !count_only && excluded_out != NULL, .collect_ignored = !count_only && ignored_out != NULL, }; + walk_cache_dir_snapshot(); walk_dir(repo_path, "", opts, gitignore, global_gi, cbmignore, &fl); /* Cleanup */