From 6b56d78e1bbb5715c9d0c715453244187bd299bc Mon Sep 17 00:00:00 2001 From: Sanskar gupta Date: Mon, 7 Sep 2026 13:04:34 +0000 Subject: [PATCH 1/4] fix(lang): track Solidity modifier invocations as decorators Signed-off-by: Sanskar gupta --- internal/cbm/lang_specs.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cbm/lang_specs.c b/internal/cbm/lang_specs.c index 3a5fad0d4..ed92bd674 100644 --- a/internal/cbm/lang_specs.c +++ b/internal/cbm/lang_specs.c @@ -1038,6 +1038,7 @@ static const char *solidity_field_types[] = {"state_variable_declaration", "stru static const char *solidity_call_types[] = {"call_expression", "call", "new_expression", NULL}; static const char *solidity_import_types[] = {"import_directive", "import", "using_directive", NULL}; +static const char *solidity_decorator_types[] = {"modifier_invocation", NULL}; static const char *solidity_branch_types[] = {"if_statement", "for_statement", "while_statement", "do_while_statement", "try_statement", NULL}; @@ -2115,7 +2116,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = { solidity_field_types, solidity_module_types, solidity_call_types, solidity_import_types, empty_types, solidity_branch_types, solidity_var_types, solidity_assign_types, solidity_throw_types, NULL, - empty_types, NULL, NULL, tree_sitter_solidity, NULL}, + solidity_decorator_types, NULL, NULL, tree_sitter_solidity, NULL}, // CBM_LANG_TYPST [CBM_LANG_TYPST] = {CBM_LANG_TYPST, typst_func_types, empty_types, empty_types, From 7036c4d5996253c05d720ec4d0b3008509b4cad4 Mon Sep 17 00:00:00 2001 From: Sanskar gupta Date: Sun, 20 Sep 2026 14:11:17 +0000 Subject: [PATCH 2/4] test: pin Solidity modifier_invocation decorator tracking (#2090) Signed-off-by: Sanskar gupta --- tests/test_extraction.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index cb01dcf14..a3610c207 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -3999,6 +3999,25 @@ TEST(extract_java_method_annotations_issue382) { PASS(); } +/* Issue #2090: Solidity modifier_invocation tracked as decorator. */ +TEST(solidity_modifier_decorator) { + CBMFileResult *r = extract("pragma solidity ^0.8.0;\n" + "contract Vault {\n" + " modifier onlyOwner() { _; }\n" + " function withdraw() public onlyOwner {\n" + " }\n" + "}\n", + CBM_LANG_SOLIDITY, "t", "Vault.sol"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + const CBMDefinition *m = find_def_by_name(r, "withdraw"); + ASSERT_NOT_NULL(m); + ASSERT(decorators_contain(m, "onlyOwner")); + cbm_free_result(r); + PASS(); +} + + /* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */ TEST(arkts_component_struct) { @@ -7445,6 +7464,7 @@ SUITE(extraction) { RUN_TEST(js_index_module_qn_not_collide_with_folder); RUN_TEST(python_regular_module_qn_unchanged); RUN_TEST(extract_java_method_annotations_issue382); + RUN_TEST(solidity_modifier_decorator); RUN_TEST(arkts_component_struct); RUN_TEST(arkts_exported_struct_decorators); RUN_TEST(arkts_member_decorators); From f9350caae0ccccf823d8cde85683a900b257f239 Mon Sep 17 00:00:00 2001 From: Sanskar gupta Date: Sun, 20 Sep 2026 14:52:06 +0000 Subject: [PATCH 3/4] fix: remove extra blank line Signed-off-by: Sanskar gupta --- tests/test_extraction.c | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_extraction.c b/tests/test_extraction.c index a3610c207..2bcb54210 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -4017,7 +4017,6 @@ TEST(solidity_modifier_decorator) { PASS(); } - /* ── ArkTS (HarmonyOS .ets) ─────────────────────────────────────── */ TEST(arkts_component_struct) { From c933d0bb4ce0b61bca95c477e536dda2069cb5b1 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Wed, 21 Oct 2026 19:17:12 +0000 Subject: [PATCH 4/4] fix(pipeline): a bare C reference never binds a struct Field A bare C identifier that names no visible declaration reached the short-name registry fallback in the USAGE and READS/WRITES resolvers, which bound it to whichever struct member carried the same name. tests/test_main.c's RUN_SELECTED_SUITE(mcp) gave `main` a USAGE edge to cbm_daemon_application_session.mcp, and every bare local write (`offset = 0`, `trait_qn = head`) became a WRITES edge to an unrelated struct's field. In C a member is only ever named as the member half of `a.b`, `a->b` or a designator `.b`, all recorded as is_member_access and bound through the C LSP owner rows. Widen the Go rule (#1942/#1962) to C: cbm_go_suppress_bare_field_ref becomes cbm_suppress_bare_field_ref and takes the caller language. C++ (and every .h, parsed as C++) keeps its bare member references, as a method body names its own members without a selector. Sequential and parallel resolvers both consult it. Fixes #2666 Signed-off-by: Gaurav Dubey --- src/pipeline/pass_parallel.c | 13 +++--- src/pipeline/pass_usages.c | 13 +++--- src/pipeline/pipeline.h | 15 +++--- src/pipeline/registry.c | 19 ++++++-- tests/test_pipeline.c | 89 ++++++++++++++++++++++++++++++++++++ tests/test_registry.c | 32 ++++++++++--- 6 files changed, 150 insertions(+), 31 deletions(-) diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 73ce742ad..fedc72f8b 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -3746,10 +3746,9 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws, if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) { continue; } - /* #1942/#1962: a bare Go reference can never denote a struct - * field; the member half of a selector may. */ - if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access, - tgt->label)) { + /* #1942/#1962/#2666: a bare Go or C reference can never denote + * a struct field; the member half of a selector may. */ + if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) { continue; } if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow || @@ -3875,9 +3874,9 @@ static void resolve_file_rw(resolve_ctx_t *rc, resolve_worker_state_t *ws, CBMFi if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) { continue; } - /* #1942/#1962: a bare Go reference can never denote a struct field; - * a selector-LHS write (`t.err = x`) may bind it. */ - if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) { + /* #1942/#1962/#2666: a bare Go or C reference can never denote a + * struct field; a selector-LHS write (`t.err = x`) may bind it. */ + if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) { continue; } const char *etype = rw->is_write ? "WRITES" : "READS"; diff --git a/src/pipeline/pass_usages.c b/src/pipeline/pass_usages.c index 972822522..b51573deb 100644 --- a/src/pipeline/pass_usages.c +++ b/src/pipeline/pass_usages.c @@ -283,10 +283,9 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res if (tgt && cbm_suppress_cross_language_ref(lang, tgt->file_path)) { continue; } - /* #1942/#1962: a bare Go reference can never denote a struct - * field; the member half of a selector may. */ - if (tgt && cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, usage->is_member_access, - tgt->label)) { + /* #1942/#1962/#2666: a bare Go or C reference can never denote + * a struct field; the member half of a selector may. */ + if (tgt && cbm_suppress_bare_field_ref(lang, usage->is_member_access, tgt->label)) { continue; } if (usage->semantic_reference_blocked && (usage->semantic_reference_local_shadow || @@ -416,9 +415,9 @@ static int resolve_rw_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *result if (cbm_suppress_cross_language_ref(lang, tgt->file_path)) { continue; } - /* #1942/#1962: a bare Go reference can never denote a struct field; - * a selector-LHS write (`t.err = x`) may bind it. */ - if (cbm_go_suppress_bare_field_ref(lang == CBM_LANG_GO, rw->is_member_access, tgt->label)) { + /* #1942/#1962/#2666: a bare Go or C reference can never denote a + * struct field; a selector-LHS write (`t.err = x`) may bind it. */ + if (cbm_suppress_bare_field_ref(lang, rw->is_member_access, tgt->label)) { continue; } diff --git a/src/pipeline/pipeline.h b/src/pipeline/pipeline.h index ddc14e737..92176a977 100644 --- a/src/pipeline/pipeline.h +++ b/src/pipeline/pipeline.h @@ -447,13 +447,14 @@ bool cbm_suppress_cross_language_suffix_match(CBMLanguage caller_lang, const cha * unit-tested in test_registry.c. */ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target_file_path); -/* #1942: a bare (dot-less) Go reference can never denote a struct field — - * field access is always a selector expression, and selector references - * resolve on the LSP path. Drops a READS/WRITES/USAGE bind whose target is a - * Field when the reference text carries no '.'. Go only: other OO languages - * legitimately reference their own members bare inside method bodies. Pure; - * unit-tested in test_registry.c. */ -bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label); +/* #1942/#2666: a bare Go or C reference can never denote a struct field — + * field access is always a selector expression (or, in C, a designator), and + * the member half of one is recorded as is_member_access. Drops a + * READS/WRITES/USAGE bind whose target is a Field when the reference was not + * the member half of a selector. Go and C only: other OO languages (and C++, + * which every `.h` is parsed as) legitimately reference their own members + * bare inside method bodies. Pure; unit-tested in test_registry.c. */ +bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access, const char *target_label); /* What to do with a call whose registry resolution landed on a Field node. * Pure; unit-tested in test_registry.c. */ diff --git a/src/pipeline/registry.c b/src/pipeline/registry.c index 8da94b096..6f4b3c897 100644 --- a/src/pipeline/registry.c +++ b/src/pipeline/registry.c @@ -1040,7 +1040,8 @@ bool cbm_suppress_cross_language_ref(CBMLanguage caller_lang, const char *target return true; } -bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const char *target_label) { +bool cbm_suppress_bare_field_ref(CBMLanguage lang, bool is_member_access, + const char *target_label) { /* #1942/#1962: a bare Go identifier can never denote a struct field — * field access is always a selector expression (x.f). The extractor * strips the receiver before the resolver runs (resolve_lhs_write_name @@ -1048,10 +1049,20 @@ bool cbm_go_suppress_bare_field_ref(bool is_go, bool is_member_access, const cha * field_identifier), so the reference TEXT is always dot-less and cannot * carry the distinction — the recorded is_member_access shape can. Only a * reference that was never the member half of a selector is refused a - * Field bind. Go-gated: a C#/Java/C++/Python method body legitimately + * Field bind. + * + * #2666: C has the same rule. A struct member is named only as the member + * half of `a.b`, `a->b` or a designator `.b`, all recorded as + * field_identifier; a bare C identifier (a macro argument such as + * RUN_SELECTED_SUITE(mcp), a stray name in an unexpanded macro body) is + * never one. The call path already drops a bare C call onto a Field + * (cbm_call_onto_field_policy). + * + * Go- and C-gated: a C#/Java/C++/Python method body legitimately * references its own members bare (cp_reads_writes_cs_static_field pins - * that shape as required), so a global veto would break those languages. */ - if (!is_go || is_member_access || !target_label) { + * that shape as required), so a global veto would break those languages. + * A `.h` is parsed as C++ and keeps that behavior. */ + if ((lang != CBM_LANG_GO && lang != CBM_LANG_C) || is_member_access || !target_label) { return false; } return strcmp(target_label, "Field") == 0; diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index bd76775f3..38f2dafad 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -11055,6 +11055,93 @@ TEST(pipeline_go_bare_ref_never_binds_field_parallel) { PASS(); } +/* #2666: tests/test_main.c runs every suite through a macro, + * RUN_SELECTED_SUITE(mcp); the argument is a bare C identifier that names no + * declaration, and the registry fallback bound it to whichever struct member + * was called `mcp`. toggle() writes a bare name the extractor cannot see + * declared (a macro-made global, say) that a struct member also carries. + * state.c holds the genuine member uses that must keep their edges. + * pad_files pushes the run past MIN_FILES_FOR_PARALLEL for the parallel twin. */ +static void write_c_bare_field_fixture(const char *tmp, int pad_files) { + write_temp_file(tmp, "include/suite.h", + "typedef struct suite_state {\n" + " char mcp[64];\n" + " int count;\n" + " int enabled;\n" + "} suite_state_t;\n"); + write_temp_file(tmp, "tests/main.c", + "#include \"suite.h\"\n" + "#define RUN_SELECTED_SUITE(name) run_suite(#name)\n" + "int run_suite(const char *name);\n" + "int main(void) {\n" + " RUN_SELECTED_SUITE(mcp);\n" + " RUN_SELECTED_SUITE(count);\n" + " return 0;\n" + "}\n" + "void toggle(void) {\n" + " enabled = 1;\n" + "}\n"); + write_temp_file(tmp, "src/state.c", + "#include \"suite.h\"\n" + "int state_count(const suite_state_t *s) { return s->count; }\n" + "void state_reset(suite_state_t *s) { s->enabled = 0; }\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "pad/filler%d.c", i); + snprintf(body, sizeof(body), "int filler%d(void) { return %d; }\n", i, i); + write_temp_file(tmp, name, body); + } +} + +static int run_c_bare_field_case(const char *tmpl, int pad_files) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "%s", tmpl); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_c_bare_field_fixture(tmp, pad_files); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/c_bare.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + ASSERT_NOT_NULL(p); + ASSERT_EQ(cbm_pipeline_run(p), 0); + const char *project = cbm_pipeline_project_name(p); + + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + /* The fields must exist for the probes to mean anything. */ + ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "mcp", "Field"), 1); + ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "count", "Field"), 1); + ASSERT_GTE(fixture_node_count(s, project, "include/suite.h", "enabled", "Field"), 1); + /* Reproduce-first: RED before the fix — the macro arguments bind the + * same-named struct members, and the bare write binds `enabled`. */ + ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "mcp", "USAGE")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "main", "count", "USAGE")); + ASSERT_FALSE(cross_file_edge_exists(s, project, "toggle", "enabled", "WRITES")); + /* The member half of `s->count` / `s->enabled = 0` still binds the field. */ + ASSERT_TRUE(cross_file_edge_exists(s, project, "state_count", "count", "USAGE")); + ASSERT_TRUE(cross_file_edge_exists(s, project, "state_reset", "enabled", "WRITES")); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_c_bare_ref_never_binds_field) { + /* Sequential path: resolve_usage_edges / resolve_rw_edges. */ + return run_c_bare_field_case("/tmp/cbm_c_bare_XXXXXX", 0); +} + +TEST(pipeline_c_bare_ref_never_binds_field_parallel) { + /* Parallel twin: resolve_file_usages / resolve_file_rw must consult the + * same predicate (#1928's lesson). */ + return run_c_bare_field_case("/tmp/cbm_c_barep_XXXXXX", 52); +} + /* A C caller of a header function that shares its name with a struct member * elsewhere (kernel: include/linux/mm.h put_page beside the arm64 * kvm_pgtable_mm_ops.put_page member). pad_files pushes the run past @@ -27700,6 +27787,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_rust_std_receiver_never_binds_project_method_parallel); RUN_TEST(pipeline_go_bare_ref_never_binds_field); RUN_TEST(pipeline_go_bare_ref_never_binds_field_parallel); + RUN_TEST(pipeline_c_bare_ref_never_binds_field); + RUN_TEST(pipeline_c_bare_ref_never_binds_field_parallel); RUN_TEST(pipeline_c_call_reaches_header_function_beside_field); RUN_TEST(pipeline_c_call_reaches_header_function_beside_field_parallel); RUN_TEST(pipeline_c_member_access_binds_field_by_object_type); diff --git a/tests/test_registry.c b/tests/test_registry.c index 391fd3b88..16b22e735 100644 --- a/tests/test_registry.c +++ b/tests/test_registry.c @@ -1245,17 +1245,36 @@ TEST(go_bare_ref_never_binds_field) { * trailing name; is_reference_node records the inner field_identifier), * so the selector-vs-bare distinction arrives as the recorded * is_member_access signal, never as a dot in the reference text. */ - ASSERT_TRUE(cbm_go_suppress_bare_field_ref(true, false, "Field")); + ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Field")); /* The member half of a selector may bind a field. */ - ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, true, "Field")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, true, "Field")); /* Bare references to non-fields are untouched. */ - ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Variable")); - ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, "Function")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Variable")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, "Function")); /* Other languages reference their own members bare inside methods — * never suppressed (cp_reads_writes_cs_static_field pins the C# shape). */ - ASSERT_FALSE(cbm_go_suppress_bare_field_ref(false, false, "Field")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CSHARP, false, "Field")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_JAVA, false, "Field")); /* Degenerate input → nothing to judge. */ - ASSERT_FALSE(cbm_go_suppress_bare_field_ref(true, false, NULL)); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_GO, false, NULL)); + PASS(); +} + +TEST(c_bare_ref_never_binds_field) { + /* #2666: a C struct member is named only as the member half of `a.b`, + * `a->b` or a designator `.b` (field_identifier → is_member_access). A + * bare C identifier — a macro argument like RUN_SELECTED_SUITE(mcp) — is + * never one, so it must not bind a same-named Field. */ + ASSERT_TRUE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Field")); + /* The member half still binds (through the C owner rows upstream). */ + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, true, "Field")); + /* A bare C name keeps binding globals, functions and types. */ + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Variable")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Function")); + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_C, false, "Type")); + /* C++ (and every `.h`, parsed as C++): a method body names its own + * members bare, so the bind is kept. */ + ASSERT_FALSE(cbm_suppress_bare_field_ref(CBM_LANG_CPP, false, "Field")); PASS(); } @@ -2270,6 +2289,7 @@ SUITE(registry) { RUN_TEST(registry_tie_prefers_csharp_implementation_over_ref_stub); RUN_TEST(cross_language_ref_drops_go_vs_c); RUN_TEST(go_bare_ref_never_binds_field); + RUN_TEST(c_bare_ref_never_binds_field); RUN_TEST(call_onto_field_follows_language_and_shape); RUN_TEST(c_bare_call_does_not_weigh_field_namesakes); RUN_TEST(c_arrow_member_call_is_c_family_and_arrow_only);