From fdce258a9d981ccbcd6127a397df0382706feeec Mon Sep 17 00:00:00 2001 From: Andrew Hundt Date: Fri, 24 Jul 2026 00:41:37 -0400 Subject: [PATCH] fix(pipeline): reject weak calls to data symbols A unique_name registry fallback could bind a C call such as format() to an unrelated Variable extracted from a JSON schema. Registry insertion order then changed the CALLS target between incremental and clean benchmark graphs. Add cbm_pipeline_should_suppress_weak_noncallable_call_target in src/pipeline/pipeline_internal.h and apply it in resolve_single_call and resolve_file_calls. Keep Function, Method, type-like constructors, exact LSP targets, and strong same_module/import_map matches so callable C variables remain supported. Add focused coverage in tests/test_pipeline.c and tests/test_lang_contract.c. Verified CBM_ONLY_SUITE=pipeline (386 passed) and CBM_ONLY_SUITE=lang_contract (39 passed). Signed-off-by: Andrew Hundt --- src/pipeline/pass_calls.c | 3 +++ src/pipeline/pass_parallel.c | 4 ++++ src/pipeline/pipeline_internal.h | 13 +++++++++++++ tests/test_lang_contract.c | 22 ++++++++++++++++++++++ tests/test_pipeline.c | 23 +++++++++++++++++++++++ 5 files changed, 65 insertions(+) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index b720500f9..b22a4fdb9 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -605,6 +605,9 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, if (!target_node || source_node->id == target_node->id) { return 0; } + if (cbm_pipeline_should_suppress_weak_noncallable_call_target(target_node, res.strategy)) { + return 0; + } if (emit_classified_edge(ctx, call, source_node, target_node, &res, module_qn, imp_keys, imp_vals, imp_count, tsjs_drop_plain_call) && !cbm_service_pattern_is_global_fetch(call->callee_name)) { diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 2c3937e92..3066cf9bf 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -2100,6 +2100,10 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB } continue; } + if (target_node != lsp_target && + cbm_pipeline_should_suppress_weak_noncallable_call_target(target_node, res.strategy)) { + continue; + } _rc_t0 = extract_now_ns(); emit_service_edge(ws->local_edge_buf, source_node, target_node, call, &res, module_qn, rc->registry, rc->main_gbuf, imp_keys, imp_vals, imp_count, diff --git a/src/pipeline/pipeline_internal.h b/src/pipeline/pipeline_internal.h index 244cc6f53..e3107526f 100644 --- a/src/pipeline/pipeline_internal.h +++ b/src/pipeline/pipeline_internal.h @@ -160,6 +160,19 @@ static inline bool cbm_pipeline_node_is_callable_scope(const cbm_gbuf_node_t *no (strcmp(node->label, "Function") == 0 || strcmp(node->label, "Method") == 0); } +/* Weak registry fallbacks may resolve a call to an unrelated Variable or Field + * with the same short name, including symbols extracted from another language. + * Preserve type-like constructor targets and strong same-module/import matches, + * which can legitimately identify callable variables such as C callbacks. */ +static inline bool cbm_pipeline_should_suppress_weak_noncallable_call_target( + const cbm_gbuf_node_t *target, const char *strategy) { + if (!target || !target->label || !cbm_registry_strategy_is_weak_short_name(strategy)) { + return false; + } + return strcmp(target->label, "Function") != 0 && strcmp(target->label, "Method") != 0 && + !cbm_label_is_type_like(target->label); +} + /* A textual `super().__init__` does not identify a local constructor without * receiver-type information. A registry suffix match can therefore select an * unrelated project Method merely because it is named `__init__`. Keep diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c index 056dac741..49e9329b1 100644 --- a/tests/test_lang_contract.c +++ b/tests/test_lang_contract.c @@ -1490,6 +1490,27 @@ TEST(contract_edge_commonjs_require_call_resolves_issue871) { PASS(); } +/* A weak short-name call match must not bind executable source to a Variable + * extracted from an unrelated data file. This exact shape previously made + * incremental and clean benchmark graphs depend on registry insertion order. */ +TEST(contract_call_weak_match_rejects_noncallable_data_symbol) { + LangProj lp; + static const LangFile f[] = { + {"caller.c", "void caller(void) {\n format();\n}\n"}, + {"benchmarks/schema/example.schema.json", + "{\"type\":\"object\",\"properties\":{\"format\":{\"type\":\"string\"}}}\n"}}; + cbm_store_t *store = lang_index_files(&lp, f, 2); + ASSERT_TRUE(store != NULL); + int false_target = calls_edge_targets(store, lp.project, "Variable", ".format"); + if (false_target) { + fprintf(stderr, + " weak call resolution must not target unrelated JSON Variable `format`\n"); + } + ASSERT_TRUE(!false_target); + lang_cleanup(&lp, store); + PASS(); +} + /* DEPENDS_ON — Helm Chart.yaml `dependencies:` -> per-dependency Chart node. * Basename must be exactly "Chart.yaml"; pass_k8s runs in both pipeline paths. */ TEST(contract_edge_depends_on) { @@ -1704,6 +1725,7 @@ SUITE(lang_contract) { RUN_TEST(contract_edge_no_infra_routes_from_ci_configs_issue999); RUN_TEST(contract_edge_infra_routes_from_deploy_configs_still_minted); RUN_TEST(contract_edge_commonjs_require_call_resolves_issue871); + RUN_TEST(contract_call_weak_match_rejects_noncallable_data_symbol); RUN_TEST(contract_edge_depends_on); RUN_TEST(contract_edge_parallel_service_edges); RUN_TEST(contract_edge_file_changes_with); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index f26e9ffec..a05f67518 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -651,6 +651,28 @@ TEST(pipeline_call_edge_props_include_args_and_line) { PASS(); } +TEST(pipeline_weak_call_target_suppression) { + cbm_gbuf_node_t function_target = {.label = "Function"}; + cbm_gbuf_node_t class_target = {.label = "Class"}; + cbm_gbuf_node_t variable_target = {.label = "Variable"}; + cbm_gbuf_node_t field_target = {.label = "Field"}; + + ASSERT_FALSE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &function_target, "unique_name")); + ASSERT_FALSE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &class_target, "suffix_match")); + ASSERT_TRUE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &variable_target, "unique_name")); + ASSERT_TRUE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &field_target, "suffix_match")); + ASSERT_FALSE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &variable_target, "same_module")); + ASSERT_FALSE(cbm_pipeline_should_suppress_weak_noncallable_call_target( + &variable_target, "import_map")); + ASSERT_FALSE(cbm_pipeline_should_suppress_weak_noncallable_call_target(NULL, "unique_name")); + PASS(); +} + TEST(pipeline_sequential_call_edges_preserve_eighth_arg) { if (setup_test_repo() != 0) { FAIL("failed to create temp dir"); @@ -18026,6 +18048,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_project_name_derived); RUN_TEST(pipeline_mode_global_semantic_edges_policy); RUN_TEST(pipeline_call_edge_props_include_args_and_line); + RUN_TEST(pipeline_weak_call_target_suppression); RUN_TEST(pipeline_sequential_call_edges_preserve_eighth_arg); RUN_TEST(pipeline_fast_mode); /* Definitions pass */