Files

5608 lines
244 KiB
C

/*
* test_parallel.c — Tests for the three-phase parallel pipeline.
*
* Validates parity between sequential (4-pass) and parallel (3-phase)
* pipeline modes on a small Go test fixture.
*
* Suite: suite_parallel
*/
#include "../src/foundation/compat.h"
#include "test_framework.h"
#include "test_helpers.h"
#include "pipeline/pipeline.h"
#include "pipeline/pipeline_internal.h"
#include "pipeline/pass_lsp_cross.h"
#include "pipeline/lsp_resolve.h"
#include "pipeline/worker_pool.h"
#include "graph_buffer/graph_buffer.h"
#include "discover/discover.h"
#include "foundation/platform.h"
#include "foundation/compat_fs.h"
#include "foundation/log.h"
#include "foundation/mem.h"
#include "cbm.h"
#include "result_spill.h"
#include <stdlib.h>
#include <string.h>
#include <stdatomic.h>
#include <sys/stat.h>
/* ── Helper: create temp test repo ───────────────────────────────── */
static char g_par_tmpdir[256];
TEST(usage_semantic_reference_candidate_trusts_marked_producer) {
CBMUsage usage = {0};
usage.kind = CBM_USAGE_VALUE;
usage.may_be_call_reference = true;
ASSERT_TRUE(cbm_pipeline_usage_semantic_reference_candidate(&usage));
usage.may_be_call_reference = false;
ASSERT_FALSE(cbm_pipeline_usage_semantic_reference_candidate(&usage));
usage.kind = CBM_USAGE_CALL_REFERENCE;
ASSERT_TRUE(cbm_pipeline_usage_semantic_reference_candidate(&usage));
/* Lexical evidence gates only the later textual fallback. An exact
* occurrence row must still get the chance to prove one callable target. */
usage.semantic_reference_blocked = true;
ASSERT_TRUE(cbm_pipeline_usage_semantic_reference_candidate(&usage));
PASS();
}
static int setup_parallel_repo(void) {
snprintf(g_par_tmpdir, sizeof(g_par_tmpdir), "/tmp/cbm_par_XXXXXX");
if (!cbm_mkdtemp(g_par_tmpdir))
return -1;
char path[512];
/* main.go */
snprintf(path, sizeof(path), "%s/main.go", g_par_tmpdir);
FILE *f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package main\n\nimport \"pkg\"\n\n"
"func main() {\n\tpkg.Serve()\n}\n");
fclose(f);
/* pkg/ */
snprintf(path, sizeof(path), "%s/pkg", g_par_tmpdir);
cbm_mkdir(path);
/* pkg/service.go */
snprintf(path, sizeof(path), "%s/pkg/service.go", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package pkg\n\nimport \"pkg/util\"\n\n"
"func Serve() {\n\tutil.Help()\n}\n");
fclose(f);
/* pkg/util/ */
snprintf(path, sizeof(path), "%s/pkg/util", g_par_tmpdir);
cbm_mkdir(path);
/* pkg/util/helper.go */
snprintf(path, sizeof(path), "%s/pkg/util/helper.go", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package util\n\nfunc Help() {}\n");
fclose(f);
/* Java interface/implements/extends trio: the parallel resolve path must
* make the same Interface-label edge split (IMPLEMENTS vs INHERITS) as
* the sequential semantic pass, and both must emit OVERRIDE for methods
* redefined from an explicit base. Without these files the IMPLEMENTS/
* INHERITS parity tests compare 0 == 0 and guard nothing (the demotion
* bug shipped: elasticsearch had 11k implements-as-INHERITS edges). */
snprintf(path, sizeof(path), "%s/probe", g_par_tmpdir);
cbm_mkdir(path);
snprintf(path, sizeof(path), "%s/probe/Shape.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic interface Shape {\n double area();\n}\n");
fclose(f);
snprintf(path, sizeof(path), "%s/probe/Circle.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic class Circle implements Shape {\n"
" @Override\n public double area() { return 1.0; }\n}\n");
fclose(f);
snprintf(path, sizeof(path), "%s/probe/Base.java", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "package probe;\npublic class Base extends Circle {\n"
" @Override\n public double area() { return 0.0; }\n}\n");
fclose(f);
/* NAMESPACE-declaring files whose import resolves through the namespace
* map (PHP `use`; C# `using` and Java package imports take the same path).
* Without these the spill-parity test compared a Go+Java fixture that never
* touches that map, so it passed while spilling silently changed import
* resolution for every namespaced repository: the map is built from the
* in-memory result cache, where a PARKED result is NULL, so a spilled file
* contributed no namespace at all and its imports fell through to the
* looser fallback (php corpus, 2026-09-18: 57,182 edges in memory against
* 59,379 while spilling, same binary, reproducible 3/3 each way). */
snprintf(path, sizeof(path), "%s/app", g_par_tmpdir);
cbm_mkdir(path);
snprintf(path, sizeof(path), "%s/app/Models.php", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "<?php\nnamespace App\\Models;\n\n"
"class User {\n public function name() { return \"u\"; }\n}\n");
fclose(f);
snprintf(path, sizeof(path), "%s/app/Services.php", g_par_tmpdir);
f = fopen(path, "w");
if (!f)
return -1;
fprintf(f, "<?php\nnamespace App\\Services;\n\nuse App\\Models\\User;\n\n"
"class UserService {\n"
" public function make() { $u = new User(); return $u->name(); }\n}\n");
fclose(f);
return 0;
}
static void rm_rf(const char *path) {
th_rmtree(path);
}
static void teardown_parallel_repo(void) {
if (g_par_tmpdir[0])
rm_rf(g_par_tmpdir);
g_par_tmpdir[0] = '\0';
}
/* The compact parity helpers normally omit the production structure pass.
* Tests that exercise graph-derived import maps must seed its File-node
* precondition explicitly, without changing legacy parity fixtures. */
static void seed_test_file_nodes(cbm_gbuf_t *gbuf, const char *project,
const cbm_file_info_t *files, int file_count) {
if (!gbuf || !project || !files) {
return;
}
for (int i = 0; i < file_count; i++) {
const char *rel = files[i].rel_path;
if (!rel) {
continue;
}
char *file_qn = cbm_pipeline_fqn_compute(project, rel, "__file__");
const char *slash = strrchr(rel, '/');
const char *basename = slash ? slash + 1 : rel;
if (file_qn) {
cbm_gbuf_upsert_node(gbuf, "File", basename, file_qn, rel, 0, 0, "{}");
}
free(file_qn);
}
}
/* ── Run sequential pipeline on files, returning gbuf ─────────────── */
/* Free the return-type table pass_calls may have built for a harness-owned
* ctx — mirrors the production teardown in pipeline.c; the harness never
* runs it, which leaked once the fixture gained typed (Java) methods. The
* fixture has no ObjectScript, so no macro table can exist here. */
static void harness_ctx_free_tables(cbm_pipeline_ctx_t *ctx) {
if (ctx->return_type_table) {
for (int i = 0; i < ctx->return_type_table->count; i++) {
free((void *)ctx->return_type_table->entries[i].return_type);
}
free((void *)ctx->return_type_table);
ctx->return_type_table = NULL;
}
}
static cbm_gbuf_t *run_sequential(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
cbm_registry_t *reg = cbm_registry_new();
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
};
cbm_init();
cbm_pipeline_pass_definitions(&ctx, files, file_count);
cbm_pipeline_pass_calls(&ctx, files, file_count);
cbm_pipeline_pass_usages(&ctx, files, file_count);
cbm_pipeline_pass_semantic(&ctx, files, file_count);
harness_ctx_free_tables(&ctx);
cbm_registry_free(reg);
return gbuf;
}
typedef void (*parallel_result_mutator_fn)(CBMFileResult **result_cache, int file_count, void *ud);
/* Production's sequential pipeline retains one extraction cache and runs the
* cross-file LSP pass between definitions and edge materialization. Keep this
* explicit helper for tests whose semantic target is declared in another file;
* the lightweight run_sequential helper above intentionally predates that pass. */
static cbm_gbuf_t *run_sequential_with_lsp_cross_and_mutator(
const char *project, const char *repo_path, cbm_file_info_t *files, int file_count,
parallel_result_mutator_fn mutator, void *mutator_ud, bool seed_structure) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
cbm_registry_t *reg = cbm_registry_new();
CBMFileResult **cache = (CBMFileResult **)calloc((size_t)file_count, sizeof(CBMFileResult *));
if (!gbuf || !reg || !cache) {
cbm_gbuf_free(gbuf);
cbm_registry_free(reg);
free(cache);
return NULL;
}
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
.result_cache = cache,
};
if (seed_structure) {
seed_test_file_nodes(gbuf, project, files, file_count);
}
cbm_init();
cbm_pipeline_pass_definitions(&ctx, files, file_count);
cbm_pipeline_pass_lsp_cross(&ctx, files, file_count, cache);
if (mutator) {
mutator(cache, file_count, mutator_ud);
}
cbm_pipeline_pass_calls(&ctx, files, file_count);
cbm_pipeline_pass_usages(&ctx, files, file_count);
cbm_pipeline_pass_semantic(&ctx, files, file_count);
for (int i = 0; i < file_count; i++) {
cbm_free_result(cache[i]);
}
free(cache);
cbm_registry_free(reg);
/* The cross pass builds its shared registries in the caller-owned
* ctx->seq_cross_arena precisely so they outlive pass_calls; production's
* run_sequential_pipeline destroys that arena after all passes, and this
* harness must too -- LSan flagged the whole registry arena (stdlib
* registrations included) leaking per test. */
if (ctx.seq_cross_arena_live) {
cbm_arena_destroy(&ctx.seq_cross_arena);
ctx.seq_cross_arena_live = false;
}
if (ctx.seq_cross_def_modules) {
for (int i = 0; i < ctx.seq_cross_def_module_count; i++) {
free(ctx.seq_cross_def_modules[i]);
}
free(ctx.seq_cross_def_modules);
ctx.seq_cross_def_modules = NULL;
}
return gbuf;
}
static cbm_gbuf_t *run_sequential_with_lsp_cross(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count) {
return run_sequential_with_lsp_cross_and_mutator(project, repo_path, files, file_count, NULL,
NULL, false);
}
/* ── Run parallel pipeline on files, returning gbuf ───────────────── */
/* Spill mode for the harness: the run below opts its context in (the way
* run_parallel_pipeline does) and records how many results the store parked. */
static bool g_harness_spill = false;
static int64_t g_harness_parked = -1;
/* Opt-in: hand cbm_parallel_resolve the production Tier-2 C# registry. */
static bool g_harness_cs_registry = false;
static cbm_gbuf_t *run_parallel_with_extract_opts_and_mutator(
const char *project, const char *repo_path, cbm_file_info_t *files, int file_count,
int worker_count, const cbm_parallel_extract_opts_t *extract_opts,
parallel_result_mutator_fn mutator, void *mutator_ud, bool seed_structure) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
cbm_registry_t *reg = cbm_registry_new();
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
.spill_allowed = g_harness_spill,
};
if (seed_structure) {
seed_test_file_nodes(gbuf, project, files, file_count);
}
_Atomic int64_t shared_ids;
int64_t gbuf_next = cbm_gbuf_next_id(gbuf);
atomic_init(&shared_ids, gbuf_next);
CBMFileResult **result_cache = calloc((size_t)file_count, sizeof(CBMFileResult *));
cbm_init();
if (extract_opts) {
cbm_parallel_extract_ex(&ctx, files, file_count, result_cache, &shared_ids, worker_count,
extract_opts);
} else {
cbm_parallel_extract(&ctx, files, file_count, result_cache, &shared_ids, worker_count);
}
cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids));
if (g_harness_spill) {
int64_t bytes = 0;
int64_t loads = 0;
g_harness_parked = -1;
if (ctx.spill) {
cbm_result_spill_stats(ctx.spill, &g_harness_parked, &bytes, &loads);
}
}
if (mutator) {
mutator(result_cache, file_count, mutator_ud);
}
cbm_build_registry_from_cache(&ctx, files, file_count, result_cache);
int64_t registry_next = cbm_gbuf_next_id(gbuf);
if (registry_next > atomic_load(&shared_ids)) {
atomic_store(&shared_ids, registry_next);
}
/* Cross-file LSP — mirrors run_parallel_pipeline ordering in pipeline.c.
* Build the project-wide all_defs[] precondition, then feed it into
* cbm_parallel_resolve where the fused resolve_worker invokes
* cbm_pxc_run_one(_ts) per file BEFORE materializing CALLS edges. */
char **def_modules = (char **)calloc((size_t)file_count, sizeof(char *));
int def_count = 0;
CBMArena cross_arena;
cbm_arena_init(&cross_arena);
CBMLSPDef *all_defs =
def_modules ? cbm_pxc_collect_all_defs(&ctx, &cross_arena, result_cache, files, file_count,
ctx.project_name, def_modules, &def_count, NULL)
: NULL;
CBMModuleDefIndex *module_def_index =
all_defs ? cbm_pxc_build_module_def_index(all_defs, def_count) : NULL;
/* C# has no per-file cross path (cbm_pxc_run_one has no C# case): production
* resolves it only through the Tier-2 registry, so a harness run that opts
* in builds that one registry exactly as run_parallel_pipeline does. */
CBMCrossLspRegistries harness_registries = {0};
if (g_harness_cs_registry && all_defs) {
harness_registries.cs = cbm_cs_build_cross_registry(&cross_arena, all_defs, def_count);
}
cbm_parallel_resolve(&ctx, files, file_count, result_cache, &shared_ids, worker_count, all_defs,
def_count, def_modules, module_def_index,
g_harness_cs_registry ? &harness_registries
: NULL /* tests use per-file path */);
cbm_gbuf_set_next_id(gbuf, atomic_load(&shared_ids));
cbm_pxc_free_module_def_index(module_def_index);
free(all_defs);
cbm_arena_destroy(&cross_arena);
if (def_modules) {
for (int i = 0; i < file_count; i++) {
free(def_modules[i]);
}
free(def_modules);
}
for (int i = 0; i < file_count; i++)
if (result_cache[i])
cbm_free_result(result_cache[i]);
free(result_cache);
cbm_pipeline_spill_close(&ctx);
harness_ctx_free_tables(&ctx);
cbm_registry_free(reg);
/* cbm_parallel_extract installs a process-global package map whose
* production owner is the surrounding pipeline lifecycle. This direct
* pass harness owns that lifecycle itself, so mirror production teardown
* before returning the graph to the test. */
cbm_pkgmap_free(cbm_pipeline_get_pkgmap());
cbm_pipeline_set_pkgmap(NULL);
return gbuf;
}
static cbm_gbuf_t *run_parallel_with_extract_opts(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count,
int worker_count,
const cbm_parallel_extract_opts_t *extract_opts) {
return run_parallel_with_extract_opts_and_mutator(
project, repo_path, files, file_count, worker_count, extract_opts, NULL, NULL, false);
}
static cbm_gbuf_t *run_parallel(const char *project, const char *repo_path, cbm_file_info_t *files,
int file_count, int worker_count) {
return run_parallel_with_extract_opts(project, repo_path, files, file_count, worker_count,
NULL);
}
/* ── Parity Tests ─────────────────────────────────────────────────── */
static cbm_gbuf_t *g_seq_gbuf = NULL;
static cbm_gbuf_t *g_par_gbuf = NULL;
static int g_parity_setup_done = 0;
static int ensure_parity_setup(void) {
if (g_parity_setup_done)
return 0;
if (setup_parallel_repo() != 0)
return -1;
/* Discover files */
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
if (cbm_discover(g_par_tmpdir, &opts, &files, &file_count) != 0)
return -1;
const char *project = "par-test";
/* Build structure for both (need File/Folder nodes before definitions) */
/* For parity, we need the structure pass too. Let's just compare
* definition/call/usage/semantic edge counts. */
/* Run both modes */
g_seq_gbuf = run_sequential(project, g_par_tmpdir, files, file_count);
g_par_gbuf = run_parallel(project, g_par_tmpdir, files, file_count, 2);
cbm_discover_free(files, file_count);
g_parity_setup_done = 1;
return 0;
}
static void parity_teardown(void) {
if (g_seq_gbuf) {
cbm_gbuf_free(g_seq_gbuf);
g_seq_gbuf = NULL;
}
if (g_par_gbuf) {
cbm_gbuf_free(g_par_gbuf);
g_par_gbuf = NULL;
}
teardown_parallel_repo();
g_parity_setup_done = 0;
}
/* Node count parity */
TEST(parallel_node_count) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
int seq = cbm_gbuf_node_count(g_seq_gbuf);
int par = cbm_gbuf_node_count(g_par_gbuf);
ASSERT_GT(seq, 0);
ASSERT_EQ(seq, par);
PASS();
}
/* Edge type parity tests */
static int assert_edge_type_parity(const char *type) {
if (ensure_parity_setup() != 0)
return -1;
int seq = cbm_gbuf_edge_count_by_type(g_seq_gbuf, type);
int par = cbm_gbuf_edge_count_by_type(g_par_gbuf, type);
if (seq != par) {
printf(" FAIL: %s edges: seq=%d par=%d\n", type, seq, par);
return 1;
}
return 0;
}
TEST(parallel_calls_parity) {
int rc = assert_edge_type_parity("CALLS");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_defines_parity) {
int rc = assert_edge_type_parity("DEFINES");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_defines_method_parity) {
int rc = assert_edge_type_parity("DEFINES_METHOD");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_imports_parity) {
int rc = assert_edge_type_parity("IMPORTS");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_usage_parity) {
int rc = assert_edge_type_parity("USAGE");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_inherits_parity) {
int rc = assert_edge_type_parity("INHERITS");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
TEST(parallel_implements_parity) {
int rc = assert_edge_type_parity("IMPLEMENTS");
if (rc == -1)
FAIL("setup failed");
ASSERT_EQ(rc, 0);
PASS();
}
/* Absolute counts on the fixture — parity alone passes vacuously at 0==0,
* which is how the parallel-path IMPLEMENTS demotion shipped unnoticed. */
TEST(parallel_semantic_fixture_expected_counts) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
/* Circle implements Shape; Base extends Circle — in BOTH venues. */
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "IMPLEMENTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "IMPLEMENTS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "INHERITS"), 1);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "INHERITS"), 1);
/* Circle.area overrides Shape.area; Base.area overrides Circle.area. */
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_seq_gbuf, "OVERRIDE"), 2);
ASSERT_EQ(cbm_gbuf_edge_count_by_type(g_par_gbuf, "OVERRIDE"), 2);
PASS();
}
TEST(parallel_total_edges) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
int seq = cbm_gbuf_edge_count(g_seq_gbuf);
int par = cbm_gbuf_edge_count(g_par_gbuf);
ASSERT_GT(seq, 0);
ASSERT_EQ(seq, par);
PASS();
}
/* ── Spill mode: the graph is the in-memory graph ─────────────────── */
/* CBM_MEM_SPILL=1 parks every compacted result on disk the moment it is
* extracted; registry build, def collection, surfaces, resolve and the infra
* passes read each one back only for the moment they need it. The graph must
* not be able to tell: same nodes, same edges per type as the in-memory run
* of the same repo -- and every file must actually have gone through the
* store (a store that failed to open would silently test nothing). */
TEST(parallel_spill_mode_builds_the_same_graph) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
ASSERT_EQ(cbm_discover(g_par_tmpdir, &opts, &files, &file_count), 0);
ASSERT_GT(file_count, 0);
cbm_setenv("CBM_MEM_SPILL", "1", 1);
g_harness_spill = true;
cbm_gbuf_t *spilled = run_parallel("par-test", g_par_tmpdir, files, file_count, 2);
g_harness_spill = false;
cbm_unsetenv("CBM_MEM_SPILL");
cbm_discover_free(files, file_count);
ASSERT(spilled != NULL);
ASSERT_EQ((int)g_harness_parked, file_count);
ASSERT_EQ(cbm_gbuf_node_count(spilled), cbm_gbuf_node_count(g_par_gbuf));
ASSERT_EQ(cbm_gbuf_edge_count(spilled), cbm_gbuf_edge_count(g_par_gbuf));
static const char *const types[] = {"CALLS", "DEFINES", "DEFINES_METHOD", "IMPORTS",
"USES", "INHERITS", "IMPLEMENTS"};
for (size_t t = 0; t < sizeof(types) / sizeof(types[0]); t++) {
int in_memory = cbm_gbuf_edge_count_by_type(g_par_gbuf, types[t]);
int on_disk = cbm_gbuf_edge_count_by_type(spilled, types[t]);
if (in_memory != on_disk) {
printf(" FAIL: %s edges: in_memory=%d spilled=%d\n", types[t], in_memory, on_disk);
}
ASSERT_EQ(in_memory, on_disk);
}
cbm_gbuf_free(spilled);
PASS();
}
/* ── #2184: the post-extraction phases are charged before they run ─── */
/* Order-independent fingerprint of a whole graph: every node (label, QN, file,
* lines, properties) and every edge (type, source QN, target QN, properties)
* hashed and folded with a sum and an xor. Two graphs that differ in any field
* of any node or edge -- not just in a per-type count -- disagree here. */
typedef struct {
const cbm_gbuf_t *gb;
uint64_t sum;
uint64_t xr;
long count;
} graph_fp_t;
static uint64_t fp_mix(uint64_t h, const char *s) {
for (const unsigned char *p = (const unsigned char *)(s ? s : "\x01"); *p; p++) {
h = (h ^ *p) * 1099511628211ULL; /* FNV-1a */
}
return (h ^ 0xffU) * 1099511628211ULL; /* field separator */
}
static uint64_t fp_mix_int(uint64_t h, long long v) {
char buf[32];
snprintf(buf, sizeof(buf), "%lld", v);
return fp_mix(h, buf);
}
static void fp_fold(graph_fp_t *fp, uint64_t h) {
fp->sum += h;
fp->xr ^= h * 0x9E3779B97F4A7C15ULL;
fp->count++;
}
static void fp_visit_node(const cbm_gbuf_node_t *n, void *ud) {
uint64_t h = fp_mix(1469598103934665603ULL, "N");
h = fp_mix(h, n->label);
h = fp_mix(h, n->qualified_name);
h = fp_mix(h, n->file_path);
h = fp_mix_int(h, n->start_line);
h = fp_mix_int(h, n->end_line);
h = fp_mix(h, n->properties_json);
fp_fold(ud, h);
}
static void fp_visit_edge(const cbm_gbuf_edge_t *e, void *ud) {
graph_fp_t *fp = ud;
const cbm_gbuf_node_t *src = cbm_gbuf_find_by_id(fp->gb, e->source_id);
const cbm_gbuf_node_t *dst = cbm_gbuf_find_by_id(fp->gb, e->target_id);
uint64_t h = fp_mix(1469598103934665603ULL, "E");
h = fp_mix(h, e->type);
h = fp_mix(h, src ? src->qualified_name : NULL);
h = fp_mix(h, dst ? dst->qualified_name : NULL);
h = fp_mix(h, e->properties_json);
fp_fold(fp, h);
}
static graph_fp_t graph_fingerprint(const cbm_gbuf_t *gb) {
graph_fp_t fp = {.gb = gb};
cbm_gbuf_foreach_node(gb, fp_visit_node, &fp);
cbm_gbuf_foreach_edge(gb, fp_visit_edge, &fp);
return fp;
}
/* Mutator hook: runs between extraction and registry build, i.e. exactly where
* the post-extraction phases start. Records how many results are still held
* in memory there. */
static void count_cached_results(CBMFileResult **cache, int file_count, void *ud) {
int n = 0;
for (int i = 0; i < file_count; i++) {
n += cache[i] != NULL;
}
*(int *)ud = n;
}
/* #2184: spill was entered only DURING extraction, when the charge crossed
* budget - budget/16. A run that ended extraction just under that line kept
* every result in memory, and the phases that cannot spill (registry build,
* cross-LSP prepare, resolve) then grew the process past the budget -- openclaw
* at 8 workers, 4079 MB budget: 3678 MB at extraction end, 5829 MB in resolve.
* The extraction end now projects that growth from the result counts and
* spills first when budget - budget/16 would be crossed.
*
* The charge is pinned through the test seam (it is the process footprint
* otherwise): run A ends extraction ONE BYTE under the latch -- the extraction
* gate never fires, only the projection can spill; run B has the whole budget
* free -- the projection fits and nothing may spill. Both graphs must be
* identical, field for field, to each other and to the plain in-memory run. */
TEST(parallel_post_extract_projection_spills_before_resolve) {
if (ensure_parity_setup() != 0)
FAIL("setup failed");
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
ASSERT_EQ(cbm_discover(g_par_tmpdir, &opts, &files, &file_count), 0);
ASSERT_GT(file_count, 0);
const size_t saved_budget = cbm_mem_budget();
const size_t budget = (size_t)1024 * 1024 * 1024;
const size_t latch = budget - budget / 16; /* spill latches on charged > latch */
cbm_mem_set_budget_for_tests(budget);
g_harness_spill = true;
cbm_mem_set_charged_for_tests(latch);
int cached_near = -1;
cbm_gbuf_t *gb_near =
run_parallel_with_extract_opts_and_mutator("par-test", g_par_tmpdir, files, file_count, 2,
NULL, count_cached_results, &cached_near, false);
int64_t parked_near = g_harness_parked;
cbm_mem_set_charged_for_tests((size_t)1);
int cached_roomy = -1;
cbm_gbuf_t *roomy = run_parallel_with_extract_opts_and_mutator(
"par-test", g_par_tmpdir, files, file_count, 2, NULL, count_cached_results, &cached_roomy,
false);
int64_t parked_roomy = g_harness_parked;
cbm_mem_set_charged_for_tests(0);
g_harness_spill = false;
cbm_mem_set_budget_for_tests(saved_budget);
cbm_discover_free(files, file_count);
ASSERT(gb_near != NULL);
ASSERT(roomy != NULL);
/* A: every result went to disk before registry build. */
ASSERT_EQ((int)parked_near, file_count);
ASSERT_EQ(cached_near, 0);
/* B: the projection fits, so no store was opened and results stay cached. */
ASSERT_EQ((int)parked_roomy, -1);
ASSERT_GT(cached_roomy, 0);
graph_fp_t fp_near = graph_fingerprint(gb_near);
graph_fp_t fp_roomy = graph_fingerprint(roomy);
graph_fp_t fp_mem = graph_fingerprint(g_par_gbuf);
ASSERT_GT(fp_mem.count, 0);
ASSERT_EQ(fp_near.count, fp_mem.count);
ASSERT_EQ(fp_roomy.count, fp_mem.count);
ASSERT_TRUE(fp_near.sum == fp_mem.sum && fp_near.xr == fp_mem.xr);
ASSERT_TRUE(fp_roomy.sum == fp_mem.sum && fp_roomy.xr == fp_mem.xr);
cbm_gbuf_free(gb_near);
cbm_gbuf_free(roomy);
PASS();
}
/* ── Empty file list ──────────────────────────────────────────────── */
TEST(parallel_empty_files) {
cbm_gbuf_t *gbuf = cbm_gbuf_new("empty-proj", "/tmp");
cbm_registry_t *reg = cbm_registry_new();
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = "empty-proj",
.repo_path = "/tmp",
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
};
_Atomic int64_t shared_ids;
atomic_init(&shared_ids, 1);
CBMFileResult **cache = NULL;
int rc = cbm_parallel_extract(&ctx, NULL, 0, cache, &shared_ids, 2);
ASSERT_EQ(rc, 0);
ASSERT_EQ(cbm_gbuf_node_count(gbuf), 0);
cbm_registry_free(reg);
cbm_gbuf_free(gbuf);
PASS();
}
/* ── Regression: args JSON must not overflow the props buffer ──────── */
/* A call with many long string arguments makes append_args_json()'s running
* position exceed the fixed CBM_SZ_2K `props` stack buffer in
* emit_normal_calls_edge(): format_call_arg() returns snprintf's UNtruncated
* length, so pos += n could run past the buffer and the trailing
* buf[pos]='\0' wrote out of bounds (stack-buffer-overflow; caught by the
* stack canary as a SIGABRT on real repos). This indexes a fixture whose
* single call carries enough long args to drive pos past 2 KB; under the
* ASan test build a regression aborts here. */
TEST(parallel_args_json_no_overflow) {
char dir[256];
snprintf(dir, sizeof(dir), "/tmp/cbm_argov_XXXXXX");
ASSERT_TRUE(cbm_mkdtemp(dir) != NULL);
char path[512];
snprintf(path, sizeof(path), "%s/app.ts", dir);
FILE *f = fopen(path, "w");
ASSERT_TRUE(f != NULL);
fputs("function sink(...xs: string[]) { return xs; }\n", f);
fputs("function caller() {\n sink(\n", f);
for (int i = 0; i < 60; i++) {
/* 100-char string literal per arg; 60 args => args JSON well past the
* 2 KB props buffer, forcing the pre-fix overshoot. */
fputs(" \"", f);
for (int j = 0; j < 100; j++)
fputc('a' + (i % 26), f);
fputs(i < 59 ? "\",\n" : "\"\n", f);
}
fputs(" );\n}\n", f);
fclose(f);
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
ASSERT_EQ(cbm_discover(dir, &opts, &files, &file_count), 0);
ASSERT_GT(file_count, 0);
cbm_gbuf_t *gbuf = run_parallel("argov-test", dir, files, file_count, 4);
ASSERT_TRUE(gbuf != NULL);
ASSERT_GT(cbm_gbuf_edge_count(gbuf), 0);
cbm_gbuf_free(gbuf);
cbm_discover_free(files, file_count);
th_rmtree(dir);
PASS();
}
/* F14: a definition's properties buffer was sized for its docstring and variants
* only. A wide signature (50 annotated parameters) filled the rest of the 2 KB, and
* the MinHash fingerprint "fp", structural profile "sp" and body tokens "bt"
* appended behind it were dropped whole: no SIMILAR_TO edge and fewer
* SEMANTICALLY_RELATED edges for exactly the widest definitions (jellyfin's
* ImageController actions, 2026-10-18). `small` has the same body and is the
* control that the fixture produces all three at all. Both resolvers. */
static const char f14_body[] =
" total = 0\n"
" for item in range(10):\n"
" if item % 3 == 0:\n"
" total += item * 2\n"
" elif item % 5 == 1:\n"
" total -= len(str(item))\n"
" else:\n"
" total ^= hash((item, total))\n"
" values = [x * x for x in range(total % 7 + 3)]\n"
" mapping = {str(k): v for k, v in enumerate(values)}\n"
" while total > 100:\n"
" total //= 3\n"
" try:\n"
" result = sorted(mapping.items(), key=lambda kv: kv[1])\n"
" except ValueError as err:\n"
" result = [(\"error\", str(err))]\n"
" return result, total, mapping\n";
static bool f14_props_have(const cbm_gbuf_t *gb, const char *project, const char *name,
const char *venue) {
char *qn = cbm_pipeline_fqn_compute(project, "pkg/wide.py", name);
const cbm_gbuf_node_t *n = qn ? cbm_gbuf_find_by_qn(gb, qn) : NULL;
free(qn);
const char *p = n && n->properties_json ? n->properties_json : "";
bool fp = strstr(p, "\"fp\":\"") != NULL;
bool sp = strstr(p, "\"sp\":\"") != NULL;
bool bt = strstr(p, "\"bt\":\"") != NULL;
bool sig = strstr(p, "\"signature\":\"") != NULL;
printf(" f14 %s %s: found=%d len=%zu fp=%d sp=%d bt=%d signature=%d\n", venue, name, n != NULL,
strlen(p), fp, sp, bt, sig);
return n && fp && sp && bt && sig;
}
TEST(parallel_wide_definition_keeps_fp_sp_bt_f14) {
char params[CBM_SZ_4K];
size_t pos = 0;
for (int i = 0; i < 50; i++) {
pos += (size_t)snprintf(params + pos, sizeof(params) - pos, "%sp%02d: Dict[str, int] = None",
i ? ", " : "", i);
}
char src[CBM_SZ_8K];
snprintf(src, sizeof(src), "def small(a, b):\n%s\n\ndef wide(%s):\n%s", f14_body, params,
f14_body);
const char *project = "cbm_f14";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_f14_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/pkg/wide.py", tmpdir);
if (th_write_file(path, src) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write the F14 fixture");
}
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"pkg/wide.py";
files[0].language = CBM_LANG_PYTHON;
cbm_gbuf_t *graphs[2] = {run_sequential(project, tmpdir, files, 1),
run_parallel(project, tmpdir, files, 1, 2)};
ASSERT_NOT_NULL(graphs[0]);
ASSERT_NOT_NULL(graphs[1]);
bool all = true;
for (int g = 0; g < 2; g++) {
const char *venue = g == 0 ? "sequential" : "parallel";
bool control = f14_props_have(graphs[g], project, "small", venue);
bool wide = f14_props_have(graphs[g], project, "wide", venue);
all = all && control && wide;
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
ASSERT_TRUE(all);
PASS();
}
/* ── Graph buffer merge tests ─────────────────────────────────────── */
TEST(gbuf_shared_ids_unique) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *ga = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_t *gb = cbm_gbuf_new_shared_ids("proj", "/", &shared);
int64_t id1 = cbm_gbuf_upsert_node(ga, "Function", "foo", "proj.foo", "a.go", 1, 5, "{}");
int64_t id2 = cbm_gbuf_upsert_node(gb, "Function", "bar", "proj.bar", "b.go", 1, 3, "{}");
ASSERT_GT(id1, 0);
ASSERT_GT(id2, 0);
ASSERT_NEQ(id1, id2);
cbm_gbuf_free(ga);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_merge_nodes) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}");
cbm_gbuf_upsert_node(dst, "Function", "b", "proj.b", "a.go", 6, 10, "{}");
cbm_gbuf_upsert_node(src, "Function", "c", "proj.c", "b.go", 1, 5, "{}");
cbm_gbuf_upsert_node(src, "Function", "d", "proj.d", "b.go", 6, 10, "{}");
ASSERT_EQ(cbm_gbuf_node_count(dst), 2);
cbm_gbuf_merge(dst, src);
ASSERT_EQ(cbm_gbuf_node_count(dst), 4);
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.c"));
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.d"));
/* dst originals still there */
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.a"));
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.b"));
cbm_gbuf_free(src);
cbm_gbuf_free(dst);
PASS();
}
TEST(gbuf_merge_edges) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared);
int64_t a = cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}");
int64_t b = cbm_gbuf_upsert_node(dst, "Function", "b", "proj.b", "a.go", 6, 10, "{}");
/* Put an edge in src that references dst nodes (by ID) */
cbm_gbuf_insert_edge(src, a, b, "CALLS", "{}");
cbm_gbuf_merge(dst, src);
ASSERT_GT(cbm_gbuf_edge_count(dst), 0);
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
cbm_gbuf_find_edges_by_source_type(dst, a, "CALLS", &edges, &count);
ASSERT_EQ(count, 1);
ASSERT_EQ(edges[0]->target_id, b);
cbm_gbuf_free(src);
cbm_gbuf_free(dst);
PASS();
}
TEST(gbuf_merge_empty_src) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_upsert_node(dst, "Function", "a", "proj.a", "a.go", 1, 5, "{}");
int before = cbm_gbuf_node_count(dst);
cbm_gbuf_merge(dst, src);
ASSERT_EQ(cbm_gbuf_node_count(dst), before);
cbm_gbuf_free(src);
cbm_gbuf_free(dst);
PASS();
}
TEST(gbuf_merge_src_free_safe) {
_Atomic int64_t shared = 1;
cbm_gbuf_t *dst = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_t *src = cbm_gbuf_new_shared_ids("proj", "/", &shared);
cbm_gbuf_upsert_node(src, "Function", "x", "proj.x", "x.go", 1, 5, "{}");
cbm_gbuf_merge(dst, src);
cbm_gbuf_free(src); /* must not crash */
/* dst node still accessible */
ASSERT_NOT_NULL(cbm_gbuf_find_by_qn(dst, "proj.x"));
cbm_gbuf_free(dst);
PASS();
}
TEST(gbuf_next_id_accessors) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/");
ASSERT_EQ(cbm_gbuf_next_id(gb), 1);
cbm_gbuf_upsert_node(gb, "Function", "foo", "proj.foo", "f.go", 1, 5, "{}");
ASSERT_GT(cbm_gbuf_next_id(gb), 1);
cbm_gbuf_set_next_id(gb, 100);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "bar", "proj.bar", "f.go", 6, 10, "{}");
ASSERT_GTE(id, 100);
cbm_gbuf_free(gb);
PASS();
}
TEST(gbuf_raise_next_id_never_lowers) {
cbm_gbuf_t *gb = cbm_gbuf_new("proj", "/");
cbm_gbuf_raise_next_id(gb, 100);
ASSERT_EQ(cbm_gbuf_next_id(gb), 100);
cbm_gbuf_raise_next_id(gb, 40);
ASSERT_EQ(cbm_gbuf_next_id(gb), 100);
int64_t id = cbm_gbuf_upsert_node(gb, "Function", "foo", "proj.foo", "f.go", 1, 5, "{}");
ASSERT_EQ(id, 100);
cbm_gbuf_free(gb);
PASS();
}
/* Edge ids in the main buffer stay unique through the parallel phases.
* Merged worker nodes keep their shared ids, and merged edges take fresh ids
* from the main counter after the highest of them, so the main counter can
* end above the shared one (12 of 17 infra corpora, deckhouse by 229,233).
* The sync after each parallel phase used to set the main counter to the
* shared value anyway, and later passes reused the merged edges' ids. Where
* the counter ends depends on worker timing, so the seam pins it: right before
* each sync the hook takes ID_SYNC_PUSH edge ids from the main buffer. */
enum { ID_FILES = 64, ID_SYNC_PUSH = 3 };
typedef struct {
bool push;
int syncs;
} id_sync_probe_t;
static void id_sync_hook(cbm_gbuf_t *gb, void *ctx) {
id_sync_probe_t *probe = ctx;
probe->syncs++;
const cbm_gbuf_node_t **fns = NULL;
int n = 0;
if (!probe->push || cbm_gbuf_find_by_label(gb, "Function", &fns, &n) != 0 || n < 2) {
return;
}
char props[CBM_SZ_64];
for (int i = 0; i < ID_SYNC_PUSH; i++) {
snprintf(props, sizeof(props), "{\"id_sync_probe\":%d}", probe->syncs * ID_SYNC_PUSH + i);
cbm_gbuf_insert_edge(gb, fns[0]->id, fns[1]->id, "CALLS", props);
}
}
typedef struct {
int edges;
int duplicates;
} edge_id_probe_t;
static void probe_edge_ids(const cbm_gbuf_t *gb, void *ctx) {
edge_id_probe_t *probe = ctx;
probe->edges = cbm_gbuf_edge_count(gb);
probe->duplicates = cbm_gbuf_duplicate_edge_ids(gb);
}
/* `count` Go files; each function calls the next file's. */
static bool write_id_fixture(const char *repo, int count) {
char path[512];
char source[256];
for (int i = 0; i < count; i++) {
snprintf(path, sizeof(path), "%s/f%02d.go", repo, i);
snprintf(source, sizeof(source),
"package main\n\nfunc F%02d() int {\n\treturn F%02d() + 1\n}\n", i,
(i + 1) % count);
if (th_write_file(path, source) != 0) {
return false;
}
}
return true;
}
TEST(parallel_full_index_keeps_edge_ids_unique) {
char repo[256];
snprintf(repo, sizeof(repo), "/tmp/cbm_par_ids_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(repo));
/* More files than MIN_FILES_FOR_PARALLEL (50): the parallel route. */
if (!write_id_fixture(repo, ID_FILES)) {
th_rmtree(repo);
FAIL("failed to write the edge-id fixture");
}
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/ids.db", repo);
char *old_workers = getenv("CBM_WORKERS");
char *saved_workers = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
id_sync_probe_t syncs = {.push = true};
edge_id_probe_t probe = {0};
cbm_pipeline_set_id_sync_hook_for_tests(id_sync_hook, &syncs);
cbm_pipeline_t *p = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL);
int rc = CBM_NOT_FOUND;
if (p) {
cbm_pipeline_set_before_dump_hook_for_tests(p, probe_edge_ids, &probe);
rc = cbm_pipeline_run(p);
cbm_pipeline_free(p);
}
cbm_pipeline_set_id_sync_hook_for_tests(NULL, NULL);
if (saved_workers) {
cbm_setenv("CBM_WORKERS", saved_workers, 1);
free(saved_workers);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(repo);
ASSERT_EQ(rc, 0);
/* Both full-route syncs (after extract, after resolve) go through the helper. */
ASSERT_EQ(syncs.syncs, 2);
ASSERT_GT(probe.edges, ID_FILES);
ASSERT_EQ(probe.duplicates, 0);
PASS();
}
/* The closure-repair route always runs the parallel phases; both of its
* syncs go through the same helper. */
TEST(parallel_closure_repair_syncs_ids_through_the_helper) {
char repo[256];
snprintf(repo, sizeof(repo), "/tmp/cbm_par_ids_XXXXXX");
ASSERT_NOT_NULL(cbm_mkdtemp(repo));
if (!write_id_fixture(repo, 3)) {
th_rmtree(repo);
FAIL("failed to write the edge-id fixture");
}
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/ids.db", repo);
cbm_pipeline_incremental_test_reset_faults();
cbm_pipeline_t *first = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL);
int first_rc = first ? cbm_pipeline_run(first) : CBM_NOT_FOUND;
cbm_pipeline_free(first);
/* A body-only edit (no new names) routes to closure repair. */
char path[512];
snprintf(path, sizeof(path), "%s/f01.go", repo);
int write_rc =
th_write_file(path, "package main\n\nfunc F01() int {\n\treturn F02() + 40 + 2\n}\n");
id_sync_probe_t syncs = {.push = false};
cbm_pipeline_set_id_sync_hook_for_tests(id_sync_hook, &syncs);
cbm_pipeline_incremental_test_reset_faults();
cbm_pipeline_t *second = cbm_pipeline_new(repo, db_path, CBM_MODE_FULL);
int second_rc = second ? cbm_pipeline_run(second) : CBM_NOT_FOUND;
cbm_incremental_route_t route = cbm_pipeline_incremental_test_last_route();
cbm_pipeline_free(second);
cbm_pipeline_set_id_sync_hook_for_tests(NULL, NULL);
th_rmtree(repo);
ASSERT_EQ(first_rc, 0);
ASSERT_EQ(write_rc, 0);
ASSERT_EQ(second_rc, 0);
ASSERT_EQ(route, CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR);
ASSERT_EQ(syncs.syncs, 2);
PASS();
}
/* ── Parallel-pipeline LSP-override regression ────────────────────── */
/* Pin the wiring fix that unified pass_calls.c (sequential) and
* pass_parallel.c (parallel) on cbm_pipeline_find_lsp_resolution +
* CBM_LSP_CONFIDENCE_FLOOR (lsp_resolve.h). Before the unification, the
* parallel path carried its own lsp_override_resolution_pp at floor 0.5
* while the sequential path used find_lsp_resolution at floor 0.6, so a
* project produced different CALLS edge attributions depending on which
* pipeline mode kicked in. This test indexes a small Python repo via
* the parallel pipeline and asserts at least one resulting CALLS edge
* carries an "lsp_*" strategy — proof the parallel path consults
* result->resolved_calls and emits LSP-attributed edges. */
typedef struct {
int lsp_strategy_count;
int total_calls;
} lsp_edge_count_ctx_t;
static void count_lsp_call_edges(const cbm_gbuf_edge_t *edge, void *ud) {
lsp_edge_count_ctx_t *c = ud;
if (!edge || !edge->type || strcmp(edge->type, "CALLS") != 0) {
return;
}
c->total_calls++;
if (edge->properties_json && strstr(edge->properties_json, "\"strategy\":\"lsp")) {
c->lsp_strategy_count++;
}
}
static const char *class_method_tail(const char *qn) {
if (!qn) {
return NULL;
}
const char *last = strrchr(qn, '.');
if (!last || last == qn) {
return NULL;
}
const char *second = last;
while (second > qn) {
second--;
if (*second == '.') {
return second == qn ? qn : second + 1;
}
}
return qn;
}
static const cbm_gbuf_node_t *find_unique_callable_node_by_tail(const cbm_gbuf_t *gbuf,
const char *tail) {
const char *method = tail ? strrchr(tail, '.') : NULL;
method = method ? method + 1 : tail;
if (!gbuf || !tail || !method) {
return NULL;
}
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_name(gbuf, method, &nodes, &count) != 0) {
return NULL;
}
const cbm_gbuf_node_t *match = NULL;
for (int i = 0; i < count; i++) {
const cbm_gbuf_node_t *node = nodes[i];
if (!node || !node->label || !node->qualified_name) {
continue;
}
if (strcmp(node->label, "Method") != 0 && strcmp(node->label, "Function") != 0) {
continue;
}
const char *node_tail = class_method_tail(node->qualified_name);
if (!node_tail || strcmp(node_tail, tail) != 0) {
continue;
}
if (match) {
return NULL;
}
match = node;
}
return match;
}
static bool qn_has_segment_suffix(const char *qualified_name, const char *suffix) {
if (!qualified_name || !suffix) {
return false;
}
size_t qualified_len = strlen(qualified_name);
size_t suffix_len = strlen(suffix);
if (suffix_len > qualified_len ||
strcmp(qualified_name + qualified_len - suffix_len, suffix) != 0) {
return false;
}
return qualified_len == suffix_len || qualified_name[qualified_len - suffix_len - 1] == '.';
}
static const cbm_gbuf_node_t *find_unique_node_by_name_label_qn_suffix(const cbm_gbuf_t *gbuf,
const char *name,
const char *label,
const char *qn_suffix) {
if (!gbuf || !name || !label || !qn_suffix) {
return NULL;
}
const cbm_gbuf_node_t **nodes = NULL;
int count = 0;
if (cbm_gbuf_find_by_name(gbuf, name, &nodes, &count) != 0) {
return NULL;
}
const cbm_gbuf_node_t *match = NULL;
for (int i = 0; i < count; i++) {
const cbm_gbuf_node_t *node = nodes[i];
if (!node || !node->label || !node->qualified_name || strcmp(node->label, label) != 0 ||
!qn_has_segment_suffix(node->qualified_name, qn_suffix)) {
continue;
}
if (match) {
return NULL;
}
match = node;
}
return match;
}
static int count_edges_between_nodes(const cbm_gbuf_t *gbuf, const cbm_gbuf_node_t *source,
const cbm_gbuf_node_t *target, const char *edge_type) {
if (!gbuf || !source || !target || !edge_type) {
return -1;
}
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, edge_type, &edges, &count) != 0) {
return -1;
}
int matches = 0;
for (int i = 0; i < count; i++) {
if (edges[i] && edges[i]->target_id == target->id) {
matches++;
}
}
return matches;
}
static bool has_edge_from_callable_to_node(const cbm_gbuf_t *gbuf, const char *source_tail,
const cbm_gbuf_node_t *target, const char *edge_type) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail);
if (!source || !target || !edge_type) {
return false;
}
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, edge_type, &edges, &count) != 0) {
return false;
}
for (int i = 0; i < count; i++) {
if (edges[i] && edges[i]->target_id == target->id) {
return true;
}
}
return false;
}
static const cbm_gbuf_edge_t *find_calls_edge_by_tails(const cbm_gbuf_t *gbuf,
const char *source_tail,
const char *target_tail) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail);
const cbm_gbuf_node_t *target = find_unique_callable_node_by_tail(gbuf, target_tail);
if (!source || !target) {
return NULL;
}
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, "CALLS", &edges, &count) != 0) {
return NULL;
}
for (int i = 0; i < count; i++) {
if (edges[i] && edges[i]->target_id == target->id) {
return edges[i];
}
}
return NULL;
}
static bool has_edge_from_callable_to_qn(const cbm_gbuf_t *gbuf, const char *source_tail,
const char *target_qn, const char *edge_type) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail);
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_qn(gbuf, target_qn);
if (!source || !target)
return false;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, edge_type, &edges, &count) != 0)
return false;
for (int i = 0; i < count; i++) {
if (edges[i] && edges[i]->target_id == target->id)
return true;
}
return false;
}
static bool callable_has_call_target_fragment(const cbm_gbuf_t *gbuf, const char *source_tail,
const char *target_fragment) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail);
if (!source || !target_fragment)
return false;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, "CALLS", &edges, &count) != 0)
return false;
for (int i = 0; i < count; i++) {
const cbm_gbuf_node_t *target =
edges[i] ? cbm_gbuf_find_by_id(gbuf, edges[i]->target_id) : NULL;
if (target && target->qualified_name && strstr(target->qualified_name, target_fragment))
return true;
}
return false;
}
static const cbm_gbuf_edge_t *find_call_edge_to_target_fragment(const cbm_gbuf_t *gbuf,
const char *source_tail,
const char *target_fragment) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gbuf, source_tail);
if (!source || !target_fragment)
return NULL;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gbuf, source->id, "CALLS", &edges, &count) != 0)
return NULL;
for (int i = 0; i < count; i++) {
const cbm_gbuf_node_t *target =
edges[i] ? cbm_gbuf_find_by_id(gbuf, edges[i]->target_id) : NULL;
if (target && target->qualified_name && strstr(target->qualified_name, target_fragment))
return edges[i];
}
return NULL;
}
static int count_calls_edges_to_tail(const cbm_gbuf_t *gbuf, const char *target_tail) {
const cbm_gbuf_node_t *target = find_unique_callable_node_by_tail(gbuf, target_tail);
if (!target)
return -1;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_target_type(gbuf, target->id, "CALLS", &edges, &count) != 0)
return -1;
return count;
}
typedef struct {
bool injected;
} lsp_legacy_injection_t;
/* Test-only cache mutation: preserve the real exact Alpha.render semantic
* record, then append a higher-confidence legacy 0:0 record for Beta.render.
* The shared linear matcher and the compact parallel index must both prefer
* the exact occurrence. */
static void inject_higher_confidence_legacy_render(CBMFileResult **result_cache, int file_count,
void *ud) {
lsp_legacy_injection_t *state = (lsp_legacy_injection_t *)ud;
if (!state) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache ? result_cache[file] : NULL;
if (!result) {
continue;
}
const CBMResolvedCall *exact = NULL;
const char *beta_qn = NULL;
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *candidate = &result->resolved_calls.items[i];
if (candidate->kind == CBM_RESOLVED_INVOCATION && candidate->caller_qn &&
strstr(candidate->caller_qn, "Caller.run") && candidate->callee_qn &&
strstr(candidate->callee_qn, "Alpha.render") &&
candidate->site_end_byte > candidate->site_start_byte) {
exact = candidate;
break;
}
}
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->qualified_name && strstr(definition->qualified_name, "Beta.render")) {
beta_qn = definition->qualified_name;
break;
}
}
if (!exact || !beta_qn) {
continue;
}
CBMResolvedCall legacy = *exact;
legacy.callee_qn = beta_qn;
legacy.strategy = "test_legacy_zero_span";
legacy.confidence = 0.99f;
legacy.site_start_byte = 0;
legacy.site_end_byte = 0;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, legacy);
state->injected = true;
return;
}
}
TEST(parallel_lsp_index_exact_site_beats_legacy_record_in_graph) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_lsp_site_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
fprintf(file, "class Alpha:\n"
" def render(self):\n"
" return 'alpha'\n"
"\n"
"class Beta:\n"
" def render(self):\n"
" return 'beta'\n"
"\n"
"class Caller:\n"
" def run(self):\n"
" value = Alpha()\n"
" return value.render()\n");
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"app.py";
files[0].language = CBM_LANG_PYTHON;
lsp_legacy_injection_t injection = {0};
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_par_lsp_site", tmpdir, files, 1, 1, NULL, inject_higher_confidence_legacy_render,
&injection, false);
ASSERT_NOT_NULL(gbuf);
ASSERT_TRUE(injection.injected);
const cbm_gbuf_edge_t *exact = find_calls_edge_by_tails(gbuf, "Caller.run", "Alpha.render");
const cbm_gbuf_edge_t *wrong = find_calls_edge_by_tails(gbuf, "Caller.run", "Beta.render");
if (!exact || wrong) {
printf(" lsp index occurrence diagnostic: exact=%s legacy_wrong=%s\n",
exact ? "present" : "absent", wrong ? "present" : "absent");
}
ASSERT_NOT_NULL(exact);
ASSERT_NULL(wrong);
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
PASS();
}
typedef struct {
bool injected;
} lsp_exact_ambiguity_injection_t;
/* Append a distinct semantic target at the same exact source occurrence and
* make the carrier semantic-only. Both the shared matcher and the compact
* parallel index must fail closed; neither confidence nor textual registry
* fallback may select one target. */
static void inject_distinct_exact_render_target(CBMFileResult **result_cache, int file_count,
void *ud) {
lsp_exact_ambiguity_injection_t *state = (lsp_exact_ambiguity_injection_t *)ud;
if (!state) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache ? result_cache[file] : NULL;
if (!result) {
continue;
}
CBMCall *carrier = NULL;
const CBMResolvedCall *exact = NULL;
const char *beta_qn = NULL;
for (int i = 0; i < result->calls.count; i++) {
CBMCall *candidate = &result->calls.items[i];
if (candidate->callee_name && candidate->enclosing_func_qn &&
strstr(candidate->enclosing_func_qn, "Caller.run") &&
strcmp(cbm_pipeline_call_callee_leaf(candidate->callee_name), "render") == 0 &&
cbm_pipeline_source_site_present(candidate->site_start_byte,
candidate->site_end_byte)) {
carrier = candidate;
break;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *candidate = &result->resolved_calls.items[i];
if (candidate->kind == CBM_RESOLVED_INVOCATION && candidate->caller_qn &&
strstr(candidate->caller_qn, "Caller.run") && candidate->callee_qn &&
strstr(candidate->callee_qn, "Alpha.render") &&
cbm_pipeline_source_site_present(candidate->site_start_byte,
candidate->site_end_byte)) {
exact = candidate;
break;
}
}
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->qualified_name && strstr(definition->qualified_name, "Beta.render")) {
beta_qn = definition->qualified_name;
break;
}
}
if (!carrier || !exact || !beta_qn ||
!cbm_pipeline_source_site_eq(carrier->site_start_byte, carrier->site_end_byte,
exact->site_start_byte, exact->site_end_byte)) {
continue;
}
carrier->requires_lsp_resolution = true;
CBMResolvedCall ambiguous = *exact;
ambiguous.callee_qn = beta_qn;
ambiguous.strategy = "test_distinct_exact_target";
ambiguous.confidence = 0.99f;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, ambiguous);
state->injected = true;
return;
}
}
TEST(parallel_lsp_index_distinct_exact_targets_fail_closed_in_graph) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_lsp_ambiguous_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
fprintf(file, "class Alpha:\n"
" def render(self): return 'alpha'\n"
"class Beta:\n"
" def render(self): return 'beta'\n"
"class Caller:\n"
" def run(self):\n"
" value = Alpha()\n"
" return value.render()\n");
fclose(file);
cbm_file_info_t file_info = {0};
file_info.path = path;
file_info.rel_path = (char *)"app.py";
file_info.language = CBM_LANG_PYTHON;
lsp_exact_ambiguity_injection_t injection = {0};
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_par_lsp_ambiguous", tmpdir, &file_info, 1, 1, NULL,
inject_distinct_exact_render_target, &injection, false);
ASSERT_NOT_NULL(gbuf);
ASSERT_TRUE(injection.injected);
ASSERT_NOT_NULL(find_unique_callable_node_by_tail(gbuf, "Alpha.render"));
ASSERT_NOT_NULL(find_unique_callable_node_by_tail(gbuf, "Beta.render"));
const cbm_gbuf_edge_t *alpha = find_calls_edge_by_tails(gbuf, "Caller.run", "Alpha.render");
const cbm_gbuf_edge_t *beta = find_calls_edge_by_tails(gbuf, "Caller.run", "Beta.render");
if (alpha || beta) {
printf(" lsp exact ambiguity diagnostic: alpha=%s beta=%s\n", alpha ? "present" : "absent",
beta ? "present" : "absent");
}
ASSERT_NULL(alpha);
ASSERT_NULL(beta);
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
PASS();
}
typedef struct {
lsp_exact_ambiguity_injection_t ambiguity;
bool legacy_injected;
bool carrier_allows_fallback;
bool shared_matcher_failed_closed;
} lsp_exact_ambiguity_legacy_probe_t;
/* Reuse the exact-ambiguity injection above, then add a unique 0:0 row while
* leaving the carrier eligible for legacy resolution. The authoritative
* matcher must keep the exact-site ambiguity authoritative; the compact
* parallel index must not reinterpret its tombstone as "try legacy". */
static void inject_legacy_after_exact_render_ambiguity(CBMFileResult **result_cache, int file_count,
void *ud) {
lsp_exact_ambiguity_legacy_probe_t *probe = (lsp_exact_ambiguity_legacy_probe_t *)ud;
if (!probe) {
return;
}
inject_distinct_exact_render_target(result_cache, file_count, &probe->ambiguity);
if (!probe->ambiguity.injected) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache ? result_cache[file] : NULL;
if (!result) {
continue;
}
CBMCall *carrier = NULL;
const CBMResolvedCall *exact = NULL;
const char *legacy_qn = NULL;
for (int i = 0; i < result->calls.count; i++) {
CBMCall *candidate = &result->calls.items[i];
if (candidate->callee_name && candidate->enclosing_func_qn &&
strstr(candidate->enclosing_func_qn, "Caller.run") &&
strcmp(cbm_pipeline_call_callee_leaf(candidate->callee_name), "render") == 0 &&
cbm_pipeline_source_site_present(candidate->site_start_byte,
candidate->site_end_byte)) {
carrier = candidate;
break;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
const CBMResolvedCall *candidate = &result->resolved_calls.items[i];
if (candidate->kind == CBM_RESOLVED_INVOCATION && candidate->caller_qn &&
strstr(candidate->caller_qn, "Caller.run") && candidate->callee_qn &&
strstr(candidate->callee_qn, "Alpha.render") &&
cbm_pipeline_source_site_present(candidate->site_start_byte,
candidate->site_end_byte)) {
exact = candidate;
break;
}
}
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->qualified_name && strstr(definition->qualified_name, "Legacy.render")) {
legacy_qn = definition->qualified_name;
break;
}
}
if (!carrier || !exact || !legacy_qn ||
!cbm_pipeline_source_site_eq(carrier->site_start_byte, carrier->site_end_byte,
exact->site_start_byte, exact->site_end_byte)) {
continue;
}
carrier->requires_lsp_resolution = false;
CBMResolvedCall legacy = *exact;
legacy.callee_qn = legacy_qn;
legacy.strategy = "test_legacy_after_exact_ambiguity";
legacy.confidence = 0.99f;
legacy.site_start_byte = 0;
legacy.site_end_byte = 0;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, legacy);
probe->legacy_injected = true;
probe->carrier_allows_fallback = !carrier->requires_lsp_resolution;
probe->shared_matcher_failed_closed =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false) == NULL;
return;
}
}
TEST(parallel_lsp_index_exact_ambiguity_does_not_fall_through_to_legacy) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_lsp_ambiguous_legacy_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
fprintf(file, "class Alpha:\n"
" def render(self): return 'alpha'\n"
"class Beta:\n"
" def render(self): return 'beta'\n"
"class Legacy:\n"
" def render(self): return 'legacy'\n"
"class Caller:\n"
" def run(self):\n"
" value = Alpha()\n"
" return value.render()\n");
fclose(file);
cbm_file_info_t file_info = {0};
file_info.path = path;
file_info.rel_path = (char *)"app.py";
file_info.language = CBM_LANG_PYTHON;
lsp_exact_ambiguity_legacy_probe_t probe = {0};
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_par_lsp_ambiguous_legacy", tmpdir, &file_info, 1, 1, NULL,
inject_legacy_after_exact_render_ambiguity, &probe, false);
ASSERT_NOT_NULL(gbuf);
int alpha_edges = count_calls_edges_to_tail(gbuf, "Alpha.render");
int beta_edges = count_calls_edges_to_tail(gbuf, "Beta.render");
int legacy_edges = count_calls_edges_to_tail(gbuf, "Legacy.render");
const cbm_gbuf_edge_t *legacy_edge =
find_calls_edge_by_tails(gbuf, "Caller.run", "Legacy.render");
if (!probe.ambiguity.injected || !probe.legacy_injected || !probe.carrier_allows_fallback ||
!probe.shared_matcher_failed_closed || alpha_edges != 0 || beta_edges != 0 ||
legacy_edges != 0) {
printf(" exact ambiguity + legacy diagnostic: exact_ambiguity=%d legacy=%d "
"fallback_allowed=%d shared_failed_closed=%d alpha=%d beta=%d legacy_edge=%d "
"props=%s\n",
probe.ambiguity.injected, probe.legacy_injected, probe.carrier_allows_fallback,
probe.shared_matcher_failed_closed, alpha_edges, beta_edges, legacy_edges,
legacy_edge && legacy_edge->properties_json ? legacy_edge->properties_json : "");
}
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(probe.ambiguity.injected);
ASSERT_TRUE(probe.legacy_injected);
ASSERT_TRUE(probe.carrier_allows_fallback);
ASSERT_TRUE(probe.shared_matcher_failed_closed);
/* alpha_edges is a SIDE EFFECT of this test, not its subject. The subject is
* beta_edges == 0 and legacy_edges == 0 below: an ambiguous exact LSP match
* must not fall through to the lower-ranked legacy semantic row. Both are
* unchanged, as are all four probe assertions above.
*
* It used to be 1 because this harness DELIBERATELY injects an ambiguous
* exact match and forces the shared matcher to fail closed, so
* `value.render()` drops past the LSP into the registry with THREE
* same-named candidates (Alpha/Beta/Legacy.render) and bound by
* suffix_match — MEASURED: strategy=suffix_match, cands=3, conf=0.5500.
* That is a 1-in-3 guess that happened to land on Alpha: exactly the weak
* member-call class the Python guard now suppresses (#1276), and the same
* accepted trade recorded on python/S6 in test_lsp_resolution_probe.c.
*
* NOT over-suppression: sibling tests in this suite index the same
* `value.render()` source and resolve it via lsp_method (cands=1,
* conf=0.9000), a strategy the guard keeps — they still assert 1. Only the
* sabotaged-LSP path degrades to a weak textual guess.
* Flips back to 1 once py_lsp_cross resolves the receiver on this path. */
ASSERT_EQ(alpha_edges, 0);
ASSERT_EQ(beta_edges, 0);
ASSERT_EQ(legacy_edges, 0);
PASS();
}
typedef struct {
bool injected;
bool authoritative_exact_wins;
bool legacy_key_fits;
bool exact_key_overflows;
} lsp_long_key_probe_t;
static void inject_long_caller_exact_and_legacy(CBMFileResult **result_cache, int file_count,
void *ud) {
lsp_long_key_probe_t *probe = (lsp_long_key_probe_t *)ud;
if (!probe)
return;
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache ? result_cache[file] : NULL;
if (!result)
continue;
CBMCall *carrier = NULL;
CBMResolvedCall *exact = NULL;
const char *beta_qn = NULL;
for (int i = 0; i < result->calls.count; i++) {
CBMCall *candidate = &result->calls.items[i];
if (candidate->callee_name &&
strcmp(cbm_pipeline_call_callee_leaf(candidate->callee_name), "render") == 0) {
carrier = candidate;
break;
}
}
for (int i = 0; i < result->resolved_calls.count; i++) {
CBMResolvedCall *candidate = &result->resolved_calls.items[i];
if (candidate->kind == CBM_RESOLVED_INVOCATION && candidate->callee_qn &&
strstr(candidate->callee_qn, "Alpha.render") &&
candidate->site_end_byte > candidate->site_start_byte) {
exact = candidate;
break;
}
}
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->qualified_name && strstr(definition->qualified_name, "Beta.render")) {
beta_qn = definition->qualified_name;
break;
}
}
if (!carrier || !exact || !beta_qn)
continue;
CBMResolvedCall legacy = *exact;
legacy.callee_qn = beta_qn;
legacy.strategy = "test_long_key_legacy";
legacy.confidence = 0.99f;
legacy.site_start_byte = 0;
legacy.site_end_byte = 0;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, legacy);
size_t leaf_len = strlen("render");
size_t legacy_len = strlen(carrier->enclosing_func_qn) + 1U + leaf_len;
char site_suffix[64];
int suffix_len = snprintf(site_suffix, sizeof(site_suffix), "|%u:%u",
carrier->site_start_byte, carrier->site_end_byte);
size_t exact_len = legacy_len + (suffix_len > 0 ? (size_t)suffix_len : 0U);
probe->legacy_key_fits = legacy_len < 1024U;
probe->exact_key_overflows = exact_len >= 1024U;
const CBMResolvedCall *authoritative =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, carrier, false);
probe->authoritative_exact_wins = authoritative && authoritative->callee_qn &&
strstr(authoritative->callee_qn, "Alpha.render") != NULL;
probe->injected = true;
return;
}
}
TEST(parallel_lsp_long_exact_key_never_yields_legacy_target) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_lsp_long_key_XXXXXX");
if (!cbm_mkdtemp(tmpdir))
FAIL("mkdtemp failed");
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
enum { FUNCTION_NAME_LEN = 997 };
char function_name[FUNCTION_NAME_LEN + 1];
memcpy(function_name, "run_", 4);
memset(function_name + 4, 'q', FUNCTION_NAME_LEN - 4);
function_name[FUNCTION_NAME_LEN] = '\0';
fputs("class Alpha:\n"
" def render(self):\n"
" return 'alpha'\n"
"class Beta:\n"
" def render(self):\n"
" return 'beta'\n",
file);
fprintf(file, "def %s(value: Alpha):\n return value.render()\n", function_name);
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"app.py";
files[0].language = CBM_LANG_PYTHON;
lsp_long_key_probe_t probe = {0};
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_long_key", tmpdir, files, 1, 1, NULL, inject_long_caller_exact_and_legacy, &probe,
false);
ASSERT_NOT_NULL(gbuf);
int alpha_edges = count_calls_edges_to_tail(gbuf, "Alpha.render");
int beta_edges = count_calls_edges_to_tail(gbuf, "Beta.render");
if (!probe.injected || !probe.authoritative_exact_wins || !probe.legacy_key_fits ||
!probe.exact_key_overflows || alpha_edges < 1 || beta_edges != 0) {
printf(" long exact-key diagnostic: injected=%d authoritative=%d legacy_fits=%d "
"exact_overflow=%d alpha=%d beta=%d\n",
probe.injected, probe.authoritative_exact_wins, probe.legacy_key_fits,
probe.exact_key_overflows, alpha_edges, beta_edges);
}
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(probe.injected);
ASSERT_TRUE(probe.authoritative_exact_wins);
ASSERT_TRUE(probe.legacy_key_fits);
ASSERT_TRUE(probe.exact_key_overflows);
ASSERT_GTE(alpha_edges, 1);
ASSERT_EQ(beta_edges, 0);
PASS();
}
typedef struct {
int carrier_count;
int exact_match_count;
} lsp_synthetic_index_probe_t;
static void inspect_synthetic_add_occurrences(CBMFileResult **result_cache, int file_count,
void *ud) {
lsp_synthetic_index_probe_t *probe = (lsp_synthetic_index_probe_t *)ud;
if (!probe) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache[file];
if (!result) {
continue;
}
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
if (!call->requires_lsp_resolution || !call->callee_name ||
strcmp(call->callee_name, "__add__") != 0 ||
!cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte)) {
continue;
}
probe->carrier_count++;
const CBMResolvedCall *resolved =
cbm_pipeline_find_lsp_resolution(&result->resolved_calls, call, false);
if (resolved &&
cbm_pipeline_source_site_eq(call->site_start_byte, call->site_end_byte,
resolved->site_start_byte, resolved->site_end_byte)) {
probe->exact_match_count++;
}
}
}
}
TEST(parallel_lsp_exact_index_handles_repeated_synthetic_occurrences_without_linear_scan) {
enum { OCCURRENCES = 128 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_lsp_perf_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
fprintf(file, "class Number:\n"
" def __add__(self, other):\n"
" return self\n"
"\n"
"def combine(left: Number, right: Number):\n");
for (int i = 0; i < OCCURRENCES; i++) {
fprintf(file, " value = left + right\n");
}
fprintf(file, " return value\n");
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"app.py";
files[0].language = CBM_LANG_PYTHON;
lsp_synthetic_index_probe_t probe = {0};
cbm_pp_lsp_linear_fallback_rows_reset();
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_par_lsp_perf", tmpdir, files, 1, 1, NULL, inspect_synthetic_add_occurrences, &probe,
false);
ASSERT_NOT_NULL(gbuf);
ASSERT_EQ(probe.carrier_count, OCCURRENCES);
ASSERT_EQ(probe.exact_match_count, OCCURRENCES);
ASSERT_NOT_NULL(find_calls_edge_by_tails(gbuf, "app.combine", "Number.__add__"));
uint64_t fallback_rows = cbm_pp_lsp_linear_fallback_rows();
if (fallback_rows != 0) {
printf(" synthetic exact-index diagnostic: occurrences=%d linear_rows=%llu\n", OCCURRENCES,
(unsigned long long)fallback_rows);
}
ASSERT_EQ(fallback_rows, 0);
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
PASS();
}
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
enum { CALL_REFERENCE_SCALE_OCCURRENCES = 128 };
typedef struct {
bool injected;
int reference_count;
} call_reference_scale_probe_t;
/* Replace incidental extractor usages with a deterministic one-to-one set of
* exact callable references. Every target is a real extracted Function node,
* so the fused resolver must traverse its ordinary usage-materialization path
* and emit 128 distinct CALL_REFERENCE edges. */
static void inject_call_reference_scale_rows(CBMFileResult **result_cache, int file_count,
void *ud) {
call_reference_scale_probe_t *probe = (call_reference_scale_probe_t *)ud;
if (!probe || !result_cache) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache[file];
if (!result) {
continue;
}
const char *caller_qn = NULL;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->name && strcmp(definition->name, "scale_caller") == 0) {
caller_qn = definition->qualified_name;
break;
}
}
if (!caller_qn) {
continue;
}
result->usages.count = 0;
result->resolved_calls.count = 0;
int inserted = 0;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *target = &result->defs.items[i];
if (!target->name || !target->qualified_name ||
strncmp(target->name, "scale_target_", strlen("scale_target_")) != 0) {
continue;
}
uint32_t start = 10000U + (uint32_t)inserted * 16U;
CBMUsage usage = {0};
usage.ref_name = target->name;
usage.enclosing_func_qn = caller_qn;
usage.kind = CBM_USAGE_CALL_REFERENCE;
usage.may_be_call_reference = true;
/* Exact occurrence proof must win even when lexical evidence
* forbids the later raw-name fallback. */
usage.semantic_reference_blocked = true;
usage.semantic_reference_local_shadow = true;
usage.site_start_byte = start;
usage.site_end_byte = start + (uint32_t)strlen(target->name);
cbm_usages_push(&result->usages, &result->arena, usage);
CBMResolvedCall resolved = {0};
resolved.caller_qn = caller_qn;
resolved.callee_qn = target->qualified_name;
resolved.strategy = "test_exact_reference_scale";
resolved.confidence = 0.95f;
resolved.kind = CBM_RESOLVED_CALL_REFERENCE;
resolved.site_start_byte = usage.site_start_byte;
resolved.site_end_byte = usage.site_end_byte;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, resolved);
inserted++;
}
probe->reference_count = inserted;
probe->injected = inserted == CALL_REFERENCE_SCALE_OCCURRENCES;
return;
}
}
TEST(parallel_call_reference_lookup_rows_grow_linearly) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_ref_scale_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.py", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
for (int i = 0; i < CALL_REFERENCE_SCALE_OCCURRENCES; i++) {
fprintf(file, "def scale_target_%03d():\n return %d\n", i, i);
}
fputs("def scale_caller():\n return None\n", file);
fclose(file);
cbm_file_info_t file_info = {0};
file_info.path = path;
file_info.rel_path = (char *)"app.py";
file_info.language = CBM_LANG_PYTHON;
call_reference_scale_probe_t probe = {0};
cbm_pipeline_lsp_reference_lookup_test_reset();
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts_and_mutator(
"cbm_par_ref_scale", tmpdir, &file_info, 1, 1, NULL, inject_call_reference_scale_rows,
&probe, false);
uint64_t rows_examined = cbm_pipeline_lsp_reference_lookup_test_rows_examined();
const cbm_gbuf_node_t *caller =
gbuf ? find_unique_callable_node_by_tail(gbuf, "app.scale_caller") : NULL;
const cbm_gbuf_edge_t **edges = NULL;
int edge_count = 0;
int lookup_rc = caller ? cbm_gbuf_find_edges_by_source_type(gbuf, caller->id, "CALL_REFERENCE",
&edges, &edge_count)
: -1;
int exact_target_edges = 0;
for (int i = 0; lookup_rc == 0 && i < edge_count; i++) {
const cbm_gbuf_node_t *target =
edges[i] ? cbm_gbuf_find_by_id(gbuf, edges[i]->target_id) : NULL;
if (target && target->name &&
strncmp(target->name, "scale_target_", strlen("scale_target_")) == 0) {
exact_target_edges++;
}
}
uint64_t linear_budget = (uint64_t)CALL_REFERENCE_SCALE_OCCURRENCES * 8U;
if (!probe.injected || lookup_rc != 0 || edge_count != CALL_REFERENCE_SCALE_OCCURRENCES ||
exact_target_edges != CALL_REFERENCE_SCALE_OCCURRENCES || rows_examined > linear_budget) {
printf(" call-reference lookup scale diagnostic: injected=%d references=%d "
"edges=%d exact_targets=%d rows=%llu budget=%llu\n",
probe.injected, probe.reference_count, edge_count, exact_target_edges,
(unsigned long long)rows_examined, (unsigned long long)linear_budget);
}
cbm_gbuf_free(gbuf);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(probe.injected);
ASSERT_EQ(probe.reference_count, CALL_REFERENCE_SCALE_OCCURRENCES);
ASSERT_EQ(lookup_rc, 0);
ASSERT_EQ(edge_count, CALL_REFERENCE_SCALE_OCCURRENCES);
ASSERT_EQ(exact_target_edges, CALL_REFERENCE_SCALE_OCCURRENCES);
/* The current full scan examines N rows for every one of N usages:
* 128*128 = 16,384. A site index should keep this below a small linear
* allowance while retaining the exact target/ambiguity checks. */
ASSERT_LTE(rows_examined, linear_budget);
PASS();
}
#endif
/* A lexical callable named like a module component owns the JSX occurrence.
* Keep a direct component call as a positive control, and require both graph
* builders to reject textual fallback for the shadowed tag. */
TEST(parallel_tsx_local_component_shadow_has_no_false_call) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_tsx_shadow_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/main.tsx", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen main.tsx failed");
}
fputs("function Card(): JSX.Element { return <div/>; }\n"
"function Good(): JSX.Element { return <Card/>; }\n"
"function Shadowed(Card: () => JSX.Element): JSX.Element { return <Card/>; }\n",
file);
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"main.tsx";
files[0].language = CBM_LANG_TSX;
cbm_gbuf_t *sequential = run_sequential("cbm_tsx_shadow", tmpdir, files, 1);
cbm_gbuf_t *parallel = run_parallel("cbm_tsx_shadow", tmpdir, files, 1, 1);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const bool sequential_nodes = find_unique_callable_node_by_tail(sequential, "main.Card") &&
find_unique_callable_node_by_tail(sequential, "main.Good") &&
find_unique_callable_node_by_tail(sequential, "main.Shadowed");
const bool parallel_nodes = find_unique_callable_node_by_tail(parallel, "main.Card") &&
find_unique_callable_node_by_tail(parallel, "main.Good") &&
find_unique_callable_node_by_tail(parallel, "main.Shadowed");
const bool sequential_good =
find_calls_edge_by_tails(sequential, "main.Good", "main.Card") != NULL;
const bool parallel_good = find_calls_edge_by_tails(parallel, "main.Good", "main.Card") != NULL;
const bool sequential_wrong =
find_calls_edge_by_tails(sequential, "main.Shadowed", "main.Card") != NULL;
const bool parallel_wrong =
find_calls_edge_by_tails(parallel, "main.Shadowed", "main.Card") != NULL;
if (!sequential_nodes || !parallel_nodes || !sequential_good || !parallel_good ||
sequential_wrong || parallel_wrong) {
printf(" TSX shadow graph diagnostic: seq_nodes=%d par_nodes=%d seq_good=%d "
"par_good=%d seq_wrong=%d par_wrong=%d\n",
sequential_nodes, parallel_nodes, sequential_good, parallel_good, sequential_wrong,
parallel_wrong);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(sequential_nodes);
ASSERT_TRUE(parallel_nodes);
ASSERT_TRUE(sequential_good);
ASSERT_TRUE(parallel_good);
ASSERT_FALSE(sequential_wrong);
ASSERT_FALSE(parallel_wrong);
PASS();
}
typedef struct {
bool injected;
} exact_noncallable_reference_probe_t;
static void inject_exact_noncallable_reference(CBMFileResult **result_cache, int file_count,
void *ud) {
exact_noncallable_reference_probe_t *probe = (exact_noncallable_reference_probe_t *)ud;
if (!probe || !result_cache) {
return;
}
for (int file = 0; file < file_count; file++) {
CBMFileResult *result = result_cache[file];
if (!result) {
continue;
}
const char *caller_qn = NULL;
const char *target_qn = NULL;
for (int i = 0; i < result->defs.count; i++) {
const CBMDefinition *definition = &result->defs.items[i];
if (definition->name && definition->qualified_name &&
strcmp(definition->name, "carrier") == 0) {
caller_qn = definition->qualified_name;
}
if (definition->name && definition->qualified_name && definition->label &&
strcmp(definition->name, "semanticValue") == 0 &&
strcmp(definition->label, "Variable") == 0) {
target_qn = definition->qualified_name;
}
}
if (!caller_qn || !target_qn) {
continue;
}
result->usages.count = 0;
result->resolved_calls.count = 0;
CBMUsage usage = {0};
usage.ref_name = "semanticAlias";
usage.enclosing_func_qn = caller_qn;
usage.kind = CBM_USAGE_VALUE;
usage.may_be_call_reference = true;
/* The raw-name path is deliberately unavailable. A resulting USAGE
* therefore proves that the exact semantic row was joined and then
* classified by the materialized target's non-callable label. */
usage.semantic_reference_blocked = true;
usage.site_start_byte = 40000U;
usage.site_end_byte = 40000U + (uint32_t)strlen(usage.ref_name);
cbm_usages_push(&result->usages, &result->arena, usage);
CBMResolvedCall resolved = {0};
resolved.caller_qn = caller_qn;
resolved.callee_qn = target_qn;
resolved.strategy = "lsp_callable_value_reference";
resolved.reason = usage.ref_name;
resolved.confidence = 0.99f;
resolved.kind = CBM_RESOLVED_CALL_REFERENCE;
resolved.site_start_byte = usage.site_start_byte;
resolved.site_end_byte = usage.site_end_byte;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, resolved);
probe->injected = true;
return;
}
}
TEST(parallel_exact_semantic_noncallable_target_stays_usage) {
static const char source[] = "const semanticValue = 1;\n"
"function consume(value: unknown): void {}\n"
"export function carrier(): void { consume(semanticValue); }\n";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_exact_noncallable_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/app.ts", tmpdir);
if (th_write_file(path, source) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write exact non-callable fixture");
}
cbm_file_info_t file = {0};
file.path = path;
file.rel_path = (char *)"app.ts";
file.language = CBM_LANG_TYPESCRIPT;
exact_noncallable_reference_probe_t sequential_probe = {0};
exact_noncallable_reference_probe_t parallel_probe = {0};
cbm_gbuf_t *sequential = run_sequential_with_lsp_cross_and_mutator(
"cbm_exact_noncallable", tmpdir, &file, 1, inject_exact_noncallable_reference,
&sequential_probe, false);
cbm_gbuf_t *parallel = run_parallel_with_extract_opts_and_mutator(
"cbm_exact_noncallable", tmpdir, &file, 1, 1, NULL, inject_exact_noncallable_reference,
&parallel_probe, false);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const cbm_gbuf_node_t *sequential_target = find_unique_node_by_name_label_qn_suffix(
sequential, "semanticValue", "Variable", "app.semanticValue");
const cbm_gbuf_node_t *parallel_target = find_unique_node_by_name_label_qn_suffix(
parallel, "semanticValue", "Variable", "app.semanticValue");
const bool sequential_usage =
has_edge_from_callable_to_node(sequential, "app.carrier", sequential_target, "USAGE");
const bool parallel_usage =
has_edge_from_callable_to_node(parallel, "app.carrier", parallel_target, "USAGE");
const bool sequential_promoted =
has_edge_from_callable_to_node(sequential, "app.carrier", sequential_target,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(sequential, "app.carrier", sequential_target, "CALLS");
const bool parallel_promoted =
has_edge_from_callable_to_node(parallel, "app.carrier", parallel_target,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(parallel, "app.carrier", parallel_target, "CALLS");
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
th_rmtree(tmpdir);
ASSERT_TRUE(sequential_probe.injected);
ASSERT_TRUE(parallel_probe.injected);
ASSERT_NOT_NULL(sequential_target);
ASSERT_NOT_NULL(parallel_target);
ASSERT_TRUE(sequential_usage);
ASSERT_TRUE(parallel_usage);
ASSERT_FALSE(sequential_promoted);
ASSERT_FALSE(parallel_promoted);
PASS();
}
/* A value argument whose exact target is a module Variable remains an ordinary
* USAGE even when the TS semantic pass marks the occurrence as reference-like.
* Conversely, a parameter or local declaration with the same spelling owns its
* lexical occurrence and must not fall through to the module Variable. Exercise
* both the sequential pass_usages path and the fused parallel resolver. */
TEST(parallel_typescript_module_value_usage_respects_lexical_shadows) {
static const char settings_source[] =
"export const config = { enabled: true };\n"
"function consumeConfig(value: { enabled: boolean }): void {}\n"
"export function moduleArgument(): void { consumeConfig(config); }\n";
static const char shadows_source[] =
"import { config } from './settings';\n"
"function consumeShadow(value: { enabled: boolean }): void {}\n"
"export function parameterShadow(config: { enabled: boolean }): void {\n"
" consumeShadow(config);\n"
"}\n"
"export function localShadow(): void {\n"
" const config = { enabled: false };\n"
" consumeShadow(config);\n"
"}\n";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_ts_usage_scope_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char settings_path[512];
char shadows_path[512];
snprintf(settings_path, sizeof(settings_path), "%s/settings.ts", tmpdir);
snprintf(shadows_path, sizeof(shadows_path), "%s/shadows.ts", tmpdir);
if (th_write_file(settings_path, settings_source) != 0 ||
th_write_file(shadows_path, shadows_source) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write TypeScript usage-scope fixture");
}
cbm_file_info_t files[2] = {0};
files[0].path = settings_path;
files[0].rel_path = (char *)"settings.ts";
files[0].language = CBM_LANG_TYPESCRIPT;
files[1].path = shadows_path;
files[1].rel_path = (char *)"shadows.ts";
files[1].language = CBM_LANG_TYPESCRIPT;
cbm_gbuf_t *sequential = run_sequential("cbm_ts_usage_scope", tmpdir, files, 2);
cbm_gbuf_t *parallel = run_parallel("cbm_ts_usage_scope", tmpdir, files, 2, 2);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const cbm_gbuf_node_t *sequential_config = find_unique_node_by_name_label_qn_suffix(
sequential, "config", "Variable", "settings.config");
const cbm_gbuf_node_t *parallel_config =
find_unique_node_by_name_label_qn_suffix(parallel, "config", "Variable", "settings.config");
const bool sequential_target = sequential_config != NULL;
const bool parallel_target = parallel_config != NULL;
const bool sequential_callers =
find_unique_callable_node_by_tail(sequential, "settings.moduleArgument") &&
find_unique_callable_node_by_tail(sequential, "shadows.parameterShadow") &&
find_unique_callable_node_by_tail(sequential, "shadows.localShadow");
const bool parallel_callers =
find_unique_callable_node_by_tail(parallel, "settings.moduleArgument") &&
find_unique_callable_node_by_tail(parallel, "shadows.parameterShadow") &&
find_unique_callable_node_by_tail(parallel, "shadows.localShadow");
const bool sequential_module_usage = has_edge_from_callable_to_node(
sequential, "settings.moduleArgument", sequential_config, "USAGE");
const bool parallel_module_usage = has_edge_from_callable_to_node(
parallel, "settings.moduleArgument", parallel_config, "USAGE");
const bool sequential_module_reference =
has_edge_from_callable_to_node(sequential, "settings.moduleArgument", sequential_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(sequential, "settings.moduleArgument", sequential_config,
"CALLS");
const bool parallel_module_reference =
has_edge_from_callable_to_node(parallel, "settings.moduleArgument", parallel_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(parallel, "settings.moduleArgument", parallel_config,
"CALLS");
const bool sequential_parameter_leak =
has_edge_from_callable_to_node(sequential, "shadows.parameterShadow", sequential_config,
"USAGE") ||
has_edge_from_callable_to_node(sequential, "shadows.parameterShadow", sequential_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(sequential, "shadows.parameterShadow", sequential_config,
"CALLS");
const bool parallel_parameter_leak =
has_edge_from_callable_to_node(parallel, "shadows.parameterShadow", parallel_config,
"USAGE") ||
has_edge_from_callable_to_node(parallel, "shadows.parameterShadow", parallel_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(parallel, "shadows.parameterShadow", parallel_config,
"CALLS");
const bool sequential_local_leak =
has_edge_from_callable_to_node(sequential, "shadows.localShadow", sequential_config,
"USAGE") ||
has_edge_from_callable_to_node(sequential, "shadows.localShadow", sequential_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(sequential, "shadows.localShadow", sequential_config,
"CALLS");
const bool parallel_local_leak =
has_edge_from_callable_to_node(parallel, "shadows.localShadow", parallel_config, "USAGE") ||
has_edge_from_callable_to_node(parallel, "shadows.localShadow", parallel_config,
"CALL_REFERENCE") ||
has_edge_from_callable_to_node(parallel, "shadows.localShadow", parallel_config, "CALLS");
if (!sequential_target || !parallel_target || !sequential_callers || !parallel_callers ||
!sequential_module_usage || !parallel_module_usage || sequential_module_reference ||
parallel_module_reference || sequential_parameter_leak || parallel_parameter_leak ||
sequential_local_leak || parallel_local_leak) {
printf(" TS module-value diagnostic: target=%d/%d callers=%d/%d usage=%d/%d "
"reference=%d/%d parameter_leak=%d/%d local_leak=%d/%d\n",
sequential_target, parallel_target, sequential_callers, parallel_callers,
sequential_module_usage, parallel_module_usage, sequential_module_reference,
parallel_module_reference, sequential_parameter_leak, parallel_parameter_leak,
sequential_local_leak, parallel_local_leak);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
th_rmtree(tmpdir);
ASSERT_TRUE(sequential_target);
ASSERT_TRUE(parallel_target);
ASSERT_TRUE(sequential_callers);
ASSERT_TRUE(parallel_callers);
ASSERT_TRUE(sequential_module_usage);
ASSERT_TRUE(parallel_module_usage);
ASSERT_FALSE(sequential_module_reference);
ASSERT_FALSE(parallel_module_reference);
ASSERT_FALSE(sequential_parameter_leak);
ASSERT_FALSE(parallel_parameter_leak);
ASSERT_FALSE(sequential_local_leak);
ASSERT_FALSE(parallel_local_leak);
PASS();
}
enum {
TS_EXACT_SELECTED_IMPORT = 0,
TS_EXACT_DECOY_IMPORT,
TS_EXACT_LOCAL_CLASS,
TS_EXACT_NAMESPACE_INNER_CALL,
TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL,
TS_EXACT_EDGE_COUNT
};
typedef struct {
int edge_counts[2][TS_EXACT_EDGE_COUNT]; /* sequential, parallel */
} ts_exact_parity_observation_t;
static ts_exact_parity_observation_t run_ts_exact_parity_fixture(CBMLanguage main_language,
const char *extension,
const char *project) {
ts_exact_parity_observation_t observation;
for (int route = 0; route < 2; route++) {
for (int edge = 0; edge < TS_EXACT_EDGE_COUNT; edge++) {
observation.edge_counts[route][edge] = -1;
}
}
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_ts_exact_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
return observation;
}
char selected_path[512];
char decoy_path[512];
char main_path[512];
char main_rel[64];
snprintf(selected_path, sizeof(selected_path), "%s/a/types.ts", tmpdir);
snprintf(decoy_path, sizeof(decoy_path), "%s/b/types.ts", tmpdir);
snprintf(main_path, sizeof(main_path), "%s/main.%s", tmpdir, extension);
snprintf(main_rel, sizeof(main_rel), "main.%s", extension);
static const char type_source[] = "export interface Config { enabled: boolean }\n";
static const char main_source[] =
"import type { Config } from './a/types';\n"
"export function choose(cfg: Config): boolean { return cfg.enabled; }\n"
"namespace App {\n"
" export namespace Utils {\n"
" export function clamp(): number { return 1; }\n"
" export function normalise(): number { return clamp(); }\n"
" }\n"
" export class Config {\n"
" constructor() { Utils.normalise(); }\n"
" }\n"
"}\n";
if (th_write_file(selected_path, type_source) != 0 ||
th_write_file(decoy_path, type_source) != 0 || th_write_file(main_path, main_source) != 0) {
th_rmtree(tmpdir);
return observation;
}
cbm_file_info_t files[3] = {0};
files[0].path = selected_path;
files[0].rel_path = (char *)"a/types.ts";
files[0].language = CBM_LANG_TYPESCRIPT;
files[1].path = decoy_path;
files[1].rel_path = (char *)"b/types.ts";
files[1].language = CBM_LANG_TYPESCRIPT;
files[2].path = main_path;
files[2].rel_path = main_rel;
files[2].language = main_language;
cbm_gbuf_t *graphs[2] = {
run_sequential_with_lsp_cross_and_mutator(project, tmpdir, files, 3, NULL, NULL, true),
run_parallel_with_extract_opts_and_mutator(project, tmpdir, files, 3, 2, NULL, NULL, NULL,
true),
};
for (int route = 0; route < 2; route++) {
cbm_gbuf_t *graph = graphs[route];
const cbm_gbuf_node_t *choose =
find_unique_node_by_name_label_qn_suffix(graph, "choose", "Function", "main.choose");
const cbm_gbuf_node_t *selected = find_unique_node_by_name_label_qn_suffix(
graph, "Config", "Interface", "a.types.Config");
const cbm_gbuf_node_t *decoy = find_unique_node_by_name_label_qn_suffix(
graph, "Config", "Interface", "b.types.Config");
const cbm_gbuf_node_t *local_class =
find_unique_node_by_name_label_qn_suffix(graph, "Config", "Class", "main.App.Config");
const cbm_gbuf_node_t *normalise = find_unique_node_by_name_label_qn_suffix(
graph, "normalise", "Function", "main.App.Utils.normalise");
const cbm_gbuf_node_t *clamp = find_unique_node_by_name_label_qn_suffix(
graph, "clamp", "Function", "main.App.Utils.clamp");
const cbm_gbuf_node_t *constructor = find_unique_node_by_name_label_qn_suffix(
graph, "constructor", "Method", "main.App.Config.constructor");
observation.edge_counts[route][TS_EXACT_SELECTED_IMPORT] =
count_edges_between_nodes(graph, choose, selected, "USAGE");
observation.edge_counts[route][TS_EXACT_DECOY_IMPORT] =
count_edges_between_nodes(graph, choose, decoy, "USAGE");
observation.edge_counts[route][TS_EXACT_LOCAL_CLASS] =
count_edges_between_nodes(graph, choose, local_class, "USAGE");
observation.edge_counts[route][TS_EXACT_NAMESPACE_INNER_CALL] =
count_edges_between_nodes(graph, normalise, clamp, "CALLS");
observation.edge_counts[route][TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL] =
count_edges_between_nodes(graph, constructor, normalise, "CALLS");
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
return observation;
}
static void print_ts_exact_parity_diagnostic(const char *language,
const ts_exact_parity_observation_t *observation) {
printf(" %s exact parity: seq=[%d,%d,%d,%d,%d] par=[%d,%d,%d,%d,%d]\n", language,
observation->edge_counts[0][TS_EXACT_SELECTED_IMPORT],
observation->edge_counts[0][TS_EXACT_DECOY_IMPORT],
observation->edge_counts[0][TS_EXACT_LOCAL_CLASS],
observation->edge_counts[0][TS_EXACT_NAMESPACE_INNER_CALL],
observation->edge_counts[0][TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL],
observation->edge_counts[1][TS_EXACT_SELECTED_IMPORT],
observation->edge_counts[1][TS_EXACT_DECOY_IMPORT],
observation->edge_counts[1][TS_EXACT_LOCAL_CLASS],
observation->edge_counts[1][TS_EXACT_NAMESPACE_INNER_CALL],
observation->edge_counts[1][TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL]);
}
TEST(parallel_typescript_import_namespace_exact_parity) {
ts_exact_parity_observation_t observation =
run_ts_exact_parity_fixture(CBM_LANG_TYPESCRIPT, "ts", "cbm_ts_exact_parity");
print_ts_exact_parity_diagnostic("TypeScript", &observation);
for (int route = 0; route < 2; route++) {
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_SELECTED_IMPORT], 1);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_DECOY_IMPORT], 0);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_LOCAL_CLASS], 0);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_NAMESPACE_INNER_CALL], 1);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL], 1);
}
PASS();
}
TEST(parallel_tsx_import_namespace_exact_parity) {
ts_exact_parity_observation_t observation =
run_ts_exact_parity_fixture(CBM_LANG_TSX, "tsx", "cbm_tsx_exact_parity");
print_ts_exact_parity_diagnostic("TSX", &observation);
for (int route = 0; route < 2; route++) {
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_SELECTED_IMPORT], 1);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_DECOY_IMPORT], 0);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_LOCAL_CLASS], 0);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_NAMESPACE_INNER_CALL], 1);
ASSERT_EQ(observation.edge_counts[route][TS_EXACT_NAMESPACE_CONSTRUCTOR_CALL], 1);
}
PASS();
}
typedef struct {
bool carrier;
bool semantic;
bool exact_join;
} kotlin_implicit_probe_t;
static kotlin_implicit_probe_t probe_kotlin_implicit_site(const CBMFileResult *result,
const char *caller, const char *callee,
const char *target_qn,
const char *strategy) {
kotlin_implicit_probe_t probe = {0};
if (!result || !caller || !callee || !target_qn || !strategy) {
return probe;
}
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
const char *call_caller =
call->enclosing_func_qn ? strrchr(call->enclosing_func_qn, '.') : NULL;
call_caller = call_caller ? call_caller + 1 : call->enclosing_func_qn;
if (!call->requires_lsp_resolution || !call->callee_name || !call_caller ||
strcmp(call->callee_name, callee) != 0 || strcmp(call_caller, caller) != 0 ||
!cbm_pipeline_source_site_present(call->site_start_byte, call->site_end_byte)) {
continue;
}
probe.carrier = true;
}
for (int j = 0; j < result->resolved_calls.count; j++) {
const CBMResolvedCall *resolved = &result->resolved_calls.items[j];
const char *resolved_caller =
resolved->caller_qn ? strrchr(resolved->caller_qn, '.') : NULL;
resolved_caller = resolved_caller ? resolved_caller + 1 : resolved->caller_qn;
if (resolved->kind != CBM_RESOLVED_INVOCATION || !resolved_caller || !resolved->callee_qn ||
!resolved->strategy || strcmp(resolved_caller, caller) != 0 ||
strcmp(resolved->callee_qn, target_qn) != 0 ||
strcmp(resolved->strategy, strategy) != 0) {
continue;
}
probe.semantic = true;
for (int i = 0; i < result->calls.count; i++) {
const CBMCall *call = &result->calls.items[i];
const char *call_caller =
call->enclosing_func_qn ? strrchr(call->enclosing_func_qn, '.') : NULL;
call_caller = call_caller ? call_caller + 1 : call->enclosing_func_qn;
if (!call->requires_lsp_resolution || !call->callee_name || !call_caller ||
strcmp(call->callee_name, callee) != 0 || strcmp(call_caller, caller) != 0) {
continue;
}
if (cbm_pipeline_source_site_eq(call->site_start_byte, call->site_end_byte,
resolved->site_start_byte, resolved->site_end_byte)) {
probe.exact_join = true;
}
}
}
return probe;
}
/* #2120: two C# projects declare a class with the same simple name in
* namespaces that share a root (Contoso.Platform.Identity.UserIdentityInfo vs
* Contoso.Platform.BackOffice.Identity.UserIdentityInfo). Graph QNs are
* path-derived, so the namespace-prefix lookups never hit a project type and
* the short-name fallback scored both variants 0 against the file namespace:
* registration order picked the target, ignoring the caller's `using`
* directive. Each caller below can see exactly ONE variant (C# binding rules),
* so exactly one edge per caller is correct whatever the file order: plain
* `using` (both directions), enclosing-namespace proximity, and a
* fully-qualified name. The last case is a reference-assembly shape: one file
* declaring several namespaces, whose file-level namespace is only the FIRST
* one; the decoy is registered first, so a mislabeled namespace falls back to
* it. `partial` pieces of one type are separate registry entries, so a member
* declared on another piece must still bind to that piece. */
TEST(parallel_csharp_same_name_class_binds_by_namespace) {
static const char platform_src[] =
"namespace Contoso.Platform.Identity;\n"
"public class UserIdentityInfo {\n"
" public static UserIdentityInfo ParseFromClaim(string c)\n"
" => new UserIdentityInfo();\n"
"}\n";
static const char backoffice_src[] =
"namespace Contoso.Platform.BackOffice.Identity;\n"
"public class UserIdentityInfo {\n"
" public static UserIdentityInfo ParseFromClaim(string c)\n"
" => new UserIdentityInfo();\n"
"}\n";
static const char bo_handler_src[] =
"using Contoso.Platform.BackOffice.Identity;\n"
"namespace Contoso.Platform.BackOffice.CqrsHandlers;\n"
"public class Handler {\n"
" public string Handle(string claim) {\n"
" var info = UserIdentityInfo.ParseFromClaim(claim);\n"
" return info.ToString();\n"
" }\n"
"}\n";
static const char pf_handler_src[] =
"using Contoso.Platform.Identity;\n"
"namespace Contoso.Platform.Api;\n"
"public class PlatformHandler {\n"
" public string Handle(string claim) {\n"
" var info = UserIdentityInfo.ParseFromClaim(claim);\n"
" return info.ToString();\n"
" }\n"
"}\n";
static const char nested_ns_src[] =
"namespace Contoso.Platform.BackOffice.Identity.Claims\n"
"{\n"
" public class ClaimReader {\n"
" public string Read(string claim) {\n"
" var info = UserIdentityInfo.ParseFromClaim(claim);\n"
" return info.ToString();\n"
" }\n"
" }\n"
"}\n";
static const char fq_src[] = "namespace Contoso.Platform.Reports;\n"
"public class Report {\n"
" public string Build(string claim) {\n"
" var info = Contoso.Platform.BackOffice.Identity"
".UserIdentityInfo.ParseFromClaim(claim);\n"
" return info.ToString();\n"
" }\n"
"}\n";
static const char decoy_src[] = "namespace Contoso.Decoy;\n"
"public class Worker {\n"
" public void Start() { }\n"
" public void Beta() { }\n"
"}\n";
static const char multi_ns_src[] = "namespace Contoso.Win32 { public class SafeHandle { } }\n"
"namespace Contoso.Threading {\n"
" public class Worker { public void Start() { } }\n"
"}\n";
static const char runner_src[] = "using Contoso.Threading;\n"
"namespace Contoso.App;\n"
"public class Runner {\n"
" public void Run() {\n"
" var w = new Worker();\n"
" w.Start();\n"
" }\n"
"}\n";
static const char part_a_src[] = "namespace Contoso.Parts;\n"
"public partial class Thing { public void Alpha() { } }\n";
static const char part_b_src[] = "namespace Contoso.Parts;\n"
"public partial class Thing { public void Beta() { } }\n";
static const char part_user_src[] = "using Contoso.Parts;\n"
"namespace Contoso.App;\n"
"public class PartUser {\n"
" public void Use() {\n"
" var t = new Thing();\n"
" t.Alpha();\n"
" t.Beta();\n"
" }\n"
"}\n";
const char *project = "cbm_cs_same_name";
static const char *rels[] = {
"src/Decoy/Worker.cs",
"src/Contoso.Platform/Identity/UserIdentityInfo.cs",
"src/Contoso.Platform.BackOffice/Identity/UserIdentityInfo.cs",
"src/Contoso.Platform.BackOffice/CqrsHandlers/Handler.cs",
"src/Contoso.Platform/Api/PlatformHandler.cs",
"src/Contoso.Platform.BackOffice/Identity/Claims/ClaimReader.cs",
"src/Contoso.Platform/Reports/Report.cs",
"src/Ref/Threading.cs",
"src/App/Runner.cs",
"src/Parts/Thing.A.cs",
"src/Parts/Thing.B.cs",
"src/App/PartUser.cs",
};
const char *srcs[] = {decoy_src, platform_src, backoffice_src, bo_handler_src,
pf_handler_src, nested_ns_src, fq_src, multi_ns_src,
runner_src, part_a_src, part_b_src, part_user_src};
enum { N_FILES = 12, N_CHECKS = 13 };
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_cs_same_name_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char paths[N_FILES][512];
cbm_file_info_t files[N_FILES] = {0};
for (int i = 0; i < N_FILES; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, rels[i]);
if (th_write_file(paths[i], srcs[i]) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write C# same-name fixture");
}
files[i].path = paths[i];
files[i].rel_path = (char *)rels[i];
files[i].language = CBM_LANG_CSHARP;
}
cbm_gbuf_t *graphs[2] = {run_sequential_with_lsp_cross(project, tmpdir, files, N_FILES), NULL};
g_harness_cs_registry = true;
graphs[1] = run_parallel(project, tmpdir, files, N_FILES, 2);
g_harness_cs_registry = false;
ASSERT_NOT_NULL(graphs[0]);
ASSERT_NOT_NULL(graphs[1]);
const char *pf = "cbm_cs_same_name.src.Contoso.Platform.Identity.UserIdentityInfo."
"UserIdentityInfo.ParseFromClaim";
const char *bo = "cbm_cs_same_name.src.Contoso.Platform.BackOffice.Identity."
"UserIdentityInfo.UserIdentityInfo.ParseFromClaim";
const char *ref_start = "cbm_cs_same_name.src.Ref.Threading.Worker.Start";
const char *decoy_start = "cbm_cs_same_name.src.Decoy.Worker.Worker.Start";
const char *part_alpha = "cbm_cs_same_name.src.Parts.Thing.A.Thing.Alpha";
const char *part_beta = "cbm_cs_same_name.src.Parts.Thing.B.Thing.Beta";
const char *decoy_beta = "cbm_cs_same_name.src.Decoy.Worker.Worker.Beta";
bool ok[2][N_CHECKS];
bool all = true;
for (int g = 0; g < 2; g++) {
cbm_gbuf_t *gb = graphs[g];
ok[g][0] = has_edge_from_callable_to_qn(gb, "Handler.Handle", bo, "CALLS");
ok[g][1] = !has_edge_from_callable_to_qn(gb, "Handler.Handle", pf, "CALLS");
ok[g][2] = has_edge_from_callable_to_qn(gb, "PlatformHandler.Handle", pf, "CALLS");
ok[g][3] = !has_edge_from_callable_to_qn(gb, "PlatformHandler.Handle", bo, "CALLS");
ok[g][4] = has_edge_from_callable_to_qn(gb, "ClaimReader.Read", bo, "CALLS");
ok[g][5] = !has_edge_from_callable_to_qn(gb, "ClaimReader.Read", pf, "CALLS");
ok[g][6] = has_edge_from_callable_to_qn(gb, "Report.Build", bo, "CALLS");
ok[g][7] = !has_edge_from_callable_to_qn(gb, "Report.Build", pf, "CALLS");
ok[g][8] = has_edge_from_callable_to_qn(gb, "Runner.Run", ref_start, "CALLS");
ok[g][9] = !has_edge_from_callable_to_qn(gb, "Runner.Run", decoy_start, "CALLS");
ok[g][10] = has_edge_from_callable_to_qn(gb, "PartUser.Use", part_alpha, "CALLS");
ok[g][11] = has_edge_from_callable_to_qn(gb, "PartUser.Use", part_beta, "CALLS");
ok[g][12] = !has_edge_from_callable_to_qn(gb, "PartUser.Use", decoy_beta, "CALLS");
for (int k = 0; k < N_CHECKS; k++) {
all = all && ok[g][k];
}
}
if (!all) {
for (int g = 0; g < 2; g++) {
printf(" C# same-name diagnostic %s: using_bo=%d/%d using_pf=%d/%d "
"enclosing_ns=%d/%d fully_qualified=%d/%d multi_ns_file=%d/%d "
"partial=%d/%d/%d\n",
g == 0 ? "sequential" : "parallel", ok[g][0], ok[g][1], ok[g][2], ok[g][3],
ok[g][4], ok[g][5], ok[g][6], ok[g][7], ok[g][8], ok[g][9], ok[g][10], ok[g][11],
ok[g][12]);
}
}
cbm_gbuf_free(graphs[0]);
cbm_gbuf_free(graphs[1]);
th_rmtree(tmpdir);
ASSERT_TRUE(all);
PASS();
}
/* External Kotlin protocol targets must not borrow a project method merely
* because the final Class.method segments agree. Raw assertions prove that the
* iterator/destructuring carriers and their stdlib semantic rows really exist;
* the graph negatives therefore cannot pass by dropping synthesis. Local alias
* and operator calls are positive controls for the narrowed target policy. */
TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail) {
static const char collision_source[] = "package collision\n"
"class IntArray {\n"
" fun iterator(): IntArray = this\n"
"}\n"
"class Pair {\n"
" operator fun component1(): Int = 0\n"
"}\n";
static const char main_source[] =
"package app\n"
"fun loopBuiltIn(values: kotlin.IntArray) {\n"
" for (value in values) { println(value) }\n"
"}\n"
"fun destructureBuiltIn(value: kotlin.Pair<Int, Int>) {\n"
" val (left, right) = value\n"
"}\n"
"fun projectHandler(): Int = 7\n"
"fun aliasCaller(): Int {\n"
" val callback = ::projectHandler\n"
" return callback()\n"
"}\n"
"class Box { operator fun plus(other: Box): Box = this }\n"
"fun operatorCaller(left: Box, right: Box): Box = left + right\n";
const char *project = "cbm_kt_target_policy";
CBMFileResult *raw = cbm_extract_file(main_source, (int)strlen(main_source), CBM_LANG_KOTLIN,
project, "app/Main.kt", 0, NULL, NULL);
ASSERT_NOT_NULL(raw);
ASSERT_FALSE(raw->has_error || raw->parse_incomplete);
kotlin_implicit_probe_t iterator = probe_kotlin_implicit_site(
raw, "loopBuiltIn", "iterator", "kotlin.IntArray.iterator", "lsp_kt_iterator");
kotlin_implicit_probe_t component = probe_kotlin_implicit_site(
raw, "destructureBuiltIn", "component1", "kotlin.Pair.component1", "lsp_kt_destructure");
cbm_free_result(raw);
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_kt_target_policy_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char collision_path[512];
char main_path[512];
snprintf(collision_path, sizeof(collision_path), "%s/collision/Collisions.kt", tmpdir);
snprintf(main_path, sizeof(main_path), "%s/app/Main.kt", tmpdir);
if (th_write_file(collision_path, collision_source) != 0 ||
th_write_file(main_path, main_source) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write Kotlin target-policy fixture");
}
cbm_file_info_t files[2] = {0};
files[0].path = collision_path;
files[0].rel_path = (char *)"collision/Collisions.kt";
files[0].language = CBM_LANG_KOTLIN;
files[1].path = main_path;
files[1].rel_path = (char *)"app/Main.kt";
files[1].language = CBM_LANG_KOTLIN;
cbm_gbuf_t *sequential = run_sequential_with_lsp_cross(project, tmpdir, files, 2);
cbm_gbuf_t *parallel = run_parallel(project, tmpdir, files, 2, 2);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const cbm_gbuf_node_t *sequential_iterator = find_unique_node_by_name_label_qn_suffix(
sequential, "iterator", "Method", "Collisions.IntArray.iterator");
const cbm_gbuf_node_t *parallel_iterator = find_unique_node_by_name_label_qn_suffix(
parallel, "iterator", "Method", "Collisions.IntArray.iterator");
const cbm_gbuf_node_t *sequential_component = find_unique_node_by_name_label_qn_suffix(
sequential, "component1", "Method", "Collisions.Pair.component1");
const cbm_gbuf_node_t *parallel_component = find_unique_node_by_name_label_qn_suffix(
parallel, "component1", "Method", "Collisions.Pair.component1");
const bool sequential_nodes =
sequential_iterator && sequential_component &&
find_unique_callable_node_by_tail(sequential, "Main.loopBuiltIn") &&
find_unique_callable_node_by_tail(sequential, "Main.destructureBuiltIn");
const bool parallel_nodes =
parallel_iterator && parallel_component &&
find_unique_callable_node_by_tail(parallel, "Main.loopBuiltIn") &&
find_unique_callable_node_by_tail(parallel, "Main.destructureBuiltIn");
const bool sequential_alias =
find_calls_edge_by_tails(sequential, "Main.aliasCaller", "Main.projectHandler") != NULL;
const bool parallel_alias =
find_calls_edge_by_tails(parallel, "Main.aliasCaller", "Main.projectHandler") != NULL;
const bool sequential_operator =
find_calls_edge_by_tails(sequential, "Main.operatorCaller", "Box.plus") != NULL;
const bool parallel_operator =
find_calls_edge_by_tails(parallel, "Main.operatorCaller", "Box.plus") != NULL;
const bool sequential_wrong_iterator = has_edge_from_callable_to_node(
sequential, "Main.loopBuiltIn", sequential_iterator, "CALLS");
const bool parallel_wrong_iterator =
has_edge_from_callable_to_node(parallel, "Main.loopBuiltIn", parallel_iterator, "CALLS");
const bool sequential_wrong_component = has_edge_from_callable_to_node(
sequential, "Main.destructureBuiltIn", sequential_component, "CALLS");
const bool parallel_wrong_component = has_edge_from_callable_to_node(
parallel, "Main.destructureBuiltIn", parallel_component, "CALLS");
if (!iterator.carrier || !iterator.semantic || !iterator.exact_join || !component.carrier ||
!component.semantic || !component.exact_join || !sequential_nodes || !parallel_nodes ||
!sequential_alias || !parallel_alias || !sequential_operator || !parallel_operator ||
sequential_wrong_iterator || parallel_wrong_iterator || sequential_wrong_component ||
parallel_wrong_component) {
printf(" Kotlin target-policy diagnostic: iterator_raw=%d/%d/%d "
"component_raw=%d/%d/%d nodes=%d/%d alias=%d/%d operator=%d/%d "
"wrong_iterator=%d/%d wrong_component=%d/%d\n",
iterator.carrier, iterator.semantic, iterator.exact_join, component.carrier,
component.semantic, component.exact_join, sequential_nodes, parallel_nodes,
sequential_alias, parallel_alias, sequential_operator, parallel_operator,
sequential_wrong_iterator, parallel_wrong_iterator, sequential_wrong_component,
parallel_wrong_component);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
th_rmtree(tmpdir);
ASSERT_TRUE(iterator.carrier);
ASSERT_TRUE(iterator.semantic);
ASSERT_TRUE(iterator.exact_join);
ASSERT_TRUE(component.carrier);
ASSERT_TRUE(component.semantic);
ASSERT_TRUE(component.exact_join);
ASSERT_TRUE(sequential_nodes);
ASSERT_TRUE(parallel_nodes);
ASSERT_TRUE(sequential_alias);
ASSERT_TRUE(parallel_alias);
ASSERT_TRUE(sequential_operator);
ASSERT_TRUE(parallel_operator);
ASSERT_FALSE(sequential_wrong_iterator);
ASSERT_FALSE(parallel_wrong_iterator);
ASSERT_FALSE(sequential_wrong_component);
ASSERT_FALSE(parallel_wrong_component);
PASS();
}
/* Kotlin's unary, membership, and index conventions are real invocations just
* like binary `plus`: the LSP already proves the exact operator method at the
* exact expression span. The syntax extractor must provide a matching guarded
* CBMCall carrier so both graph paths can materialize CALLS without a textual
* guess. This test separately proves carrier, semantic row, exact join, and
* graph edge for each syntax family. */
TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph) {
static const char definitions_source[] =
"package app\n"
"class UnaryBox { operator fun unaryMinus(): UnaryBox = this }\n"
"class Bag { operator fun contains(value: Int): Boolean = true }\n"
"class Slots { operator fun get(index: Int): Int = index }\n"
"class Counter {\n"
" operator fun inc(): Counter = this\n"
" operator fun dec(): Counter = this\n"
"}\n";
static const char callers_source[] = "package app\n"
"fun negate(value: UnaryBox): UnaryBox = -value\n"
"fun hasValue(bag: Bag): Boolean = 1 in bag\n"
"fun lacksValue(bag: Bag): Boolean = 1 !in bag\n"
"fun lookup(slots: Slots): Int = slots[0]\n"
"fun increment(value: Counter): Counter { var current = "
"value; current++; return current }\n"
"fun decrement(value: Counter): Counter { var current = "
"value; current--; return current }\n"
"fun isBag(value: Any): Boolean = value is Bag\n"
"fun assign(slots: Slots) { slots[0] = 1 }\n";
static const char source[] = "package app\n"
"class UnaryBox { operator fun unaryMinus(): UnaryBox = this }\n"
"class Bag { operator fun contains(value: Int): Boolean = true }\n"
"class Slots { operator fun get(index: Int): Int = index }\n"
"class Counter {\n"
" operator fun inc(): Counter = this\n"
" operator fun dec(): Counter = this\n"
"}\n"
"fun negate(value: UnaryBox): UnaryBox = -value\n"
"fun hasValue(bag: Bag): Boolean = 1 in bag\n"
"fun lacksValue(bag: Bag): Boolean = 1 !in bag\n"
"fun lookup(slots: Slots): Int = slots[0]\n"
"fun increment(value: Counter): Counter { var current = value; "
"current++; return current }\n"
"fun decrement(value: Counter): Counter { var current = value; "
"current--; return current }\n"
"fun isBag(value: Any): Boolean = value is Bag\n"
"fun assign(slots: Slots) { slots[0] = 1 }\n";
const char *project = "cbm_kt_nonbinary_operators";
CBMFileResult *raw = cbm_extract_file(source, (int)strlen(source), CBM_LANG_KOTLIN, project,
"app/Main.kt", 0, NULL, NULL);
ASSERT_NOT_NULL(raw);
ASSERT_FALSE(raw->has_error || raw->parse_incomplete);
kotlin_implicit_probe_t unary = probe_kotlin_implicit_site(
raw, "negate", "unaryMinus", "app.UnaryBox.unaryMinus", "lsp_kt_operator");
kotlin_implicit_probe_t contains = probe_kotlin_implicit_site(
raw, "hasValue", "contains", "app.Bag.contains", "lsp_kt_operator");
kotlin_implicit_probe_t not_contains = probe_kotlin_implicit_site(
raw, "lacksValue", "contains", "app.Bag.contains", "lsp_kt_operator");
kotlin_implicit_probe_t index =
probe_kotlin_implicit_site(raw, "lookup", "get", "app.Slots.get", "lsp_kt_operator");
kotlin_implicit_probe_t increment =
probe_kotlin_implicit_site(raw, "increment", "inc", "app.Counter.inc", "lsp_kt_operator");
kotlin_implicit_probe_t decrement =
probe_kotlin_implicit_site(raw, "decrement", "dec", "app.Counter.dec", "lsp_kt_operator");
kotlin_implicit_probe_t is_negative =
probe_kotlin_implicit_site(raw, "isBag", "contains", "app.Bag.contains", "lsp_kt_operator");
kotlin_implicit_probe_t assignment_negative =
probe_kotlin_implicit_site(raw, "assign", "get", "app.Slots.get", "lsp_kt_operator");
cbm_free_result(raw);
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_kt_nonbinary_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char definitions_path[512];
char callers_path[512];
snprintf(definitions_path, sizeof(definitions_path), "%s/app/Operators.kt", tmpdir);
snprintf(callers_path, sizeof(callers_path), "%s/app/Main.kt", tmpdir);
if (th_write_file(definitions_path, definitions_source) != 0 ||
th_write_file(callers_path, callers_source) != 0) {
th_rmtree(tmpdir);
FAIL("failed to write Kotlin nonbinary-operator fixture");
}
cbm_file_info_t files[2] = {0};
files[0].path = definitions_path;
files[0].rel_path = (char *)"app/Operators.kt";
files[0].language = CBM_LANG_KOTLIN;
files[1].path = callers_path;
files[1].rel_path = (char *)"app/Main.kt";
files[1].language = CBM_LANG_KOTLIN;
cbm_gbuf_t *sequential = run_sequential_with_lsp_cross(project, tmpdir, files, 2);
cbm_gbuf_t *parallel = run_parallel(project, tmpdir, files, 2, 2);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const bool sequential_unary =
find_calls_edge_by_tails(sequential, "Main.negate", "UnaryBox.unaryMinus") != NULL;
const bool parallel_unary =
find_calls_edge_by_tails(parallel, "Main.negate", "UnaryBox.unaryMinus") != NULL;
const bool sequential_contains =
find_calls_edge_by_tails(sequential, "Main.hasValue", "Bag.contains") != NULL;
const bool parallel_contains =
find_calls_edge_by_tails(parallel, "Main.hasValue", "Bag.contains") != NULL;
const bool sequential_not_contains =
find_calls_edge_by_tails(sequential, "Main.lacksValue", "Bag.contains") != NULL;
const bool parallel_not_contains =
find_calls_edge_by_tails(parallel, "Main.lacksValue", "Bag.contains") != NULL;
const bool sequential_index =
find_calls_edge_by_tails(sequential, "Main.lookup", "Slots.get") != NULL;
const bool parallel_index =
find_calls_edge_by_tails(parallel, "Main.lookup", "Slots.get") != NULL;
const bool sequential_increment =
find_calls_edge_by_tails(sequential, "Main.increment", "Counter.inc") != NULL;
const bool parallel_increment =
find_calls_edge_by_tails(parallel, "Main.increment", "Counter.inc") != NULL;
const bool sequential_decrement =
find_calls_edge_by_tails(sequential, "Main.decrement", "Counter.dec") != NULL;
const bool parallel_decrement =
find_calls_edge_by_tails(parallel, "Main.decrement", "Counter.dec") != NULL;
const bool sequential_false_contains =
find_calls_edge_by_tails(sequential, "Main.isBag", "Bag.contains") != NULL;
const bool parallel_false_contains =
find_calls_edge_by_tails(parallel, "Main.isBag", "Bag.contains") != NULL;
const bool sequential_false_get =
find_calls_edge_by_tails(sequential, "Main.assign", "Slots.get") != NULL;
const bool parallel_false_get =
find_calls_edge_by_tails(parallel, "Main.assign", "Slots.get") != NULL;
if (!unary.carrier || !unary.semantic || !unary.exact_join || !contains.carrier ||
!contains.semantic || !contains.exact_join || !not_contains.carrier ||
!not_contains.semantic || !not_contains.exact_join || !index.carrier || !index.semantic ||
!index.exact_join || !increment.carrier || !increment.semantic || !increment.exact_join ||
!decrement.carrier || !decrement.semantic || !decrement.exact_join || is_negative.carrier ||
is_negative.semantic || assignment_negative.carrier || !sequential_unary ||
!parallel_unary || !sequential_contains || !parallel_contains || !sequential_not_contains ||
!parallel_not_contains || !sequential_index || !parallel_index || !sequential_increment ||
!parallel_increment || !sequential_decrement || !parallel_decrement ||
sequential_false_contains || parallel_false_contains || sequential_false_get ||
parallel_false_get) {
printf(" Kotlin nonbinary diagnostic: unary=%d/%d/%d/%d/%d "
"contains=%d/%d/%d/%d/%d notin=%d/%d/%d/%d/%d "
"index=%d/%d/%d/%d/%d inc=%d/%d/%d/%d/%d dec=%d/%d/%d/%d/%d "
"negative=%d/%d/%d/%d/%d/%d\n",
unary.carrier, unary.semantic, unary.exact_join, sequential_unary, parallel_unary,
contains.carrier, contains.semantic, contains.exact_join, sequential_contains,
parallel_contains, not_contains.carrier, not_contains.semantic,
not_contains.exact_join, sequential_not_contains, parallel_not_contains,
index.carrier, index.semantic, index.exact_join, sequential_index, parallel_index,
increment.carrier, increment.semantic, increment.exact_join, sequential_increment,
parallel_increment, decrement.carrier, decrement.semantic, decrement.exact_join,
sequential_decrement, parallel_decrement, is_negative.carrier, is_negative.semantic,
assignment_negative.carrier, sequential_false_contains, parallel_false_contains,
sequential_false_get || parallel_false_get);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
th_rmtree(tmpdir);
ASSERT_TRUE(unary.carrier);
ASSERT_TRUE(unary.semantic);
ASSERT_TRUE(unary.exact_join);
ASSERT_TRUE(contains.carrier);
ASSERT_TRUE(contains.semantic);
ASSERT_TRUE(contains.exact_join);
ASSERT_TRUE(not_contains.carrier);
ASSERT_TRUE(not_contains.semantic);
ASSERT_TRUE(not_contains.exact_join);
ASSERT_TRUE(index.carrier);
ASSERT_TRUE(index.semantic);
ASSERT_TRUE(index.exact_join);
ASSERT_TRUE(increment.carrier);
ASSERT_TRUE(increment.semantic);
ASSERT_TRUE(increment.exact_join);
ASSERT_TRUE(decrement.carrier);
ASSERT_TRUE(decrement.semantic);
ASSERT_TRUE(decrement.exact_join);
ASSERT_FALSE(is_negative.carrier);
ASSERT_FALSE(is_negative.semantic);
ASSERT_FALSE(assignment_negative.carrier);
ASSERT_TRUE(sequential_unary);
ASSERT_TRUE(parallel_unary);
ASSERT_TRUE(sequential_contains);
ASSERT_TRUE(parallel_contains);
ASSERT_TRUE(sequential_not_contains);
ASSERT_TRUE(parallel_not_contains);
ASSERT_TRUE(sequential_index);
ASSERT_TRUE(parallel_index);
ASSERT_TRUE(sequential_increment);
ASSERT_TRUE(parallel_increment);
ASSERT_TRUE(sequential_decrement);
ASSERT_TRUE(parallel_decrement);
ASSERT_FALSE(sequential_false_contains);
ASSERT_FALSE(parallel_false_contains);
ASSERT_FALSE(sequential_false_get);
ASSERT_FALSE(parallel_false_get);
PASS();
}
/* Build the smallest real Cargo workspace that exercises the direct parallel
* resolve API. crate_a is a declared workspace member, while unlisted is a
* graph-visible decoy with the same helper name. That decoy is intentional:
* without manifest-aware routing, a bare-name match cannot accidentally make
* the test green. The optional crate_b-local helper pins the second failure
* mode where a confident in-file resolution used to suppress cross-LSP. */
static cbm_gbuf_t *run_issue56_parallel_workspace(bool local_decoy) {
static const char workspace_toml[] = "[workspace]\n"
"members = [\"crate_a\", \"crate_b\"]\n"
"resolver = \"2\"\n";
static const char crate_a_toml[] =
"[package]\nname = \"crate_a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n";
static const char crate_b_toml[] =
"[package]\nname = \"crate_b\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"
"\n[dependencies]\ncrate_a = { path = \"../crate_a\" }\n";
static const char crate_a_source[] = "pub fn helper() {}\n";
static const char unlisted_source[] = "pub fn helper() {}\n";
static const char caller_without_local[] = "fn run() { crate_a::helper(); }\n"
"fn main() { run(); }\n";
static const char caller_with_local[] = "fn helper() {}\n"
"fn run() { crate_a::helper(); }\n"
"fn main() { run(); }\n";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_issue56_parallel_XXXXXX");
if (!cbm_mkdtemp(tmpdir))
return NULL;
char crate_a[512];
char crate_a_src[512];
char crate_b[512];
char crate_b_src[512];
char unlisted[512];
char unlisted_src[512];
snprintf(crate_a, sizeof(crate_a), "%s/crate_a", tmpdir);
snprintf(crate_a_src, sizeof(crate_a_src), "%s/src", crate_a);
snprintf(crate_b, sizeof(crate_b), "%s/crate_b", tmpdir);
snprintf(crate_b_src, sizeof(crate_b_src), "%s/src", crate_b);
snprintf(unlisted, sizeof(unlisted), "%s/unlisted", tmpdir);
snprintf(unlisted_src, sizeof(unlisted_src), "%s/src", unlisted);
if (cbm_mkdir(crate_a) != 0 || cbm_mkdir(crate_a_src) != 0 || cbm_mkdir(crate_b) != 0 ||
cbm_mkdir(crate_b_src) != 0 || cbm_mkdir(unlisted) != 0 || cbm_mkdir(unlisted_src) != 0) {
th_rmtree(tmpdir);
return NULL;
}
char root_manifest[512];
char crate_a_manifest[512];
char crate_b_manifest[512];
char crate_a_file[512];
char crate_b_file[512];
char unlisted_file[512];
snprintf(root_manifest, sizeof(root_manifest), "%s/Cargo.toml", tmpdir);
snprintf(crate_a_manifest, sizeof(crate_a_manifest), "%s/Cargo.toml", crate_a);
snprintf(crate_b_manifest, sizeof(crate_b_manifest), "%s/Cargo.toml", crate_b);
snprintf(crate_a_file, sizeof(crate_a_file), "%s/lib.rs", crate_a_src);
snprintf(crate_b_file, sizeof(crate_b_file), "%s/main.rs", crate_b_src);
snprintf(unlisted_file, sizeof(unlisted_file), "%s/lib.rs", unlisted_src);
if (th_write_file(root_manifest, workspace_toml) != 0 ||
th_write_file(crate_a_manifest, crate_a_toml) != 0 ||
th_write_file(crate_b_manifest, crate_b_toml) != 0 ||
th_write_file(crate_a_file, crate_a_source) != 0 ||
th_write_file(crate_b_file, local_decoy ? caller_with_local : caller_without_local) != 0 ||
th_write_file(unlisted_file, unlisted_source) != 0) {
th_rmtree(tmpdir);
return NULL;
}
cbm_discover_opts_t opts = {.mode = CBM_MODE_FULL};
cbm_file_info_t *files = NULL;
int file_count = 0;
if (cbm_discover(tmpdir, &opts, &files, &file_count) != 0 || file_count < 3) {
cbm_discover_free(files, file_count);
th_rmtree(tmpdir);
return NULL;
}
cbm_gbuf_t *gbuf =
run_parallel("cbm_issue56_parallel", tmpdir, files, file_count, 2 /* real workers */);
cbm_discover_free(files, file_count);
th_rmtree(tmpdir);
return gbuf;
}
TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest) {
cbm_gbuf_t *gbuf = run_issue56_parallel_workspace(false);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *correct =
find_call_edge_to_target_fragment(gbuf, "main.run", ".crate_a.");
const bool correct_found = correct != NULL;
const bool unlisted = callable_has_call_target_fragment(gbuf, "main.run", ".unlisted.");
const bool manifest_strategy =
correct && correct->properties_json && strstr(correct->properties_json, "lsp_cross_crate");
if (!correct || unlisted || !manifest_strategy) {
printf(" issue56 manifest diagnostic: correct=%d unlisted=%d strategy=%d\n", correct_found,
unlisted, manifest_strategy);
}
cbm_gbuf_free(gbuf);
ASSERT_TRUE(correct_found);
ASSERT_FALSE(unlisted);
ASSERT_TRUE(manifest_strategy);
PASS();
}
TEST(parallel_rust_cross_crate_manifest_beats_confident_local_resolution) {
cbm_gbuf_t *gbuf = run_issue56_parallel_workspace(true);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *correct =
find_call_edge_to_target_fragment(gbuf, "main.run", ".crate_a.");
const bool correct_found = correct != NULL;
const bool wrong_local = callable_has_call_target_fragment(gbuf, "main.run", ".crate_b.");
const bool manifest_strategy =
correct && correct->properties_json && strstr(correct->properties_json, "lsp_cross_crate");
if (!correct || wrong_local || !manifest_strategy) {
printf(" issue56 local-shadow diagnostic: correct=%d wrong_local=%d strategy=%d\n",
correct_found, wrong_local, manifest_strategy);
}
cbm_gbuf_free(gbuf);
ASSERT_TRUE(correct_found);
ASSERT_FALSE(wrong_local);
ASSERT_TRUE(manifest_strategy);
PASS();
}
/* A known external macro semantic owns its parser occurrence even when the
* external target is not materialized as a graph node. It must not fall back
* to a same-named local function in either pipeline. */
TEST(parallel_rust_known_macro_does_not_fallback_to_local_function) {
static const char source[] = "fn println() {}\nfn run() { println!(\"macro\"); }\n";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_rust_macro_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/main.rs", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen main.rs failed");
}
fputs(source, file);
fclose(file);
CBMFileResult *extracted = cbm_extract_file(source, (int)strlen(source), CBM_LANG_RUST,
"cbm_rust_macro", "main.rs", 0, NULL, NULL);
ASSERT_NOT_NULL(extracted);
const char *site = strstr(source, "println!(\"macro\")");
const uint32_t start = site ? (uint32_t)(site - source) : 0;
const uint32_t end = start + (uint32_t)strlen("println!(\"macro\")");
bool semantic_owner = false;
bool exact_gated_carrier = false;
for (int i = 0; i < extracted->resolved_calls.count; i++) {
const CBMResolvedCall *resolved = &extracted->resolved_calls.items[i];
if (resolved->caller_qn && strstr(resolved->caller_qn, "run") && resolved->callee_qn &&
strcmp(resolved->callee_qn, "std.macros.println") == 0 && resolved->strategy &&
strcmp(resolved->strategy, "lsp_macro") == 0) {
semantic_owner = true;
}
}
for (int i = 0; i < extracted->calls.count; i++) {
const CBMCall *call = &extracted->calls.items[i];
if (call->enclosing_func_qn && strstr(call->enclosing_func_qn, "run") &&
call->callee_name && strcmp(call->callee_name, "println") == 0 &&
call->site_start_byte == start && call->site_end_byte == end &&
call->requires_lsp_resolution) {
exact_gated_carrier = true;
}
}
cbm_free_result(extracted);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"main.rs";
files[0].language = CBM_LANG_RUST;
cbm_gbuf_t *sequential = run_sequential("cbm_rust_macro", tmpdir, files, 1);
cbm_gbuf_t *parallel = run_parallel("cbm_rust_macro", tmpdir, files, 1, 1);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const bool sequential_nodes = find_unique_callable_node_by_tail(sequential, "main.run") &&
find_unique_callable_node_by_tail(sequential, "main.println");
const bool parallel_nodes = find_unique_callable_node_by_tail(parallel, "main.run") &&
find_unique_callable_node_by_tail(parallel, "main.println");
const bool sequential_wrong =
find_calls_edge_by_tails(sequential, "main.run", "main.println") != NULL;
const bool parallel_wrong =
find_calls_edge_by_tails(parallel, "main.run", "main.println") != NULL;
if (!semantic_owner || !exact_gated_carrier || !sequential_nodes || !parallel_nodes ||
sequential_wrong || parallel_wrong) {
printf(" Rust macro fallback diagnostic: semantic=%d gated_carrier=%d seq_nodes=%d "
"par_nodes=%d seq_wrong=%d par_wrong=%d\n",
semantic_owner, exact_gated_carrier, sequential_nodes, parallel_nodes,
sequential_wrong, parallel_wrong);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(semantic_owner);
ASSERT_TRUE(exact_gated_carrier);
ASSERT_TRUE(sequential_nodes);
ASSERT_TRUE(parallel_nodes);
ASSERT_FALSE(sequential_wrong);
ASSERT_FALSE(parallel_wrong);
PASS();
}
/* Attribute proc-macros are source-level decorator references. Both pipelines
* must represent them as DECORATES + USAGE and must not fabricate calls to
* implementation details that are knowable only by executing the macro. */
TEST(parallel_rust_proc_macros_are_decorates_and_usage_only) {
static const char source[] = "#[tokio::main]\nasync fn main() {}\n"
"#[tracing::instrument]\nfn work() {}\n";
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_rust_proc_macro_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char path[512];
snprintf(path, sizeof(path), "%s/main.rs", tmpdir);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen main.rs failed");
}
fputs(source, file);
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = (char *)"main.rs";
files[0].language = CBM_LANG_RUST;
cbm_gbuf_t *sequential = run_sequential("cbm_rust_proc_macro", tmpdir, files, 1);
cbm_gbuf_t *parallel = run_parallel("cbm_rust_proc_macro", tmpdir, files, 1, 1);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const bool seq_tokio_decorates = has_edge_from_callable_to_qn(
sequential, "main.main", "<decorator:tokio::main>", "DECORATES");
const bool seq_tokio_usage =
has_edge_from_callable_to_qn(sequential, "main.main", "<decorator:tokio::main>", "USAGE");
const bool par_tokio_decorates =
has_edge_from_callable_to_qn(parallel, "main.main", "<decorator:tokio::main>", "DECORATES");
const bool par_tokio_usage =
has_edge_from_callable_to_qn(parallel, "main.main", "<decorator:tokio::main>", "USAGE");
const bool seq_tracing_decorates = has_edge_from_callable_to_qn(
sequential, "main.work", "<decorator:tracing::instrument>", "DECORATES");
const bool seq_tracing_usage = has_edge_from_callable_to_qn(
sequential, "main.work", "<decorator:tracing::instrument>", "USAGE");
const bool par_tracing_decorates = has_edge_from_callable_to_qn(
parallel, "main.work", "<decorator:tracing::instrument>", "DECORATES");
const bool par_tracing_usage = has_edge_from_callable_to_qn(
parallel, "main.work", "<decorator:tracing::instrument>", "USAGE");
const bool seq_fake = callable_has_call_target_fragment(sequential, "main.main", "Runtime") ||
callable_has_call_target_fragment(sequential, "main.work", "Span.enter");
const bool par_fake = callable_has_call_target_fragment(parallel, "main.main", "Runtime") ||
callable_has_call_target_fragment(parallel, "main.work", "Span.enter");
if (!seq_tokio_decorates || !seq_tokio_usage || !par_tokio_decorates || !par_tokio_usage ||
!seq_tracing_decorates || !seq_tracing_usage || !par_tracing_decorates ||
!par_tracing_usage || seq_fake || par_fake) {
printf(" Rust proc-macro policy diagnostic: tokio seq=%d/%d par=%d/%d "
"tracing seq=%d/%d par=%d/%d fake=%d/%d\n",
seq_tokio_decorates, seq_tokio_usage, par_tokio_decorates, par_tokio_usage,
seq_tracing_decorates, seq_tracing_usage, par_tracing_decorates, par_tracing_usage,
seq_fake, par_fake);
}
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
unlink(path);
rmdir(tmpdir);
ASSERT_TRUE(seq_tokio_decorates);
ASSERT_TRUE(seq_tokio_usage);
ASSERT_TRUE(par_tokio_decorates);
ASSERT_TRUE(par_tokio_usage);
ASSERT_TRUE(seq_tracing_decorates);
ASSERT_TRUE(seq_tracing_usage);
ASSERT_TRUE(par_tracing_decorates);
ASSERT_TRUE(par_tracing_usage);
ASSERT_FALSE(seq_fake);
ASSERT_FALSE(par_fake);
PASS();
}
static int assert_c_family_preprocessed_collision_graph(CBMLanguage language, const char *extension,
const char *project, const char *source,
const char *target_tail) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_c_origin_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char rel_path[64];
snprintf(rel_path, sizeof(rel_path), "main.%s", extension);
char path[512];
snprintf(path, sizeof(path), "%s/%s", tmpdir, rel_path);
FILE *file = cbm_fopen(path, "w");
if (!file) {
rmdir(tmpdir);
FAIL("fopen C-family fixture failed");
}
fputs(source, file);
fclose(file);
cbm_file_info_t files[1] = {0};
files[0].path = path;
files[0].rel_path = rel_path;
files[0].language = language;
cbm_gbuf_t *sequential = run_sequential(project, tmpdir, files, 1);
cbm_gbuf_t *parallel = run_parallel(project, tmpdir, files, 1, 1);
ASSERT_NOT_NULL(sequential);
ASSERT_NOT_NULL(parallel);
const cbm_gbuf_edge_t *sequential_edge =
find_calls_edge_by_tails(sequential, "main.occurrence_probe", target_tail);
const cbm_gbuf_edge_t *parallel_edge =
find_calls_edge_by_tails(parallel, "main.occurrence_probe", target_tail);
if (!sequential_edge || !parallel_edge) {
printf(" C-family origin diagnostic (%s): sequential=%s parallel=%s\n", extension,
sequential_edge ? "present" : "absent", parallel_edge ? "present" : "absent");
}
ASSERT_NOT_NULL(sequential_edge);
ASSERT_NOT_NULL(parallel_edge);
cbm_gbuf_free(sequential);
cbm_gbuf_free(parallel);
unlink(path);
rmdir(tmpdir);
return 0;
}
TEST(parallel_c_preprocessed_coordinate_collision_preserves_hidden_target) {
static const char source[] =
"static int alpha_target(void){return 1;}\n"
"static int bravo_target(void){return 2;}\n"
"int occurrence_probe(void){\n"
" int (*fp)(void) = alpha_target;\n"
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ fp ( );\n"
"#define HIDDEN_FP_CALL() (fp = bravo_target, fp())\n"
" return HIDDEN_FP_CALL();\n"
"}\n";
return assert_c_family_preprocessed_collision_graph(CBM_LANG_C, "c", "cbm_c_origin", source,
"main.bravo_target");
}
static int assert_cpp_like_preprocessed_collision_graph(CBMLanguage language, const char *extension,
const char *project) {
static const char source[] =
"struct Alpha { int render(){return 1;} };\n"
"struct Bravo { int render(){return 2;} };\n"
"int occurrence_probe(){\n"
" Alpha alpha; Bravo bravo;\n"
" /*xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx*/ alpha . render ( );\n"
"#define HIDDEN_RENDER(x) x.render()\n"
" return HIDDEN_RENDER(bravo);\n"
"}\n";
return assert_c_family_preprocessed_collision_graph(language, extension, project, source,
"Bravo.render");
}
TEST(parallel_cpp_preprocessed_coordinate_collision_preserves_hidden_target) {
return assert_cpp_like_preprocessed_collision_graph(CBM_LANG_CPP, "cpp", "cbm_cpp_origin");
}
TEST(parallel_cuda_preprocessed_coordinate_collision_preserves_hidden_target) {
return assert_cpp_like_preprocessed_collision_graph(CBM_LANG_CUDA, "cu", "cbm_cuda_origin");
}
TEST(parallel_java_kotlin_lsp_override_cross_file_emits_lsp_strategy_edges) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_jvm_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char jpath[512];
snprintf(jpath, sizeof(jpath), "%s/src/main/java/com/example/Example.java", tmpdir);
char jdir[512];
snprintf(jdir, sizeof(jdir), "%s/src/main/java/com/example", tmpdir);
cbm_mkdir_p(jdir, 0755);
FILE *jf = cbm_fopen(jpath, "w");
if (!jf) {
FAIL("fopen example.java failed");
}
fprintf(jf, "package com.example;\n"
"\n"
"class JavaCaller {\n"
" String call(KotlinService kotlinService) {\n"
" return kotlinService.ping(new JavaService());\n"
" }\n"
"}\n"
"\n"
"class JavaService {\n"
" String pong() {\n"
" return \"pong\";\n"
" }\n"
"}\n");
fclose(jf);
char kpath[512];
snprintf(kpath, sizeof(kpath), "%s/src/main/kotlin/com/example/KotlinService.kt", tmpdir);
char kdir[512];
snprintf(kdir, sizeof(kdir), "%s/src/main/kotlin/com/example", tmpdir);
cbm_mkdir_p(kdir, 0755);
FILE *kf = cbm_fopen(kpath, "w");
if (!kf) {
unlink(jpath);
rmdir(tmpdir);
FAIL("fopen example.kt failed");
}
fprintf(kf, "package com.example\n"
"\n"
"class KotlinService {\n"
" fun ping(javaService: JavaService): String {\n"
" return javaService.pong()\n"
" }\n"
"}\n");
fclose(kf);
cbm_file_info_t files[2] = {0};
files[0].path = jpath;
files[0].rel_path = (char *)"src/main/java/com/example/Example.java";
files[0].language = CBM_LANG_JAVA;
files[1].path = kpath;
files[1].rel_path = (char *)"src/main/kotlin/com/example/KotlinService.kt";
files[1].language = CBM_LANG_KOTLIN;
cbm_gbuf_t *gbuf = run_parallel("com", tmpdir, files, 2, 2);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *java_to_kotlin =
find_calls_edge_by_tails(gbuf, "JavaCaller.call", "KotlinService.ping");
const cbm_gbuf_edge_t *kotlin_to_java =
find_calls_edge_by_tails(gbuf, "KotlinService.ping", "JavaService.pong");
ASSERT_NOT_NULL(java_to_kotlin);
ASSERT_NOT_NULL(kotlin_to_java);
ASSERT_NOT_NULL(java_to_kotlin->properties_json);
ASSERT_NOT_NULL(kotlin_to_java->properties_json);
ASSERT_NOT_NULL(strstr(java_to_kotlin->properties_json, "\"strategy\":\"lsp"));
ASSERT_NOT_NULL(strstr(kotlin_to_java->properties_json, "\"strategy\":\"lsp"));
ASSERT_TRUE(strstr(java_to_kotlin->properties_json, "\"strategy\":\"callee_suffix\"") == NULL);
ASSERT_TRUE(strstr(kotlin_to_java->properties_json, "\"strategy\":\"callee_suffix\"") == NULL);
cbm_gbuf_free(gbuf);
unlink(kpath);
unlink(jpath);
rmdir(tmpdir);
PASS();
}
/* #2053 contract for cbm_pipeline_rust_external_target: only a Rust row whose
* strategy names a registered target, and whose QN lies outside the project
* prefix, counts as external. The end-to-end probes live in test_pipeline.c
* (pipeline_rust_std_receiver_never_binds_project_method*). */
TEST(parallel_rust_external_target_contract) {
const char *proj = "proj";
/* External: registered std / seeded-crate targets. */
ASSERT_TRUE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_method_dispatch",
"std.path.Path.join", proj));
ASSERT_TRUE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_deref_dispatch",
"std.path.Path.join", proj));
ASSERT_TRUE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_constructor",
"core.sync.atomic.AtomicUsize.new", proj));
/* A prefix that is not a whole segment is still outside the project. */
ASSERT_TRUE(
cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_direct", "projx.helper", proj));
/* Project-prefixed targets keep the registry fallback. */
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_method_dispatch",
"proj.src.lib.EvidenceTier.join", proj));
/* Synthesized / non-registered strategies are not evidence. */
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_prelude_trait",
"std.path.PathBuf.clone", proj));
ASSERT_FALSE(
cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_unresolved", "root.join", proj));
/* Per-language: no other language is affected. */
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_GO, "lsp_method_dispatch",
"std.path.Path.join", proj));
/* Defensive NULL/empty inputs. */
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, NULL, "std.x", proj));
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_direct", "std.x", NULL));
ASSERT_FALSE(cbm_pipeline_rust_external_target(CBM_LANG_RUST, "lsp_direct", "", proj));
PASS();
}
/* Gate guard for the JVM-only unique-tail fallbacks (lsp_resolve.h).
*
* The tail fallbacks join LSP overrides across QN drift by unique
* "Class.method" leaf. That is only sound where class-per-file package
* semantics hold (Java/Kotlin); in any other language a single
* wrong-module coincidence would fabricate a CALLS edge, so
* cbm_pipeline_lsp_allow_tail_match must keep the fallbacks OFF there.
*
* NOTE: a natural end-to-end non-JVM coincidence fixture is impractical:
* reaching the fallbacks requires the LSP and the textual extraction to
* disagree on QN prefixes, which path-derived single-root languages do
* not produce in a small fixture (that drift is precisely the JVM
* mixed-source-root symptom the fallback exists for). So this test
* exercises the gated branches directly: the SAME wrong-module
* coincidence must resolve with the gate open (JVM) and must NOT with
* the gate closed. If the gate were removed — fallbacks made
* unconditional again — the gate-closed assertions below would fail. */
TEST(parallel_lsp_tail_match_fallbacks_gated_to_jvm) {
/* Policy: exactly the JVM languages. */
ASSERT_TRUE(cbm_pipeline_lsp_allow_tail_match(CBM_LANG_JAVA));
ASSERT_TRUE(cbm_pipeline_lsp_allow_tail_match(CBM_LANG_KOTLIN));
ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_PYTHON));
ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_GO));
ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_TYPESCRIPT));
ASSERT_TRUE(!cbm_pipeline_lsp_allow_tail_match(CBM_LANG_CPP));
/* Wrong-module coincidence: the resolved entry's caller shares only
* the "Service.handle" tail with the textual call's enclosing
* function, so the exact caller_qn pass misses and only the tail
* fallback could join them. */
CBMResolvedCall rc_item = {0};
rc_item.caller_qn = "com.example.pkg.Service.handle";
rc_item.callee_qn = "com.example.pkg.Helper.run";
rc_item.strategy = "lsp";
rc_item.confidence = 0.9f;
CBMResolvedCallArray arr = {0};
arr.items = &rc_item;
arr.count = 1;
arr.cap = 1;
CBMCall call = {0};
call.enclosing_func_qn = "proj.other_mod.Service.handle";
call.callee_name = "helper.run";
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&arr, &call, false) == NULL);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&arr, &call, true) == &rc_item);
/* Kotlin top-level functions have one more legitimate QN drift: the graph
* owner is file-shaped (`...Main.operatorCaller`) while the semantic owner
* is package-shaped (`app.operatorCaller`). An exact source occurrence and
* equal caller leaf may reconcile that JVM-only drift; the non-JVM policy
* remains closed, a different caller leaf remains closed, and distinct
* semantic targets at the same occurrence remain ambiguous. */
CBMResolvedCall top_level_rows[2] = {0};
top_level_rows[0].caller_qn = "app.operatorCaller";
top_level_rows[0].callee_qn = "app.Box.plus";
top_level_rows[0].strategy = "lsp_kt_operator";
top_level_rows[0].confidence = 0.9f;
top_level_rows[0].site_start_byte = 80;
top_level_rows[0].site_end_byte = 92;
top_level_rows[0].source_origin = CBM_SOURCE_ORIGIN_RAW;
CBMResolvedCallArray top_level_arr = {0};
top_level_arr.items = top_level_rows;
top_level_arr.count = 1;
top_level_arr.cap = 2;
CBMCall top_level_call = {0};
top_level_call.enclosing_func_qn = "proj.app.Main.operatorCaller";
top_level_call.callee_name = "plus";
top_level_call.site_start_byte = 80;
top_level_call.site_end_byte = 92;
top_level_call.source_origin = CBM_SOURCE_ORIGIN_RAW;
top_level_call.requires_lsp_resolution = true;
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&top_level_arr, &top_level_call, false) == NULL);
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&top_level_arr, &top_level_call, true) ==
&top_level_rows[0]);
top_level_call.enclosing_func_qn = "proj.app.Main.differentCaller";
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&top_level_arr, &top_level_call, true) == NULL);
top_level_call.enclosing_func_qn = "proj.app.Main.operatorCaller";
top_level_rows[1] = top_level_rows[0];
top_level_rows[1].callee_qn = "app.Other.plus";
top_level_arr.count = 2;
ASSERT_TRUE(cbm_pipeline_find_lsp_resolution(&top_level_arr, &top_level_call, true) == NULL);
/* Target-node fallback: callee_qn misses both as-is and
* project-prefixed; exactly one node coincidentally shares the
* "Helper.run" tail in an unrelated module. */
cbm_gbuf_t *tgbuf = cbm_gbuf_new("proj", "/tmp");
ASSERT_NOT_NULL(tgbuf);
int64_t nid = cbm_gbuf_upsert_node(tgbuf, "Method", "run", "proj.zeta.Helper.run",
"zeta/helper.py", 1, 3, NULL);
ASSERT_TRUE(nid != 0);
ASSERT_TRUE(cbm_pipeline_lsp_target_node(tgbuf, "proj", "com.other.Helper.run", false) == NULL);
const cbm_gbuf_node_t *jvm_hit =
cbm_pipeline_lsp_target_node(tgbuf, "proj", "com.other.Helper.run", true);
ASSERT_NOT_NULL(jvm_hit);
ASSERT_TRUE(strcmp(jvm_hit->qualified_name, "proj.zeta.Helper.run") == 0);
/* A synthetic external protocol target has no source-level evidence for
* that Class.method-tail equivalence. Its strict lookup must therefore
* remain exact and fail closed on the same coincidence. */
ASSERT_TRUE(cbm_pipeline_lsp_target_node_strict(tgbuf, "proj", "com.other.Helper.run", true) ==
NULL);
cbm_gbuf_free(tgbuf);
PASS();
}
TEST(parallel_python_lsp_override_emits_lsp_strategy_edges) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_pylsp_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
/* Single-file scenario: pins the in-file LSP path where py_lsp
* registers Greeter from the file's own defs, types `g = Greeter()`
* as NAMED("…Greeter"), and resolves `g.hello()` to Greeter.hello
* via attribute lookup. callee_qn matches the gbuf QN directly. The
* cross-file equivalent is covered by
* parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges,
* which exercises the project-prefix fallback in
* cbm_pipeline_lsp_target_node. */
char fpath0[512];
snprintf(fpath0, sizeof(fpath0), "%s/app.py", tmpdir);
FILE *f = fopen(fpath0, "w");
if (!f) {
FAIL("fopen app.py failed");
}
fprintf(f, "class Greeter:\n"
" def hello(self):\n"
" return 'hi'\n"
"\n"
"def main():\n"
" g = Greeter()\n"
" g.hello()\n");
fclose(f);
cbm_file_info_t files[1] = {0};
files[0].path = fpath0;
files[0].rel_path = (char *)"app.py";
files[0].language = CBM_LANG_PYTHON;
cbm_gbuf_t *gbuf = run_parallel("cbm_par_pylsp", tmpdir, files, 1, 1);
ASSERT_NOT_NULL(gbuf);
lsp_edge_count_ctx_t c = {0};
cbm_gbuf_foreach_edge(gbuf, count_lsp_call_edges, &c);
/* Sanity: extraction produced at least one call edge. */
ASSERT_GT(c.total_calls, 0);
/* The parallel pipeline must surface at least one LSP-attributed
* CALLS edge. This proves the unified cbm_pipeline_find_lsp_resolution
* (shared with pass_calls.c at floor 0.6) is actually consulted in
* the parallel pipeline, and that the resulting edge is emitted with
* the LSP strategy intact rather than overwritten by the registry
* fallback. */
ASSERT_GT(c.lsp_strategy_count, 0);
cbm_gbuf_free(gbuf);
unlink(fpath0);
rmdir(tmpdir);
PASS();
}
/* ── Go cross-package field-chain fixture (field_defs fold) ─────── */
/* Production's sequential driver seeds Folder nodes via pass_structure; the
* compact harness (run_sequential_with_lsp_cross_*) does not. Go imports
* resolve to Folder nodes, so a Go import-map fixture must seed File and
* Folder nodes itself — replicating the production shape. */
static cbm_gbuf_t *run_go_field_chain_sequential(const char *project, const char *repo_path,
cbm_file_info_t *files, int file_count) {
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, repo_path);
cbm_registry_t *reg = cbm_registry_new();
CBMFileResult **cache = (CBMFileResult **)calloc((size_t)file_count, sizeof(CBMFileResult *));
if (!gbuf || !reg || !cache) {
cbm_gbuf_free(gbuf);
cbm_registry_free(reg);
free(cache);
return NULL;
}
atomic_int cancelled;
atomic_init(&cancelled, 0);
cbm_pipeline_ctx_t ctx = {
.project_name = project,
.repo_path = repo_path,
.gbuf = gbuf,
.registry = reg,
.cancelled = &cancelled,
.result_cache = cache,
};
seed_test_file_nodes(gbuf, project, files, file_count);
char *svc_dir_qn = cbm_pipeline_fqn_folder(project, "svc");
if (svc_dir_qn) {
cbm_gbuf_upsert_node(gbuf, "Folder", "svc", svc_dir_qn, "svc", 0, 0, "{}");
free(svc_dir_qn);
}
cbm_init();
cbm_pipeline_pass_definitions(&ctx, files, file_count);
cbm_pipeline_pass_lsp_cross(&ctx, files, file_count, cache);
cbm_pipeline_pass_calls(&ctx, files, file_count);
cbm_pipeline_pass_usages(&ctx, files, file_count);
cbm_pipeline_pass_semantic(&ctx, files, file_count);
/* CBM_GO_FIELD_DIAG dump. NOTE: resolved-call records may borrow QN
* strings across file results (cross-file resolution in pass_lsp_cross),
* so every result must stay alive while ANY is inspected. Print all
* first, free all second. */
if (getenv("CBM_GO_FIELD_DIAG")) {
for (int i = 0; i < file_count; i++) {
if (!cache[i]) {
continue;
}
const CBMFileResult *r = cache[i];
printf(" [diag] file %s defs=%d imports=%d calls=%d resolved=%d\n", files[i].rel_path,
r->defs.count, r->imports.count, r->calls.count, r->resolved_calls.count);
for (int j = 0; j < r->resolved_calls.count; j++) {
const CBMResolvedCall *rc = &r->resolved_calls.items[j];
printf(" [diag] rc caller=%s callee=%s strategy=%s conf=%.2f kind=%d "
"span=[%u,%u)\n",
rc->caller_qn ? rc->caller_qn : "?", rc->callee_qn ? rc->callee_qn : "?",
rc->strategy ? rc->strategy : "?", rc->confidence, (int)rc->kind,
(unsigned)rc->site_start_byte, (unsigned)rc->site_end_byte);
}
for (int j = 0; j < r->calls.count; j++) {
const CBMCall *c = &r->calls.items[j];
printf(" [diag] call callee=%s enclosing=%s span=[%u,%u) req=%d\n",
c->callee_name ? c->callee_name : "?",
c->enclosing_func_qn ? c->enclosing_func_qn : "?",
(unsigned)c->site_start_byte, (unsigned)c->site_end_byte,
(int)c->requires_lsp_resolution);
}
for (int j = 0; j < r->defs.count; j++) {
const CBMDefinition *d = &r->defs.items[j];
printf(" [diag] def label=%s qn=%s parent=%s ret=%s\n",
d->label ? d->label : "?", d->qualified_name ? d->qualified_name : "?",
d->parent_class ? d->parent_class : "?",
d->return_type ? d->return_type : "?");
}
}
}
for (int i = 0; i < file_count; i++) {
cbm_free_result(cache[i]);
}
free(cache);
harness_ctx_free_tables(&ctx);
cbm_registry_free(reg);
if (ctx.seq_cross_arena_live) {
cbm_arena_destroy(&ctx.seq_cross_arena);
ctx.seq_cross_arena_live = false;
}
if (ctx.seq_cross_def_modules) {
for (int i = 0; i < ctx.seq_cross_def_module_count; i++) {
free(ctx.seq_cross_def_modules[i]);
}
free(ctx.seq_cross_def_modules);
ctx.seq_cross_def_modules = NULL;
}
return gbuf;
}
/* Cross-package field chains (h.S.Ping()) resolve end to end through the
* shared prebuilt Go registry. Regression for the field_defs fold: Go struct
* fields are extracted as flat "Field" definitions that pxc_map_label drops,
* so structs registered with zero fields and field chains never resolved.
* Mirrors the real-world handler shape — an app package holds an aliased
* cross-package field ("S *s.Svc") and app.Call calls through it. */
TEST(parallel_go_cross_package_field_chain_resolves) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_gofold_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char svc_path[512];
char app_path[512];
snprintf(svc_path, sizeof(svc_path), "%s/acme-order/internal/service/order_service.go", tmpdir);
snprintf(app_path, sizeof(app_path), "%s/acme-order/internal/handler/order_handler.go", tmpdir);
if (th_write_file(svc_path, "package service\n"
"\n"
"import (\n"
" \"context\"\n"
" pb \"acme-sdk/apis/order\"\n"
")\n"
"\n"
"type OrderService struct{}\n"
"\n"
"func (s *OrderService) PlaceOrder(ctx context.Context, req "
"*pb.PlaceOrderReq) (*pb.PlaceOrderRsp, error) {\n"
" return nil, nil\n"
"}\n"
"\n"
"func (s *OrderService) ListOrders(ctx context.Context, req "
"*pb.ListOrdersReq) (*pb.ListOrdersRsp, error) {\n"
" return nil, nil\n"
"}\n") != 0 ||
th_write_file(app_path, "package handler\n"
"\n"
"import (\n"
" \"context\"\n"
"\n"
" pb \"acme-sdk/apis/order\"\n"
"\n"
" \"acme-order/internal/service\"\n"
")\n"
"\n"
"type OrderHandler struct {\n"
" pb.UnimplementedOrderServer\n"
" cartSvc *service.CartService\n"
" userSvc *service.UserService\n"
" orderSvc *service.OrderService\n"
" inventorySvc *service.InventoryService\n"
" paymentSvc *service.PaymentService\n"
" shipmentSvc *service.ShipmentService\n"
" couponSvc *service.CouponService\n"
" searchSvc *service.SearchService\n"
"}\n"
"\n"
"func (h *OrderHandler) ListOrders(ctx context.Context, req "
"*pb.ListOrdersReq) (*pb.ListOrdersRsp, error) {\n"
" return h.orderSvc.ListOrders(ctx, req)\n"
"}\n"
"\n"
"func (h *OrderHandler) PlaceOrder(ctx context.Context, req "
"*pb.PlaceOrderReq) (*pb.PlaceOrderRsp, error) {\n"
" return h.orderSvc.PlaceOrder(ctx, req)\n"
"}\n"
"\n"
"func (h *OrderHandler) UpdateCart(ctx context.Context, req "
"*pb.UpdateCartReq) (*pb.UpdateCartRsp, error) {\n"
" return h.cartSvc.UpdateCart(ctx, req)\n"
"}\n") != 0) {
th_rmtree(tmpdir);
FAIL("write fixture failed");
}
cbm_file_info_t files[2] = {0};
files[0].path = svc_path;
files[0].rel_path = (char *)"acme-order/internal/service/order_service.go";
files[0].language = CBM_LANG_GO;
files[1].path = app_path;
files[1].rel_path = (char *)"acme-order/internal/handler/order_handler.go";
files[1].language = CBM_LANG_GO;
cbm_gbuf_t *gbuf = run_go_field_chain_sequential("go_field_fold", tmpdir, files, 2);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *edge =
find_call_edge_to_target_fragment(gbuf, "handler.PlaceOrder", ".service.PlaceOrder");
const bool found = edge != NULL;
const bool dispatch = edge && edge->properties_json &&
strstr(edge->properties_json, "\"strategy\":\"lsp_type_dispatch\"");
if (!found || !dispatch) {
printf(" go field chain diagnostic: found=%d dispatch=%d\n", found, dispatch);
if (edge && edge->properties_json) {
printf(" go field chain props: %s\n", edge->properties_json);
}
/* Dump all callable nodes and CALLS edges for cross-referencing */
{
const cbm_gbuf_node_t **nodes = NULL;
int ncount = 0;
if (cbm_gbuf_find_by_label(gbuf, "Function", &nodes, &ncount) == 0) {
for (int i = 0; i < ncount; i++) {
printf(" node Function: %s\n", nodes[i]->qualified_name);
}
}
if (cbm_gbuf_find_by_label(gbuf, "Method", &nodes, &ncount) == 0) {
for (int i = 0; i < ncount; i++) {
printf(" node Method: %s\n", nodes[i]->qualified_name);
}
}
cbm_gbuf_edge_visitor_fn edge_dump = NULL;
(void)edge_dump;
/* print every CALLS edge with endpoints */
const cbm_gbuf_edge_t **all = NULL;
int ecount = 0;
if (cbm_gbuf_find_edges_by_type(gbuf, "CALLS", &all, &ecount) == 0) {
for (int i = 0; i < ecount; i++) {
const cbm_gbuf_node_t *src =
all[i] ? cbm_gbuf_find_by_id(gbuf, all[i]->source_id) : NULL;
const cbm_gbuf_node_t *dst =
all[i] ? cbm_gbuf_find_by_id(gbuf, all[i]->target_id) : NULL;
printf(" CALLS edge: %s -> %s props=%s\n", src ? src->qualified_name : "?",
dst ? dst->qualified_name : "?",
all[i]->properties_json ? all[i]->properties_json : "{}");
}
}
}
}
cbm_gbuf_free(gbuf);
th_rmtree(tmpdir);
ASSERT_TRUE(found);
ASSERT_TRUE(dispatch);
PASS();
}
/* Cross-file regression for the QN-mismatch bug: py_lsp's per-file mode
* emits resolved_calls.callee_qn as the raw import-module path (e.g.
* `greeter.Greeter` from `from greeter import Greeter`) rather than the
* project-qualified QN the gbuf stores (`<project>.greeter.Greeter`).
* Before cbm_pipeline_lsp_target_node added the project-prefix fallback,
* the LSP match succeeded (lsp_overrides counter incremented) but the
* downstream cbm_gbuf_find_by_qn lookup missed silently, dropping the
* edge. With the fallback in place, the cross-file `g.hello()` call is
* attributed to <project>.greeter.Greeter.hello with an lsp_* strategy.
*
* Two-file scenario: greeter.py defines Greeter; app.py imports it and
* calls hello() — same shape as the original failing reproduction. */
TEST(parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_pylsp_xf_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char gpath[512];
snprintf(gpath, sizeof(gpath), "%s/greeter.py", tmpdir);
FILE *gf = fopen(gpath, "w");
if (!gf) {
FAIL("fopen greeter.py failed");
}
fprintf(gf, "class Greeter:\n"
" def hello(self):\n"
" return 'hi'\n");
fclose(gf);
char apath[512];
snprintf(apath, sizeof(apath), "%s/app.py", tmpdir);
FILE *af = fopen(apath, "w");
if (!af) {
unlink(gpath);
rmdir(tmpdir);
FAIL("fopen app.py failed");
}
fprintf(af, "from greeter import Greeter\n"
"\n"
"def main():\n"
" g = Greeter()\n"
" g.hello()\n");
fclose(af);
cbm_file_info_t files[2] = {0};
files[0].path = gpath;
files[0].rel_path = (char *)"greeter.py";
files[0].language = CBM_LANG_PYTHON;
files[1].path = apath;
files[1].rel_path = (char *)"app.py";
files[1].language = CBM_LANG_PYTHON;
cbm_gbuf_t *gbuf = run_parallel("cbm_par_pylsp_xf", tmpdir, files, 2, 2);
ASSERT_NOT_NULL(gbuf);
lsp_edge_count_ctx_t c = {0};
cbm_gbuf_foreach_edge(gbuf, count_lsp_call_edges, &c);
ASSERT_GT(c.total_calls, 0);
/* The cross-file LSP override must produce at least one lsp_*
* CALLS edge. Without the project-prefix fallback in
* cbm_pipeline_lsp_target_node this assertion would fail because the
* raw module-path callee_qn doesn't match the project-qualified
* gbuf node QN. */
ASSERT_GT(c.lsp_strategy_count, 0);
cbm_gbuf_free(gbuf);
unlink(apath);
unlink(gpath);
rmdir(tmpdir);
PASS();
}
/* RED/GREEN A — the graph-quality guarantee behind the low-RAM retention cap.
*
* The fused cross-file LSP step re-parses each file's source to resolve calls
* whose receiver type lives in ANOTHER file (the per-file pass cannot). When
* the retention cap drops a file's source, that resolution MUST still happen
* via a bounded on-demand re-read; otherwise the cross-file CALLS edge is LOST.
*
* Fixture: a Java<->Kotlin pair with genuinely cross-language calls that only
* the cross-file LSP resolves — JavaCaller.call -> KotlinService.ping (Java ->
* Kotlin) and KotlinService.ping -> JavaService.pong (Kotlin -> Java). These
* carry the "lsp" strategy and do NOT exist without the cross-file source,
* unlike same-file or import-local Python calls which the per-file pass already
* resolves (so counting lsp edges on those cannot detect the fallback).
*
* Three scenarios asserted GREEN with the re-read fallback in place:
* 1. CONTROL — default retention: both cross-file edges present. Proves the
* fixture genuinely produces them (non-vacuity guard).
* 2. NO-RETAIN — retain_sources=false: nothing retained -> edges survive only
* via the re-read fallback.
* 3. OVER-CAP — per-file cap = 1 byte: every file dropped by the SIZE cap ->
* edges survive only via the re-read fallback.
* On main (no fallback) scenarios 2 and 3 LOSE both edges = RED; scenario 1
* stays present = the non-vacuity control. */
TEST(parallel_cross_file_reread_preserves_unretained_edges) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_xf_reread_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char jpath[512];
snprintf(jpath, sizeof(jpath), "%s/src/main/java/com/example/Example.java", tmpdir);
char jdir[512];
snprintf(jdir, sizeof(jdir), "%s/src/main/java/com/example", tmpdir);
cbm_mkdir_p(jdir, 0755);
FILE *jf = fopen(jpath, "w");
if (!jf) {
FAIL("fopen Example.java failed");
}
fprintf(jf, "package com.example;\n"
"\n"
"class JavaCaller {\n"
" String call(KotlinService kotlinService) {\n"
" return kotlinService.ping(new JavaService());\n"
" }\n"
"}\n"
"\n"
"class JavaService {\n"
" String pong() {\n"
" return \"pong\";\n"
" }\n"
"}\n");
fclose(jf);
char kpath[512];
snprintf(kpath, sizeof(kpath), "%s/src/main/kotlin/com/example/KotlinService.kt", tmpdir);
char kdir[512];
snprintf(kdir, sizeof(kdir), "%s/src/main/kotlin/com/example", tmpdir);
cbm_mkdir_p(kdir, 0755);
FILE *kf = fopen(kpath, "w");
if (!kf) {
FAIL("fopen KotlinService.kt failed");
}
fprintf(kf, "package com.example\n"
"\n"
"class KotlinService {\n"
" fun ping(javaService: JavaService): String {\n"
" return javaService.pong()\n"
" }\n"
"}\n");
fclose(kf);
cbm_file_info_t files[2] = {0};
files[0].path = jpath;
files[0].rel_path = (char *)"src/main/java/com/example/Example.java";
files[0].language = CBM_LANG_JAVA;
files[1].path = kpath;
files[1].rel_path = (char *)"src/main/kotlin/com/example/KotlinService.kt";
files[1].language = CBM_LANG_KOTLIN;
/* CONTROL (retained) + two drop scenarios that reach the cross-file edge
* only via the on-demand re-read: NO-RETAIN disables retention entirely;
* OVER-CAP sets a 1-byte per-file cap so every file is dropped by size. */
const cbm_parallel_extract_opts_t no_retain = {
.retain_sources = false,
.retain_sources_set = true,
};
const cbm_parallel_extract_opts_t over_cap = {
.retain_sources = true,
.retain_sources_set = true,
.retain_per_file_max_bytes = 1, /* 1 byte → every file dropped by the size cap */
};
const cbm_parallel_extract_opts_t *scenarios[3] = {NULL, &no_retain, &over_cap};
for (int s = 0; s < 3; s++) {
cbm_gbuf_t *gbuf = run_parallel_with_extract_opts("com", tmpdir, files, 2, 2, scenarios[s]);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *java_to_kotlin =
find_calls_edge_by_tails(gbuf, "JavaCaller.call", "KotlinService.ping");
const cbm_gbuf_edge_t *kotlin_to_java =
find_calls_edge_by_tails(gbuf, "KotlinService.ping", "JavaService.pong");
/* Both cross-file (Java↔Kotlin) CALLS edges must be present in EVERY
* scenario. In the drop scenarios (s=1,2) the caller's source is NOT
* retained, so these edges exist ONLY because resolve_worker re-reads
* the source on demand. Without that fallback (main) they are LOST. */
ASSERT_NOT_NULL(java_to_kotlin);
ASSERT_NOT_NULL(kotlin_to_java);
/* And they must come from the source-dependent cross-file LSP, not a
* source-free suffix heuristic — proving the re-read actually ran. */
ASSERT_NOT_NULL(java_to_kotlin->properties_json);
ASSERT_NOT_NULL(strstr(java_to_kotlin->properties_json, "\"strategy\":\"lsp"));
ASSERT_NOT_NULL(kotlin_to_java->properties_json);
ASSERT_NOT_NULL(strstr(kotlin_to_java->properties_json, "\"strategy\":\"lsp"));
cbm_gbuf_free(gbuf);
}
th_rmtree(tmpdir);
PASS();
}
/* issue #294: gRPC service-name extraction must (a) preserve the canonical
* proto service name (FooServiceClient → FooService, not Foo) and (b) only
* match real stub/client types — ordinary receiver vars must NOT produce
* phantom __grpc__ Routes. */
TEST(grpc_service_name_preserves_service_suffix_issue294) {
char svc[256];
char meth[256];
/* Generated client class keeps the "Service" part of the name. */
ASSERT_TRUE(extract_grpc_service_method("pb.NewFooServiceClient.GetBar", svc, sizeof(svc), meth,
sizeof(meth)));
ASSERT_STR_EQ(svc, "FooService");
ASSERT_STR_EQ(meth, "GetBar");
/* Java-style ...ServiceGrpc strips only "Grpc". */
ASSERT_TRUE(extract_grpc_service_method("CartServiceGrpc.getCart", svc, sizeof(svc), meth,
sizeof(meth)));
ASSERT_STR_EQ(svc, "CartService");
/* BlockingStub wins over Stub (longest-suffix-first). */
ASSERT_TRUE(extract_grpc_service_method("CartServiceBlockingStub.getCart", svc, sizeof(svc),
meth, sizeof(meth)));
ASSERT_STR_EQ(svc, "CartService");
PASS();
}
TEST(grpc_no_phantom_route_from_plain_var_issue294) {
char svc[256];
char meth[256];
/* Ordinary receiver vars carry no gRPC stub suffix → must NOT match,
* so no phantom __grpc__provider/... or __grpc__builder/... Route. */
ASSERT_FALSE(
extract_grpc_service_method("_provider.GetGroup", svc, sizeof(svc), meth, sizeof(meth)));
ASSERT_FALSE(extract_grpc_service_method("_builder.AddSomeService", svc, sizeof(svc), meth,
sizeof(meth)));
ASSERT_FALSE(extract_grpc_service_method("logger.Info", svc, sizeof(svc), meth, sizeof(meth)));
PASS();
}
/* ── Shared "::" normalization in cbm_pipeline_find_lsp_resolution (QA F3) ─
*
* The last-"::"-segment normalization in lsp_resolve.h widens matching for
* qualified static callees (Perl `Pkg::sub`, C++ `Ns::fn`, etc.) across ALL
* languages, not just Perl. These tests lock the intended behavior directly
* against cbm_pipeline_find_lsp_resolution: (1) a qualified static call still
* resolves to the right resolved entry, and (2) the theoretical
* mis-attribution edge case (two same-named subs from different namespaces) is
* bounded by caller-QN equality + the confidence floor. */
static CBMResolvedCall make_rc(const char *caller, const char *callee, float conf) {
CBMResolvedCall rc = {0};
memset(&rc, 0, sizeof(rc));
rc.caller_qn = caller;
rc.callee_qn = callee;
rc.strategy = "test";
rc.confidence = conf;
return rc;
}
static CBMCall make_call(const char *enclosing, const char *callee_name) {
CBMCall c;
memset(&c, 0, sizeof(c));
c.enclosing_func_qn = enclosing;
c.callee_name = callee_name;
return c;
}
TEST(lsp_bare_segment_skips_preprocessor_spacing) {
/* simplecpp serializes macro-expanded member access with spaces around the
* separator (for example `bravo . render`). The shared semantic join must
* still compare the identifier leaf, not the whitespace-prefixed suffix. */
ASSERT_STR_EQ(cbm_lsp_bare_segment("bravo . render"), "render");
ASSERT_STR_EQ(cbm_lsp_bare_segment("ptr -> run"), "run");
PASS();
}
TEST(lsp_resolve_qualified_static_call_normalizes_colons) {
/* A qualified static call `Pkg::sub` (callee_name keeps the package
* prefix) must still match a resolved entry whose callee_qn short-name is
* the bare `sub`. This is the cross-language "::"-normalization contract. */
CBMResolvedCall items[] = {
make_rc("proj.mod.caller", "proj.Pkg.sub", 0.9f),
};
CBMResolvedCallArray arr = {items, 1, 1};
CBMCall call = make_call("proj.mod.caller", "Pkg::sub");
const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false);
ASSERT(hit != NULL);
ASSERT(strcmp(hit->callee_qn, "proj.Pkg.sub") == 0);
/* A bare call (no "::") to the same short name resolves identically —
* normalization must not regress the common case. */
CBMCall bare = make_call("proj.mod.caller", "sub");
const CBMResolvedCall *bare_hit = cbm_pipeline_find_lsp_resolution(&arr, &bare, false);
ASSERT(bare_hit != NULL);
ASSERT(strcmp(bare_hit->callee_qn, "proj.Pkg.sub") == 0);
PASS();
}
TEST(lsp_resolve_distinct_exact_caller_targets_fail_closed) {
/* Two same-named targets from different namespaces at the same caller and
* legacy occurrence are semantic ambiguity, not a confidence contest.
* Choosing the higher score would fabricate one exact CALLS target. */
CBMResolvedCall items[] = {
make_rc("proj.mod.caller", "proj.A.foo", 0.7f),
make_rc("proj.mod.caller", "proj.B.foo", 0.9f),
/* Below the confidence floor: must be ignored entirely. */
make_rc("proj.mod.caller", "proj.C.foo", 0.3f),
/* Different caller: must never match regardless of short-name. */
make_rc("proj.mod.other", "proj.D.foo", 0.95f),
};
CBMResolvedCallArray arr = {items, 4, 4};
CBMCall call = make_call("proj.mod.caller", "B::foo");
const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false);
ASSERT_NULL(hit);
/* The cross-caller high-confidence entry only matches its own caller. */
CBMCall other = make_call("proj.mod.other", "D::foo");
const CBMResolvedCall *other_hit = cbm_pipeline_find_lsp_resolution(&arr, &other, false);
ASSERT(other_hit != NULL);
ASSERT(strcmp(other_hit->callee_qn, "proj.D.foo") == 0);
/* A caller with no qualifying entry resolves to nothing (no widening can
* manufacture an edge across callers). */
CBMCall absent = make_call("proj.mod.absent", "foo");
ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &absent, false) == NULL);
PASS();
}
TEST(lsp_resolve_duplicate_exact_caller_rows_same_target_are_not_ambiguous) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 75;
call.site_end_byte = 91;
CBMResolvedCall low = make_rc("proj.mod.caller", "proj.Alpha.render", 0.75f);
low.site_start_byte = call.site_start_byte;
low.site_end_byte = call.site_end_byte;
CBMResolvedCall high = low;
high.confidence = 0.95f;
CBMResolvedCall items[] = {low, high};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &call, false) == &items[1]);
PASS();
}
TEST(lsp_resolve_distinct_exact_caller_site_targets_are_ambiguous) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 92;
call.site_end_byte = 108;
call.requires_lsp_resolution = true;
CBMResolvedCall alpha = make_rc("proj.mod.caller", "proj.Alpha.render", 0.75f);
alpha.site_start_byte = call.site_start_byte;
alpha.site_end_byte = call.site_end_byte;
CBMResolvedCall beta = make_rc("proj.mod.caller", "proj.Beta.render", 0.99f);
beta.site_start_byte = call.site_start_byte;
beta.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {alpha, beta};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT_NULL(cbm_pipeline_find_lsp_resolution(&arr, &call, false));
PASS();
}
/* Per-file and project-wide passes may spell one exact non-JVM invocation
* target with and without the project prefix. Graph identity, not raw string
* identity, decides whether those occurrence-identical rows conflict. */
TEST(lsp_resolve_project_prefixed_duplicate_is_not_ambiguous) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t target_id = cbm_gbuf_upsert_node(gbuf, "Function", "handler", "proj.mod.Target.handler",
"target.py", 1, 1, "{}");
ASSERT_GT(target_id, 0);
CBMCall call = make_call("proj.mod.Caller.run", "handler");
call.site_start_byte = 110;
call.site_end_byte = 119;
CBMResolvedCall raw = make_rc("proj.mod.Caller.run", "mod.Target.handler", 0.75f);
raw.site_start_byte = call.site_start_byte;
raw.site_end_byte = call.site_end_byte;
CBMResolvedCall prefixed = make_rc("proj.mod.Caller.run", "proj.mod.Target.handler", 0.90f);
prefixed.site_start_byte = call.site_start_byte;
prefixed.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {raw, prefixed};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_resolution_in_graph(&arr, &call, false, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT(hit == &items[1]);
PASS();
}
TEST(lsp_resolve_project_prefix_spellings_stay_ambiguous_when_both_nodes_exist) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t raw_id = cbm_gbuf_upsert_node(gbuf, "Function", "handler", "mod.Target.handler",
"raw.py", 1, 1, "{}");
int64_t prefixed_id = cbm_gbuf_upsert_node(
gbuf, "Function", "handler", "proj.mod.Target.handler", "prefixed.py", 1, 1, "{}");
ASSERT_GT(raw_id, 0);
ASSERT_GT(prefixed_id, 0);
ASSERT(raw_id != prefixed_id);
CBMCall call = make_call("proj.mod.Caller.run", "handler");
call.site_start_byte = 120;
call.site_end_byte = 129;
CBMResolvedCall raw = make_rc("proj.mod.Caller.run", "mod.Target.handler", 0.75f);
raw.site_start_byte = call.site_start_byte;
raw.site_end_byte = call.site_end_byte;
CBMResolvedCall prefixed = make_rc("proj.mod.Caller.run", "proj.mod.Target.handler", 0.90f);
prefixed.site_start_byte = call.site_start_byte;
prefixed.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {raw, prefixed};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_resolution_in_graph(&arr, &call, false, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT_NULL(hit);
PASS();
}
TEST(lsp_resolve_jvm_package_alias_of_materialized_target_is_not_ambiguous) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t target_id =
cbm_gbuf_upsert_node(gbuf, "Method", "twice", "proj.Util.twice", "Util.java", 1, 1, "{}");
ASSERT_GT(target_id, 0);
CBMCall call = make_call("proj.Client.run", "twice");
call.site_start_byte = 120;
call.site_end_byte = 128;
CBMResolvedCall local = make_rc("proj.Client.run", "proj.Util.twice", 0.92f);
local.site_start_byte = call.site_start_byte;
local.site_end_byte = call.site_end_byte;
CBMResolvedCall package = make_rc("proj.Client.run", "demo.Util.twice", 0.92f);
package.site_start_byte = call.site_start_byte;
package.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {local, package};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_resolution_in_graph(&arr, &call, true, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT_NOT_NULL(hit);
PASS();
}
TEST(lsp_resolve_jvm_two_tail_only_packages_stay_ambiguous) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t target_id = cbm_gbuf_upsert_node(gbuf, "Method", "run", "proj.generated.Service.run",
"Service.java", 1, 1, "{}");
ASSERT_GT(target_id, 0);
CBMCall call = make_call("proj.Client.call", "run");
call.site_start_byte = 130;
call.site_end_byte = 138;
CBMResolvedCall first = make_rc("proj.Client.call", "pkg1.Service.run", 0.80f);
first.site_start_byte = call.site_start_byte;
first.site_end_byte = call.site_end_byte;
CBMResolvedCall second = make_rc("proj.Client.call", "pkg2.Service.run", 0.90f);
second.site_start_byte = call.site_start_byte;
second.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {first, second};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_resolution_in_graph(&arr, &call, true, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT_NULL(hit);
PASS();
}
TEST(lsp_resolve_exact_site_beats_higher_confidence_legacy_record) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 100;
call.site_end_byte = 117;
CBMResolvedCall exact = make_rc("proj.mod.caller", "proj.Alpha.render", 0.80f);
exact.site_start_byte = call.site_start_byte;
exact.site_end_byte = call.site_end_byte;
/* A legacy resolver record has no occurrence span. Even with greater
* confidence it must not override the semantic record for this exact
* carrier occurrence. */
CBMResolvedCall legacy = make_rc("proj.mod.caller", "proj.Beta.render", 0.95f);
CBMResolvedCall items[] = {exact, legacy};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false);
ASSERT(hit == &items[0]);
ASSERT(strcmp(hit->callee_qn, "proj.Alpha.render") == 0);
PASS();
}
TEST(lsp_resolve_exact_site_beats_equal_confidence_legacy_record) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 200;
call.site_end_byte = 217;
/* Put the legacy record first to prove equal-confidence selection is not
* an array-order accident: occurrence exactness is the primary key. */
CBMResolvedCall legacy = make_rc("proj.mod.caller", "proj.Beta.render", 0.90f);
CBMResolvedCall exact = make_rc("proj.mod.caller", "proj.Alpha.render", 0.90f);
exact.site_start_byte = call.site_start_byte;
exact.site_end_byte = call.site_end_byte;
CBMResolvedCall items[] = {legacy, exact};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false);
ASSERT(hit == &items[1]);
ASSERT(strcmp(hit->callee_qn, "proj.Alpha.render") == 0);
PASS();
}
TEST(lsp_resolve_malformed_span_is_not_legacy_compatible) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 300;
call.site_end_byte = 317;
/* A reversed/partial span is corrupt occurrence metadata, not a legacy
* 0:0 record. It must be rejected even when it has greater confidence. */
CBMResolvedCall malformed = make_rc("proj.mod.caller", "proj.Wrong.render", 0.99f);
malformed.site_start_byte = 317;
malformed.site_end_byte = 300;
CBMResolvedCall legacy = make_rc("proj.mod.caller", "proj.Legacy.render", 0.75f);
CBMResolvedCall items[] = {malformed, legacy};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit = cbm_pipeline_find_lsp_resolution(&arr, &call, false);
ASSERT(hit == &items[1]);
PASS();
}
TEST(lsp_resolve_below_floor_exact_does_not_block_legacy) {
CBMCall call = make_call("proj.mod.caller", "receiver.render");
call.site_start_byte = 400;
call.site_end_byte = 417;
CBMResolvedCall below_floor = make_rc("proj.mod.caller", "proj.Exact.render", 0.55f);
below_floor.site_start_byte = call.site_start_byte;
below_floor.site_end_byte = call.site_end_byte;
CBMResolvedCall legacy = make_rc("proj.mod.caller", "proj.Legacy.render", 0.70f);
CBMResolvedCall items[] = {below_floor, legacy};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &call, false) == &items[1]);
PASS();
}
static CBMCall make_tail_call(uint32_t start, uint32_t end) {
CBMCall call = make_call("proj.raw.Service.handle", "helper.run");
call.site_start_byte = start;
call.site_end_byte = end;
return call;
}
static CBMResolvedCall make_tail_rc(const char *callee, float confidence, uint32_t start,
uint32_t end) {
CBMResolvedCall resolved = make_rc("com.example.generated.Service.handle", callee, confidence);
resolved.site_start_byte = start;
resolved.site_end_byte = end;
return resolved;
}
TEST(lsp_tail_exact_site_beats_legacy_regardless_of_order) {
CBMCall call = make_tail_call(500, 512);
CBMResolvedCall exact = make_tail_rc("com.example.Alpha.run", 0.80f, 500, 512);
CBMResolvedCall legacy = make_tail_rc("com.example.Beta.run", 0.99f, 0, 0);
CBMResolvedCall exact_first[] = {exact, legacy};
CBMResolvedCall legacy_first[] = {legacy, exact};
CBMResolvedCallArray first = {exact_first, 2, 2};
CBMResolvedCallArray second = {legacy_first, 2, 2};
ASSERT(cbm_pipeline_find_lsp_resolution(&first, &call, true) == &exact_first[0]);
ASSERT(cbm_pipeline_find_lsp_resolution(&second, &call, true) == &legacy_first[1]);
PASS();
}
TEST(lsp_tail_duplicate_exact_rows_same_target_are_not_ambiguous) {
CBMCall call = make_tail_call(600, 612);
CBMResolvedCall low = make_tail_rc("com.example.Helper.run", 0.75f, 600, 612);
CBMResolvedCall high = make_tail_rc("com.example.Helper.run", 0.90f, 600, 612);
CBMResolvedCall items[] = {low, high};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &call, true) == &items[1]);
PASS();
}
TEST(lsp_tail_distinct_exact_targets_are_ambiguous) {
CBMCall call = make_tail_call(700, 712);
CBMResolvedCall alpha = make_tail_rc("com.example.Alpha.run", 0.75f, 700, 712);
CBMResolvedCall beta = make_tail_rc("com.example.Beta.run", 0.99f, 700, 712);
CBMResolvedCall items[] = {alpha, beta};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT_NULL(cbm_pipeline_find_lsp_resolution(&arr, &call, true));
PASS();
}
TEST(lsp_tail_duplicate_legacy_rows_same_target_are_not_ambiguous) {
CBMCall call = make_tail_call(800, 812);
CBMResolvedCall low = make_tail_rc("com.example.Helper.run", 0.75f, 0, 0);
CBMResolvedCall high = make_tail_rc("com.example.Helper.run", 0.90f, 0, 0);
CBMResolvedCall items[] = {low, high};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_resolution(&arr, &call, true) == &items[1]);
PASS();
}
TEST(lsp_tail_distinct_legacy_targets_are_ambiguous) {
CBMCall call = make_tail_call(900, 912);
CBMResolvedCall alpha = make_tail_rc("com.example.Alpha.run", 0.75f, 0, 0);
CBMResolvedCall beta = make_tail_rc("com.example.Beta.run", 0.99f, 0, 0);
CBMResolvedCall items[] = {alpha, beta};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT_NULL(cbm_pipeline_find_lsp_resolution(&arr, &call, true));
PASS();
}
static CBMUsage make_reference_usage(const char *caller, const char *name, uint32_t start,
uint32_t end) {
CBMUsage usage = {0};
usage.enclosing_func_qn = caller;
usage.ref_name = name;
usage.kind = CBM_USAGE_CALL_REFERENCE;
usage.site_start_byte = start;
usage.site_end_byte = end;
return usage;
}
static CBMResolvedCall make_reference_rc(const char *caller, const char *callee, float confidence,
uint32_t start, uint32_t end) {
CBMResolvedCall resolved = make_rc(caller, callee, confidence);
resolved.kind = CBM_RESOLVED_CALL_REFERENCE;
resolved.site_start_byte = start;
resolved.site_end_byte = end;
return resolved;
}
TEST(lsp_reference_duplicate_exact_rows_same_target_are_not_ambiguous) {
CBMUsage usage = make_reference_usage("proj.mod.Caller.run", "handler", 1000, 1007);
CBMResolvedCall low =
make_reference_rc("proj.mod.Caller.run", "proj.Target.handler", 0.75f, 1000, 1007);
CBMResolvedCall high =
make_reference_rc("proj.mod.Caller.run", "proj.Target.handler", 0.90f, 1000, 1007);
CBMResolvedCall items[] = {low, high};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_reference(&arr, &usage, false) == &items[1]);
PASS();
}
/* Per-file LSP rows may carry a package-shaped target while a cross-file row
* carries the same graph QN with the project prefix already attached. Those
* spellings identify one target and must not trip the distinct-target
* ambiguity guard. */
TEST(lsp_reference_project_prefixed_duplicate_is_not_ambiguous) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t target_id = cbm_gbuf_upsert_node(gbuf, "Function", "handler", "proj.mod.Target.handler",
"target.go", 1, 1, "{}");
ASSERT_GT(target_id, 0);
const cbm_gbuf_node_t *raw_target =
cbm_pipeline_lsp_target_node(gbuf, project, "mod.Target.handler", false);
const cbm_gbuf_node_t *prefixed_target =
cbm_pipeline_lsp_target_node(gbuf, project, "proj.mod.Target.handler", false);
ASSERT_NOT_NULL(raw_target);
ASSERT_NOT_NULL(prefixed_target);
ASSERT_EQ(raw_target->id, prefixed_target->id);
CBMUsage usage = make_reference_usage("proj.mod.Caller.run", "handler", 1050, 1057);
CBMResolvedCall raw =
make_reference_rc("proj.mod.Caller.run", "mod.Target.handler", 0.75f, 1050, 1057);
CBMResolvedCall prefixed =
make_reference_rc("proj.mod.Caller.run", "proj.mod.Target.handler", 0.90f, 1050, 1057);
CBMResolvedCall items[] = {raw, prefixed};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_reference_in_graph(&arr, &usage, false, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT(hit == &items[1]);
PASS();
}
TEST(lsp_reference_project_prefix_spellings_stay_ambiguous_when_both_nodes_exist) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t raw_id = cbm_gbuf_upsert_node(gbuf, "Function", "handler", "mod.Target.handler",
"raw.go", 1, 1, "{}");
int64_t prefixed_id = cbm_gbuf_upsert_node(
gbuf, "Function", "handler", "proj.mod.Target.handler", "prefixed.go", 1, 1, "{}");
ASSERT_GT(raw_id, 0);
ASSERT_GT(prefixed_id, 0);
ASSERT(raw_id != prefixed_id);
CBMUsage usage = make_reference_usage("proj.mod.Caller.run", "handler", 1070, 1077);
CBMResolvedCall raw =
make_reference_rc("proj.mod.Caller.run", "mod.Target.handler", 0.75f, 1070, 1077);
CBMResolvedCall prefixed =
make_reference_rc("proj.mod.Caller.run", "proj.mod.Target.handler", 0.90f, 1070, 1077);
CBMResolvedCall items[] = {raw, prefixed};
CBMResolvedCallArray arr = {items, 2, 2};
const CBMResolvedCall *hit =
cbm_pipeline_find_lsp_reference_in_graph(&arr, &usage, false, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT_NULL(hit);
PASS();
}
/* A JVM tail lookup may recover one materialized graph node when package- and
* path-shaped QNs drift, but it must not erase semantic disagreement between
* two resolver rows. `pkg1.Service.handler` and `pkg2.Service.handler` are
* distinct source targets even when each independently falls back to the same
* unique `Service.handler` graph node. Collapsing them would let row order or
* confidence fabricate an exact CALL_REFERENCE instead of failing closed. */
TEST(lsp_reference_jvm_distinct_packages_do_not_collapse_via_tail_node) {
const char *project = "proj";
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t materialized_id = cbm_gbuf_upsert_node(
gbuf, "Method", "handler", "proj.generated.Service.handler", "Service.kt", 1, 1, "{}");
ASSERT_GT(materialized_id, 0);
CBMUsage usage = make_reference_usage("proj.consumer.Caller.run", "handler", 1080, 1087);
CBMResolvedCall pkg1 =
make_reference_rc("proj.consumer.Caller.run", "pkg1.Service.handler", 0.75f, 1080, 1087);
CBMResolvedCall pkg2 =
make_reference_rc("proj.consumer.Caller.run", "pkg2.Service.handler", 0.90f, 1080, 1087);
CBMResolvedCall pkg1_first[] = {pkg1, pkg2};
CBMResolvedCall pkg2_first[] = {pkg2, pkg1};
CBMResolvedCallArray first = {pkg1_first, 2, 2};
CBMResolvedCallArray second = {pkg2_first, 2, 2};
const cbm_gbuf_node_t *pkg1_tail =
cbm_pipeline_lsp_target_node(gbuf, project, pkg1.callee_qn, true);
const cbm_gbuf_node_t *pkg2_tail =
cbm_pipeline_lsp_target_node(gbuf, project, pkg2.callee_qn, true);
bool same_tail_node = pkg1_tail && pkg2_tail && pkg1_tail->id == materialized_id &&
pkg2_tail->id == materialized_id;
const CBMResolvedCall *first_hit =
cbm_pipeline_find_lsp_reference_in_graph(&first, &usage, true, gbuf, project);
const CBMResolvedCall *second_hit =
cbm_pipeline_find_lsp_reference_in_graph(&second, &usage, true, gbuf, project);
cbm_gbuf_free(gbuf);
ASSERT_TRUE(same_tail_node);
ASSERT_NULL(first_hit);
ASSERT_NULL(second_hit);
PASS();
}
TEST(lsp_reference_distinct_exact_targets_are_order_invariant) {
CBMUsage usage = make_reference_usage("proj.mod.Caller.run", "handler", 1100, 1107);
CBMResolvedCall alpha =
make_reference_rc("proj.mod.Caller.run", "proj.Alpha.handler", 0.90f, 1100, 1107);
CBMResolvedCall beta =
make_reference_rc("proj.mod.Caller.run", "proj.Beta.handler", 0.90f, 1100, 1107);
CBMResolvedCall alpha_first[] = {alpha, beta};
CBMResolvedCall beta_first[] = {beta, alpha};
CBMResolvedCallArray first = {alpha_first, 2, 2};
CBMResolvedCallArray second = {beta_first, 2, 2};
const CBMResolvedCall *first_hit = cbm_pipeline_find_lsp_reference(&first, &usage, false);
const CBMResolvedCall *second_hit = cbm_pipeline_find_lsp_reference(&second, &usage, false);
bool invariant = (!first_hit && !second_hit) ||
(first_hit && second_hit && first_hit->callee_qn && second_hit->callee_qn &&
strcmp(first_hit->callee_qn, second_hit->callee_qn) == 0);
ASSERT_TRUE(invariant);
PASS();
}
TEST(lsp_reference_duplicate_tail_rows_same_target_are_not_ambiguous) {
CBMUsage usage = make_reference_usage("proj.raw.Service.handle", "handler", 1200, 1207);
CBMResolvedCall low =
make_reference_rc("com.generated.Service.handle", "com.Target.handler", 0.75f, 1200, 1207);
CBMResolvedCall high =
make_reference_rc("com.generated.Service.handle", "com.Target.handler", 0.90f, 1200, 1207);
CBMResolvedCall items[] = {low, high};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT(cbm_pipeline_find_lsp_reference(&arr, &usage, true) == &items[1]);
PASS();
}
TEST(lsp_reference_distinct_tail_targets_are_ambiguous) {
CBMUsage usage = make_reference_usage("proj.raw.Service.handle", "handler", 1300, 1307);
CBMResolvedCall alpha =
make_reference_rc("com.generated.Service.handle", "com.Alpha.handler", 0.75f, 1300, 1307);
CBMResolvedCall beta =
make_reference_rc("com.generated.Service.handle", "com.Beta.handler", 0.99f, 1300, 1307);
CBMResolvedCall items[] = {alpha, beta};
CBMResolvedCallArray arr = {items, 2, 2};
ASSERT_NULL(cbm_pipeline_find_lsp_reference(&arr, &usage, true));
PASS();
}
TEST(lsp_target_node_supports_long_prefixed_qualified_name) {
const char *project = "long_project";
enum { CALLEE_LEN = 1100 };
char *callee = malloc(CALLEE_LEN + 1U);
char *qualified = malloc(strlen(project) + 1U + CALLEE_LEN + 1U);
ASSERT_NOT_NULL(callee);
ASSERT_NOT_NULL(qualified);
memset(callee, 'n', CALLEE_LEN);
callee[CALLEE_LEN] = '\0';
snprintf(qualified, strlen(project) + 1U + CALLEE_LEN + 1U, "%s.%s", project, callee);
cbm_gbuf_t *gbuf = cbm_gbuf_new(project, "/tmp");
ASSERT_NOT_NULL(gbuf);
int64_t id =
cbm_gbuf_upsert_node(gbuf, "Function", "target", qualified, "target.c", 1, 1, "{}");
ASSERT_GT(id, 0);
const cbm_gbuf_node_t *found = cbm_pipeline_lsp_target_node(gbuf, project, callee, false);
if (!found) {
printf(" long target-QN diagnostic: project=%zu callee=%zu combined=%zu\n",
strlen(project), strlen(callee), strlen(qualified));
}
bool same_node = found && found->id == id;
cbm_gbuf_free(gbuf);
free(qualified);
free(callee);
ASSERT_TRUE(same_node);
PASS();
}
/* ── Suite Registration ──────────────────────────────────────────── */
/* ── PHP vendor-imported types never bind by name (#1186) ─────────── */
/* Foo.php imports `Request` from a vendor namespace no project file declares,
* so the import stays unresolved. The project has its own class Request
* (another namespace) and a TypeScript function sendAsync. Every edge from
* Foo.php that names the vendor Request -- its base class, calls on a
* Request-typed receiver, the `new`, the type references -- used to bind by
* name to those project symbols. Svc is imported from a declared project
* namespace and is the control that must keep resolving. */
static const struct {
const char *rel;
CBMLanguage lang;
const char *src;
} k_php_vendor_files[] = {
{"lib/Requests/Request.php", CBM_LANG_PHP,
"<?php\nnamespace App\\Http\\Requests;\n\nclass Request {\n"
" public function validated() { return 1; }\n"
" public static function makeIt() { return 2; }\n"
" public function get($k) { return $k; }\n}\n"},
{"lib/Services/Svc.php", CBM_LANG_PHP,
"<?php\nnamespace App\\Services;\n\nclass Svc {\n"
" public function go() { return 3; }\n"
" public static function build() { return 4; }\n}\n"},
{"web/api.ts", CBM_LANG_TYPESCRIPT, "export function sendAsync(x: number) { return x; }\n"},
{"lib/Integrations/Foo.php", CBM_LANG_PHP,
"<?php\nnamespace App\\Http\\Integrations;\n\n"
"use Saloon\\Http\\Request;\nuse App\\Services\\Svc;\n\n"
"class Foo extends Request {\n"
" public function run(Request $r, Svc $s) {\n"
" $r->validated();\n"
" Request::makeIt();\n"
" $made = new Request();\n"
" $s->go();\n"
" Svc::build();\n"
" $r->get('id');\n"
" return $r->sendAsync(1);\n"
" }\n}\n\n"
"class Bar extends Svc {\n}\n"},
{"lib/Integrations/Clients.php", CBM_LANG_PHP,
"<?php\nnamespace App\\Http\\Integrations;\n"
"use GuzzleHttp\\Client as GuzzleClient;\nuse Kafka\\Producer as KafkaProducer;\n"
"class Clients { function run() {\n"
" GuzzleClient::get('/api/vendor-health');\n"
" KafkaProducer::publish('vendor-orders', 'payload');\n}\n}\n"},
};
enum { PHP_VENDOR_FILE_COUNT = (int)(sizeof(k_php_vendor_files) / sizeof(k_php_vendor_files[0])) };
typedef struct {
const cbm_gbuf_t *gbuf;
const char *type;
const char *src_rel;
const char *tgt_rel;
int count;
int source_nodes;
bool query_failed;
} php_file_edge_count_t;
static void php_count_source_edges(const cbm_gbuf_node_t *source, void *ud) {
php_file_edge_count_t *c = ud;
if (!source->file_path || strcmp(source->file_path, c->src_rel) != 0)
return;
c->source_nodes++;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(c->gbuf, source->id, c->type, &edges, &count) != 0) {
c->query_failed = true;
return;
}
for (int i = 0; i < count; i++) {
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(c->gbuf, edges[i]->target_id);
if (!target) {
c->query_failed = true;
} else if (target->file_path && strcmp(target->file_path, c->tgt_rel) == 0) {
c->count++;
}
}
}
static int php_file_edges(const cbm_gbuf_t *gbuf, const char *type, const char *source,
const char *target) {
char *source_qn = cbm_pipeline_fqn_compute("cbm_par_phpvendor", source, "__file__");
char *target_qn = cbm_pipeline_fqn_compute("cbm_par_phpvendor", target, "__file__");
const cbm_gbuf_node_t *s = source_qn ? cbm_gbuf_find_by_qn(gbuf, source_qn) : NULL;
const cbm_gbuf_node_t *t = target_qn ? cbm_gbuf_find_by_qn(gbuf, target_qn) : NULL;
bool files_exist = s && t && s->label && t->label && strcmp(s->label, "File") == 0 &&
strcmp(t->label, "File") == 0;
free(source_qn);
free(target_qn);
if (!files_exist)
return -1;
php_file_edge_count_t c = {.gbuf = gbuf, .type = type, .src_rel = source, .tgt_rel = target};
cbm_gbuf_foreach_node(gbuf, php_count_source_edges, &c);
return c.query_failed || c.source_nodes == 0 ? -1 : c.count;
}
static bool php_vendor_extracted_sites(void) {
for (int file = 3; file < PHP_VENDOR_FILE_COUNT; file++) {
const char *source = k_php_vendor_files[file].src;
CBMFileResult *r =
cbm_extract_file(source, (int)strlen(source), CBM_LANG_PHP, "cbm_par_phpvendor",
k_php_vendor_files[file].rel, 0, NULL, NULL);
if (!r)
return false;
bool ok = r->defs.count > 0 && r->imports.count == 2;
const char *core[] = {"validated", "makeIt", "go", "build", "get", "sendAsync"};
const char *service[] = {"get", "publish"};
const char *const *wanted = file == 3 ? core : service;
int wanted_count = file == 3 ? 6 : 2;
for (int i = 0; i < wanted_count; i++) {
int count = 0;
for (int j = 0; j < r->calls.count; j++) {
const CBMCall *c = &r->calls.items[j];
if (!c->callee_name || !c->enclosing_func_qn ||
strcmp(cbm_pipeline_call_callee_leaf(c->callee_name), wanted[i]) != 0 ||
c->site_end_byte <= c->site_start_byte || c->site_end_byte > strlen(source))
continue;
if (file == 4) {
const char *arg = i == 0 ? "/api/vendor-health" : "vendor-orders";
const char *alias = i == 0 ? "GuzzleClient::" : "KafkaProducer::";
/* The extractor names a scoped call by its method alone
* (it keeps only a literal `Route::` scope, #952), so the
* aliased scope is proven on the occurrence text. */
if (!c->first_string_arg || strcmp(c->first_string_arg, arg) != 0 ||
strncmp(source + c->site_start_byte, alias, strlen(alias)) != 0)
continue;
}
count++;
}
ok = count == 1 && ok;
}
cbm_free_result(r);
if (!ok)
return false;
}
return true;
}
static bool php_vendor_service_edge(const cbm_gbuf_t *gb, const char *type, const char *name) {
const cbm_gbuf_node_t *source = find_unique_callable_node_by_tail(gb, "Clients.run");
if (!source)
return false;
const cbm_gbuf_edge_t **edges = NULL;
int count = 0;
if (cbm_gbuf_find_edges_by_source_type(gb, source->id, type, &edges, &count) != 0 || count != 1)
return false;
const cbm_gbuf_node_t *target = cbm_gbuf_find_by_id(gb, edges[0]->target_id);
return target && target->label && strcmp(target->label, "Route") == 0 &&
edges[0]->properties_json && strstr(edges[0]->properties_json, name);
}
static void php_vendor_edge_fp(const cbm_gbuf_edge_t *edge, void *ud) {
if (strcmp(edge->type, "CALLS") == 0 || strcmp(edge->type, "USAGE") == 0 ||
strcmp(edge->type, "INHERITS") == 0 || strcmp(edge->type, "IMPORTS") == 0 ||
strcmp(edge->type, "HTTP_CALLS") == 0 || strcmp(edge->type, "ASYNC_CALLS") == 0)
fp_visit_edge(edge, ud);
}
/* 1 when the graph holds no vendor-named edge and every control edge. */
static int php_vendor_graph_ok(const cbm_gbuf_t *gbuf, const char *driver) {
static const char *const foo = "lib/Integrations/Foo.php";
static const char *const req = "lib/Requests/Request.php";
static const char *const svc = "lib/Services/Svc.php";
struct {
const char *type;
const char *tgt;
int want_min;
int want_max;
} checks[] = {
{"INHERITS", req, 0, 0}, {"CALLS", req, 0, 0}, {"USAGE", req, 0, 0},
{"CALLS", "web/api.ts", 0, 0}, {"INHERITS", svc, 1, 1}, {"CALLS", svc, 2, 2},
{"CALLS", foo, 0, 0}, {"USAGE", svc, 1, 1 << 20},
};
int ok = 1;
for (size_t i = 0; i < sizeof(checks) / sizeof(checks[0]); i++) {
int got = php_file_edges(gbuf, checks[i].type, foo, checks[i].tgt);
if (got < checks[i].want_min || got > checks[i].want_max) {
printf(" [%s] %s Foo.php -> %s: got %d, want %d..%d\n", driver, checks[i].type,
checks[i].tgt, got, checks[i].want_min, checks[i].want_max);
ok = 0;
}
}
return ok && php_vendor_service_edge(gbuf, "HTTP_CALLS", "/api/vendor-health") &&
php_vendor_service_edge(gbuf, "ASYNC_CALLS", "vendor-orders");
}
TEST(parallel_php_vendor_types_never_bind_by_name_issue1186) {
ASSERT_TRUE(php_vendor_extracted_sites());
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_phpvendor_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
FAIL("mkdtemp failed");
}
char paths[PHP_VENDOR_FILE_COUNT][512];
cbm_file_info_t files[PHP_VENDOR_FILE_COUNT];
memset(files, 0, sizeof(files));
for (int i = 0; i < PHP_VENDOR_FILE_COUNT; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, k_php_vendor_files[i].rel);
char dir[512];
snprintf(dir, sizeof(dir), "%s", paths[i]);
*strrchr(dir, '/') = '\0';
cbm_mkdir_p(dir, 0755);
FILE *f = fopen(paths[i], "w");
if (!f) {
th_rmtree(tmpdir);
FAIL("fopen failed");
}
bool wrote = fputs(k_php_vendor_files[i].src, f) >= 0;
bool closed = fclose(f) == 0;
if (!wrote || !closed) {
th_rmtree(tmpdir);
FAIL("fixture write failed");
}
files[i].path = paths[i];
files[i].rel_path = (char *)k_php_vendor_files[i].rel;
files[i].language = k_php_vendor_files[i].lang;
}
cbm_gbuf_t *seq = run_sequential_with_lsp_cross_and_mutator(
"cbm_par_phpvendor", tmpdir, files, PHP_VENDOR_FILE_COUNT, NULL, NULL, true);
cbm_gbuf_t *par = run_parallel_with_extract_opts_and_mutator(
"cbm_par_phpvendor", tmpdir, files, PHP_VENDOR_FILE_COUNT, 2, NULL, NULL, NULL, true);
const size_t saved_budget = cbm_mem_budget();
const size_t budget = (size_t)1024 * 1024 * 1024;
cbm_mem_set_budget_for_tests(budget);
cbm_mem_set_charged_for_tests(budget - budget / 16);
g_harness_spill = true;
int cached = -1;
cbm_gbuf_t *spilled = run_parallel_with_extract_opts_and_mutator(
"cbm_par_phpvendor", tmpdir, files, PHP_VENDOR_FILE_COUNT, 2, NULL, count_cached_results,
&cached, true);
int64_t parked = g_harness_parked;
g_harness_spill = false;
cbm_mem_set_charged_for_tests(0);
cbm_mem_set_budget_for_tests(saved_budget);
int seq_ok = seq ? php_vendor_graph_ok(seq, "sequential") : 0;
int par_ok = par ? php_vendor_graph_ok(par, "parallel") : 0;
int spill_ok = spilled ? php_vendor_graph_ok(spilled, "spilled") : 0;
graph_fp_t seq_edges = {.gb = seq}, par_edges = {.gb = par};
graph_fp_t par_all = {0}, spill_all = {0};
if (seq)
cbm_gbuf_foreach_edge(seq, php_vendor_edge_fp, &seq_edges);
if (par) {
cbm_gbuf_foreach_edge(par, php_vendor_edge_fp, &par_edges);
par_all = graph_fingerprint(par);
}
if (spilled)
spill_all = graph_fingerprint(spilled);
bool edge_parity = seq_edges.count > 0 && seq_edges.count == par_edges.count &&
seq_edges.sum == par_edges.sum && seq_edges.xr == par_edges.xr;
bool spill_parity = par_all.count > 0 && par_all.count == spill_all.count &&
par_all.sum == spill_all.sum && par_all.xr == spill_all.xr;
cbm_gbuf_free(seq);
cbm_gbuf_free(par);
cbm_gbuf_free(spilled);
th_rmtree(tmpdir);
ASSERT_EQ(cached, 0);
ASSERT_EQ((int)parked, PHP_VENDOR_FILE_COUNT);
ASSERT_TRUE(seq_ok);
ASSERT_TRUE(par_ok);
ASSERT_TRUE(spill_ok);
ASSERT_TRUE(edge_parity);
ASSERT_TRUE(spill_parity);
PASS();
}
/* ── PHP LSP types receivers of imported project classes (#1186) ─── */
/* Foo.php calls methods on receivers typed with classes imported from OTHER
* project files: a promoted property (`$this->svc->go()`) and an aliased
* import (`R $r` -> `$r->find()`). The import map handed to the PHP LSP held
* the IMPORTS edge target -- the imported file's File node QN
* (`...Svc.php.__file__`) -- as the class, so no receiver ever typed and the
* calls fell to name-only strategies (the #1186 "useful calls at 0.38"). The
* decoy App\Other\Svc shares the short name and the method, so a name-only
* match cannot tell the two apart. Controls: the vendor-typed receiver
* (`Client $c`) must stay unresolved, and the same-file calls keep their LSP
* strategies. */
static const struct {
const char *rel;
const char *src;
} k_php_xfile_files[] = {
{"lib/Services/Svc.php", "<?php\nnamespace App\\Services;\n\nclass Svc {\n"
" public function go() { return 1; }\n}\n"},
{"lib/Other/Svc.php", "<?php\nnamespace App\\Other;\n\nclass Svc {\n"
" public function go() { return 2; }\n}\n"},
{"lib/Repo/Repo.php", "<?php\nnamespace App\\Repo;\n\nclass Repo {\n"
" public function find() { return 3; }\n}\n"},
{"lib/Http/Foo.php",
"<?php\nnamespace App\\Http;\n\n"
"use App\\Services\\Svc;\nuse App\\Repo\\Repo as R;\nuse Vendor\\Pkg\\Client;\n\n"
"class Foo {\n"
" public function __construct(private Svc $svc) {}\n"
" public function run(R $r, Client $c) {\n"
" $this->svc->go();\n"
" $r->find();\n"
" $c->go();\n"
" $this->local();\n"
" (new Local())->ping();\n"
" }\n"
" private function local() { return 4; }\n}\n\n"
"class Local {\n"
" public function ping() { return 5; }\n}\n"},
};
enum { PHP_XFILE_FILE_COUNT = (int)(sizeof(k_php_xfile_files) / sizeof(k_php_xfile_files[0])) };
typedef struct {
const cbm_gbuf_t *gbuf;
const char *src_rel;
const char *tgt_rel;
const char *tgt_suffix;
const char *strategy_prefix;
int total;
int with_strategy;
} php_xfile_edge_count_t;
static void php_xfile_count_edge(const cbm_gbuf_edge_t *edge, void *ud) {
php_xfile_edge_count_t *c = ud;
if (!edge->type || strcmp(edge->type, "CALLS") != 0) {
return;
}
const cbm_gbuf_node_t *s = cbm_gbuf_find_by_id(c->gbuf, edge->source_id);
const cbm_gbuf_node_t *t = cbm_gbuf_find_by_id(c->gbuf, edge->target_id);
if (!s || !t || !s->file_path || !t->file_path || !t->qualified_name ||
strcmp(s->file_path, c->src_rel) != 0 || strcmp(t->file_path, c->tgt_rel) != 0) {
return;
}
size_t qlen = strlen(t->qualified_name);
size_t slen = strlen(c->tgt_suffix);
if (qlen < slen || strcmp(t->qualified_name + qlen - slen, c->tgt_suffix) != 0) {
return;
}
c->total++;
char needle[128];
snprintf(needle, sizeof(needle), "\"strategy\":\"%s", c->strategy_prefix);
if (edge->properties_json && strstr(edge->properties_json, needle)) {
c->with_strategy++;
}
}
/* 1 when every cross-file receiver resolved through the PHP LSP, the decoy and
* the vendor receiver drew nothing, and the same-file controls held. */
static int php_xfile_graph_ok(const cbm_gbuf_t *gbuf, const char *driver) {
static const char *const foo = "lib/Http/Foo.php";
struct {
const char *tgt;
const char *suffix;
int want_total;
int want_lsp;
} checks[] = {
{"lib/Services/Svc.php", ".Svc.go", 1, 1}, /* $this->svc->go(), not $c->go() */
{"lib/Other/Svc.php", ".Svc.go", 0, 0}, /* decoy: same short name + method */
{"lib/Repo/Repo.php", ".Repo.find", 1, 1}, /* aliased import */
{foo, ".Foo.local", 1, 1}, /* same-file control */
{foo, ".Local.ping", 1, 1}, /* same-file control */
};
int ok = 1;
for (size_t i = 0; i < sizeof(checks) / sizeof(checks[0]); i++) {
php_xfile_edge_count_t c = {gbuf, foo, checks[i].tgt, checks[i].suffix, "php_", 0, 0};
cbm_gbuf_foreach_edge(gbuf, php_xfile_count_edge, &c);
if (c.total != checks[i].want_total || c.with_strategy != checks[i].want_lsp) {
printf(" [%s] CALLS Foo.php -> %s%s: %d edge(s), %d via php_*; want %d, %d\n", driver,
checks[i].tgt, checks[i].suffix, c.total, c.with_strategy, checks[i].want_total,
checks[i].want_lsp);
ok = 0;
}
}
return ok;
}
/* Run the fixture on both venues. extra_use (nullable) is one more `use` line
* added to Foo.php. Returns 1 when both graphs pass, 0 when not, -1 on setup
* failure. */
static int php_xfile_run(const char *extra_use) {
char tmpdir[256];
snprintf(tmpdir, sizeof(tmpdir), "/tmp/cbm_par_phpxfile_XXXXXX");
if (!cbm_mkdtemp(tmpdir)) {
return -1;
}
char paths[PHP_XFILE_FILE_COUNT][512];
cbm_file_info_t files[PHP_XFILE_FILE_COUNT];
memset(files, 0, sizeof(files));
static const char foo_use[] = "use Vendor\\Pkg\\Client;\n";
for (int i = 0; i < PHP_XFILE_FILE_COUNT; i++) {
snprintf(paths[i], sizeof(paths[i]), "%s/%s", tmpdir, k_php_xfile_files[i].rel);
char dir[512];
snprintf(dir, sizeof(dir), "%s", paths[i]);
*strrchr(dir, '/') = '\0';
cbm_mkdir_p(dir, 0755);
char src[2048];
const char *s = k_php_xfile_files[i].src;
const char *at = extra_use ? strstr(s, foo_use) : NULL;
if (at) {
size_t head = (size_t)(at - s) + sizeof(foo_use) - 1;
snprintf(src, sizeof(src), "%.*s%s%s", (int)head, s, extra_use, s + head);
s = src;
}
if (th_write_file(paths[i], s) != 0) {
th_rmtree(tmpdir);
return -1;
}
files[i].path = paths[i];
files[i].rel_path = (char *)k_php_xfile_files[i].rel;
files[i].language = CBM_LANG_PHP;
}
cbm_gbuf_t *seq = run_sequential_with_lsp_cross_and_mutator(
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, NULL, NULL, true);
cbm_gbuf_t *par = run_parallel_with_extract_opts_and_mutator(
"cbm_par_phpxfile", tmpdir, files, PHP_XFILE_FILE_COUNT, 2, NULL, NULL, NULL, true);
int seq_ok = seq ? php_xfile_graph_ok(seq, "sequential") : 0;
int par_ok = par ? php_xfile_graph_ok(par, "parallel") : 0;
cbm_gbuf_free(seq);
cbm_gbuf_free(par);
th_rmtree(tmpdir);
return seq_ok && par_ok;
}
TEST(parallel_php_lsp_types_imported_project_receivers_issue1186) {
ASSERT_EQ(php_xfile_run(NULL), 1);
PASS();
}
/* The same file with one more import that no file in the project declares,
* the global class `use Exception;` written in most namespaced PHP files.
* The typed import evidence leaves such a file "unknown" as a whole (#2520),
* so the class-valued import mapping must not depend on that evidence. */
TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186) {
ASSERT_EQ(php_xfile_run("use Exception;\n"), 1);
PASS();
}
/* A fresh PHP external row replaces stale evidence for its exact occurrence,
* including an older external row, without clearing a real project invocation
* of the same method at the next occurrence. Test both old-row orders. */
TEST(parallel_php_external_row_replaces_only_its_exact_occurrence) {
const char *source = "<?php\nnamespace App;\nuse Vendor\\Http\\Request;\n"
"use App\\Project\\Request as LocalRequest;\n"
"class Caller { function run(Request $r, LocalRequest $p) {\n"
" $r->validated();\n $p->validated();\n}\n}\n";
const char *project_type = "p.lib.Request.Request";
CBMLSPDef defs[] = {
{.qualified_name = project_type,
.short_name = "Request",
.label = "Class",
.def_module_qn = "p.lib.Request",
.lang = CBM_LANG_PHP},
{.qualified_name = "p.lib.Request.Request.validated",
.short_name = "validated",
.label = "Method",
.receiver_type = project_type,
.def_module_qn = "p.lib.Request",
.lang = CBM_LANG_PHP},
};
for (int order = 0; order < 2; order++) {
CBMFileResult *r = cbm_extract_file(source, (int)strlen(source), CBM_LANG_PHP, "p",
"main.php", 0, NULL, NULL);
ASSERT_NOT_NULL(r);
ASSERT_EQ(r->imports.count, 2);
const CBMCall *sites[2] = {0};
int count = 0;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *call = &r->calls.items[i];
if (call->enclosing_func_qn && strstr(call->enclosing_func_qn, "Caller.run") &&
call->callee_name &&
strcmp(cbm_pipeline_call_callee_leaf(call->callee_name), "validated") == 0 &&
call->site_end_byte > call->site_start_byte) {
ASSERT_TRUE(count < 2);
sites[count++] = call;
}
}
ASSERT_EQ(count, 2);
if (sites[0]->site_start_byte > sites[1]->site_start_byte) {
const CBMCall *swap = sites[0];
sites[0] = sites[1];
sites[1] = swap;
}
ASSERT_TRUE(sites[0]->site_end_byte <= sites[1]->site_start_byte);
CBMResolvedCall stale = {0};
stale.caller_qn = sites[0]->enclosing_func_qn;
stale.callee_qn = "p.lib.Request.Request.validated";
stale.strategy = "test_stale_project";
stale.confidence = 0.99f;
stale.kind = CBM_RESOLVED_INVOCATION;
stale.site_start_byte = sites[0]->site_start_byte;
stale.site_end_byte = sites[0]->site_end_byte;
stale.source_origin = sites[0]->source_origin;
CBMResolvedCall old_external = stale;
old_external.callee_qn = "Old.Vendor.Request.validated";
old_external.strategy = "php_external_receiver";
CBMResolvedCall separate = stale;
separate.strategy = "test_separate_project";
separate.site_start_byte = sites[1]->site_start_byte;
separate.site_end_byte = sites[1]->site_end_byte;
separate.source_origin = sites[1]->source_origin;
r->resolved_calls.count = 0;
cbm_resolvedcall_push(&r->resolved_calls, &r->arena, order ? old_external : stale);
cbm_resolvedcall_push(&r->resolved_calls, &r->arena, order ? stale : old_external);
cbm_resolvedcall_push(&r->resolved_calls, &r->arena, separate);
const char *targets[2] = {0};
for (int i = 0; i < r->imports.count; i++)
if (strcmp(r->imports.items[i].local_name, "LocalRequest") == 0)
targets[i] = "p.lib.Request.__file__";
CBMPHPImportBindings bindings = {.imports = &r->imports, .targets = targets};
const char *keys[] = {"LocalRequest"};
const char *vals[] = {project_type};
cbm_pxc_run_one_with_php_bindings(CBM_LANG_PHP, r, source, (int)strlen(source), "p.main",
defs, 2, keys, vals, 1, &bindings);
int external_count = 0, project_count = 0;
for (int i = 0; i < r->resolved_calls.count; i++) {
const CBMResolvedCall *row = &r->resolved_calls.items[i];
if (!row->caller_qn || strcmp(row->caller_qn, sites[0]->enclosing_func_qn) != 0 ||
row->kind != CBM_RESOLVED_INVOCATION)
continue;
if (row->site_start_byte == sites[0]->site_start_byte &&
row->site_end_byte == sites[0]->site_end_byte &&
row->source_origin == sites[0]->source_origin) {
ASSERT_STR_EQ(row->callee_qn, "Vendor.Http.Request.validated");
ASSERT_STR_EQ(row->strategy, "php_external_receiver");
external_count++;
} else if (row->site_start_byte == sites[1]->site_start_byte &&
row->site_end_byte == sites[1]->site_end_byte &&
row->source_origin == sites[1]->source_origin) {
ASSERT_STR_EQ(row->callee_qn, "p.lib.Request.Request.validated");
project_count++;
}
}
ASSERT_EQ(external_count, 1);
ASSERT_EQ(project_count, 1);
const CBMResolvedCall *vendor =
cbm_pipeline_find_lsp_resolution(&r->resolved_calls, sites[0], false);
const CBMResolvedCall *project =
cbm_pipeline_find_lsp_resolution(&r->resolved_calls, sites[1], false);
ASSERT_NOT_NULL(vendor);
ASSERT_NOT_NULL(project);
ASSERT_STR_EQ(vendor->strategy, "php_external_receiver");
ASSERT_STR_EQ(project->callee_qn, "p.lib.Request.Request.validated");
cbm_free_result(r);
}
PASS();
}
SUITE(parallel) {
RUN_TEST(parallel_php_external_row_replaces_only_its_exact_occurrence);
RUN_TEST(parallel_php_vendor_types_never_bind_by_name_issue1186);
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_issue1186);
RUN_TEST(parallel_php_lsp_types_imported_project_receivers_unknown_bindings_issue1186);
RUN_TEST(usage_semantic_reference_candidate_trusts_marked_producer);
RUN_TEST(lsp_bare_segment_skips_preprocessor_spacing);
RUN_TEST(lsp_resolve_qualified_static_call_normalizes_colons);
RUN_TEST(lsp_resolve_distinct_exact_caller_targets_fail_closed);
RUN_TEST(lsp_resolve_duplicate_exact_caller_rows_same_target_are_not_ambiguous);
RUN_TEST(lsp_resolve_distinct_exact_caller_site_targets_are_ambiguous);
RUN_TEST(lsp_resolve_project_prefixed_duplicate_is_not_ambiguous);
RUN_TEST(lsp_resolve_project_prefix_spellings_stay_ambiguous_when_both_nodes_exist);
RUN_TEST(lsp_resolve_jvm_package_alias_of_materialized_target_is_not_ambiguous);
RUN_TEST(lsp_resolve_jvm_two_tail_only_packages_stay_ambiguous);
RUN_TEST(lsp_resolve_exact_site_beats_higher_confidence_legacy_record);
RUN_TEST(lsp_resolve_exact_site_beats_equal_confidence_legacy_record);
RUN_TEST(lsp_resolve_malformed_span_is_not_legacy_compatible);
RUN_TEST(lsp_resolve_below_floor_exact_does_not_block_legacy);
RUN_TEST(lsp_tail_exact_site_beats_legacy_regardless_of_order);
RUN_TEST(lsp_tail_duplicate_exact_rows_same_target_are_not_ambiguous);
RUN_TEST(lsp_tail_distinct_exact_targets_are_ambiguous);
RUN_TEST(lsp_tail_duplicate_legacy_rows_same_target_are_not_ambiguous);
RUN_TEST(lsp_tail_distinct_legacy_targets_are_ambiguous);
RUN_TEST(lsp_reference_duplicate_exact_rows_same_target_are_not_ambiguous);
RUN_TEST(lsp_reference_project_prefixed_duplicate_is_not_ambiguous);
RUN_TEST(lsp_reference_project_prefix_spellings_stay_ambiguous_when_both_nodes_exist);
RUN_TEST(lsp_reference_jvm_distinct_packages_do_not_collapse_via_tail_node);
RUN_TEST(lsp_reference_distinct_exact_targets_are_order_invariant);
RUN_TEST(lsp_reference_duplicate_tail_rows_same_target_are_not_ambiguous);
RUN_TEST(lsp_reference_distinct_tail_targets_are_ambiguous);
RUN_TEST(lsp_target_node_supports_long_prefixed_qualified_name);
RUN_TEST(grpc_service_name_preserves_service_suffix_issue294);
RUN_TEST(grpc_no_phantom_route_from_plain_var_issue294);
/* Graph buffer merge/shared-ID tests */
RUN_TEST(gbuf_shared_ids_unique);
RUN_TEST(gbuf_merge_nodes);
RUN_TEST(gbuf_merge_edges);
RUN_TEST(gbuf_merge_empty_src);
RUN_TEST(gbuf_merge_src_free_safe);
RUN_TEST(gbuf_next_id_accessors);
RUN_TEST(gbuf_raise_next_id_never_lowers);
RUN_TEST(parallel_full_index_keeps_edge_ids_unique);
RUN_TEST(parallel_closure_repair_syncs_ids_through_the_helper);
/* Parallel pipeline parity tests */
RUN_TEST(parallel_node_count);
RUN_TEST(parallel_python_lsp_override_emits_lsp_strategy_edges);
RUN_TEST(parallel_lsp_index_exact_site_beats_legacy_record_in_graph);
RUN_TEST(parallel_lsp_index_distinct_exact_targets_fail_closed_in_graph);
RUN_TEST(parallel_lsp_index_exact_ambiguity_does_not_fall_through_to_legacy);
RUN_TEST(parallel_lsp_long_exact_key_never_yields_legacy_target);
RUN_TEST(parallel_lsp_exact_index_handles_repeated_synthetic_occurrences_without_linear_scan);
#if defined(CBM_CALL_REFERENCE_LOOKUP_TEST_API) && CBM_CALL_REFERENCE_LOOKUP_TEST_API
RUN_TEST(parallel_call_reference_lookup_rows_grow_linearly);
#endif
RUN_TEST(parallel_tsx_local_component_shadow_has_no_false_call);
RUN_TEST(parallel_exact_semantic_noncallable_target_stays_usage);
RUN_TEST(parallel_typescript_module_value_usage_respects_lexical_shadows);
RUN_TEST(parallel_typescript_import_namespace_exact_parity);
RUN_TEST(parallel_tsx_import_namespace_exact_parity);
RUN_TEST(parallel_csharp_same_name_class_binds_by_namespace);
RUN_TEST(parallel_kotlin_external_protocol_does_not_use_project_class_method_tail);
RUN_TEST(parallel_kotlin_nonbinary_operator_carriers_reach_graph);
RUN_TEST(parallel_rust_cross_crate_worker_receives_workspace_manifest);
RUN_TEST(parallel_rust_cross_crate_manifest_beats_confident_local_resolution);
RUN_TEST(parallel_rust_known_macro_does_not_fallback_to_local_function);
RUN_TEST(parallel_rust_proc_macros_are_decorates_and_usage_only);
RUN_TEST(parallel_c_preprocessed_coordinate_collision_preserves_hidden_target);
RUN_TEST(parallel_cpp_preprocessed_coordinate_collision_preserves_hidden_target);
RUN_TEST(parallel_cuda_preprocessed_coordinate_collision_preserves_hidden_target);
RUN_TEST(parallel_python_lsp_override_cross_file_emits_lsp_strategy_edges);
RUN_TEST(parallel_go_cross_package_field_chain_resolves);
RUN_TEST(parallel_cross_file_reread_preserves_unretained_edges);
RUN_TEST(parallel_java_kotlin_lsp_override_cross_file_emits_lsp_strategy_edges);
RUN_TEST(parallel_rust_external_target_contract);
RUN_TEST(parallel_lsp_tail_match_fallbacks_gated_to_jvm);
RUN_TEST(parallel_calls_parity);
RUN_TEST(parallel_defines_parity);
RUN_TEST(parallel_defines_method_parity);
RUN_TEST(parallel_imports_parity);
RUN_TEST(parallel_usage_parity);
RUN_TEST(parallel_inherits_parity);
RUN_TEST(parallel_implements_parity);
RUN_TEST(parallel_semantic_fixture_expected_counts);
RUN_TEST(parallel_total_edges);
RUN_TEST(parallel_spill_mode_builds_the_same_graph);
RUN_TEST(parallel_post_extract_projection_spills_before_resolve);
RUN_TEST(parallel_empty_files);
RUN_TEST(parallel_args_json_no_overflow);
RUN_TEST(parallel_wide_definition_keeps_fp_sp_bt_f14);
/* Cleanup shared state */
parity_teardown();
}