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 <ATHundt@gmail.com>
This commit is contained in:
Andrew Hundt
2026-07-24 00:41:37 -04:00
parent 8ab712029c
commit fdce258a9d
5 changed files with 65 additions and 0 deletions
+3
View File
@@ -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)) {
+4
View File
@@ -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,
+13
View File
@@ -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
+22
View File
@@ -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);
+23
View File
@@ -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 */