From 6b56d78e1bbb5715c9d0c715453244187bd299bc Mon Sep 17 00:00:00 2001 From: Sanskar gupta Date: Mon, 7 Sep 2026 13:04:34 +0000 Subject: [PATCH 1/8] 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/8] 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/8] 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 255463126650ab6deab3e62aab441b71479704f1 Mon Sep 17 00:00:00 2001 From: News Date: Wed, 23 Sep 2026 08:21:53 +0900 Subject: [PATCH 4/8] fix(windows): set UTF-8 environment values through wide CRT Signed-off-by: News --- src/foundation/compat.h | 8 +++----- tests/test_platform.c | 12 ++++++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/foundation/compat.h b/src/foundation/compat.h index 821d901f0..3080930f8 100644 --- a/src/foundation/compat.h +++ b/src/foundation/compat.h @@ -166,11 +166,9 @@ static inline int cbm_setenv(const char *name, const char *value, int overwrite) free(wide_value); return EINVAL; } - /* Keep the CRT's narrow environment useful for legacy getenv callers, - * then repair the process-wide Windows environment with the actual UTF-16 - * value. _putenv_s alone routes UTF-8 path bytes through the active ANSI - * code page, which corrupts non-ASCII cache roots inherited by children. */ - int status = _putenv_s(name, value); + /* _putenv_s can reject UTF-8 bytes outside the active ANSI code page. + * Narrow getenv returns bytes in that code page after this wide update. */ + int status = _wputenv_s(wide_name, wide_value); if (status == 0 && !SetEnvironmentVariableW(wide_name, wide_value)) { status = EINVAL; } diff --git a/tests/test_platform.c b/tests/test_platform.c index 5995a4b01..77d325d91 100644 --- a/tests/test_platform.c +++ b/tests/test_platform.c @@ -869,8 +869,8 @@ TEST(platform_env_long_refuses_what_it_cannot_read) { * setter must update that same wide environment; _putenv_s alone interprets * UTF-8 path bytes through the active ANSI code page. */ TEST(platform_setenv_preserves_utf8_in_wide_environment) { - static const char utf8[] = "C:/cbm-cache-\xce\x94-\xe6\x97\xa5\xe6\x9c\xac"; - static const wchar_t wide[] = L"C:/cbm-cache-\u0394-\u65e5\u672c"; + static const char utf8[] = "C:/cbm-cache-\xce\x94-\xe6\x97\xa5\xe6\x9c\xac-\xe4\xb8\x81"; + static const wchar_t wide[] = L"C:/cbm-cache-\u0394-\u65e5\u672c-\u4e01"; ASSERT_EQ(cbm_setenv("CBM_CACHE_DIR", utf8, 1), 0); wchar_t observed_wide[128]; ASSERT_EQ(GetEnvironmentVariableW(L"CBM_CACHE_DIR", observed_wide, 128), (DWORD)(wcslen(wide))); @@ -878,6 +878,14 @@ TEST(platform_setenv_preserves_utf8_in_wide_environment) { char observed_utf8[128]; ASSERT_NOT_NULL(cbm_safe_getenv("CBM_CACHE_DIR", observed_utf8, sizeof(observed_utf8), NULL)); ASSERT_STR_EQ(observed_utf8, utf8); + /* Narrow getenv must agree with the CRT's active ANSI code page. */ + char expected_narrow[128]; + ASSERT_GT(WideCharToMultiByte(CP_ACP, 0, wide, -1, expected_narrow, + (int)sizeof(expected_narrow), NULL, NULL), + 0); + const char *observed_narrow = getenv("CBM_CACHE_DIR"); + ASSERT_NOT_NULL(observed_narrow); + ASSERT_STR_EQ(observed_narrow, expected_narrow); (void)cbm_unsetenv("CBM_CACHE_DIR"); PASS(); } From 26e2d0cba6df7b1cb53e655d0b902bdb7debdbd4 Mon Sep 17 00:00:00 2001 From: News Date: Sat, 26 Sep 2026 20:30:02 +0900 Subject: [PATCH 5/8] fix(windows): read worker and CLI environment paths as UTF-8 Signed-off-by: News --- internal/cbm/cbm.c | 9 +++++++-- src/cli/cli.c | 4 +++- tests/test_extraction.c | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/internal/cbm/cbm.c b/internal/cbm/cbm.c index b54fb54a7..f17d54757 100644 --- a/internal/cbm/cbm.c +++ b/internal/cbm/cbm.c @@ -25,6 +25,7 @@ #include "foundation/compat_fs.h" // cbm_fopen — crash-supervisor per-file marker write #include "foundation/hash_table.h" // CBMHashTable — crash-supervisor quarantine set #include "tree_sitter/api.h" // TSParser, TSNode, TSTree, TSInput, TSLanguage, TSPoint, TSParseOptions, TSParseState +#include "foundation/platform.h" #include "foundation/constants.h" #include "mimalloc.h" // mi_malloc/mi_calloc/mi_realloc/mi_free/mi_usable_size — bind 3rd-party allocators (#424) #if defined(CBM_BIND_TS_ALLOCATOR) && CBM_BIND_TS_ALLOCATOR @@ -969,7 +970,9 @@ static int count_params_from_signature(const char *sig) { * unlucky in-flight file is never quarantined alone. The env var is set * solely by the supervisor during recovery — a no-op on normal runs. */ static void cbm_index_mark(const char *rel_path, char event) { - const char *mf = getenv("CBM_INDEX_MARKER_FILE"); + char marker_path[CBM_SZ_4K]; + const char *mf = + cbm_safe_getenv("CBM_INDEX_MARKER_FILE", marker_path, sizeof(marker_path), NULL); if (!mf || !mf[0] || !rel_path || !rel_path[0]) { return; } @@ -1006,7 +1009,9 @@ enum { CBM_QSET_UNINIT = 0, CBM_QSET_INITING = 1, CBM_QSET_INITED = 2 }; static atomic_int g_quarantine_state = CBM_QSET_UNINIT; static void cbm_quarantine_load(void) { - const char *qf = getenv("CBM_INDEX_QUARANTINE_FILE"); + char quarantine_path[CBM_SZ_4K]; + const char *qf = cbm_safe_getenv("CBM_INDEX_QUARANTINE_FILE", quarantine_path, + sizeof(quarantine_path), NULL); if (!qf || !qf[0]) { return; /* normal path: empty set */ } diff --git a/src/cli/cli.c b/src/cli/cli.c index b59a894a6..5de7b9db5 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -759,7 +759,9 @@ static bool cli_activation_production_context_init(cli_activation_production_con context->cleanup_ok = true; context->deadline_ms = cli_activation_deadline_after(CLI_ACTIVATION_DRAIN_TIMEOUT_MS); context->control_deadline_ms = cli_activation_deadline_after(CLI_ACTIVATION_CONTROL_TIMEOUT_MS); - const char *original_cache_environment = getenv("CBM_CACHE_DIR"); + char original_cache_buffer[CLI_BUF_4K]; + const char *original_cache_environment = cbm_safe_getenv("CBM_CACHE_DIR", original_cache_buffer, + sizeof(original_cache_buffer), NULL); context->original_cache_environment_present = original_cache_environment != NULL; if (context->original_cache_environment_present) { context->original_cache_environment = strdup(original_cache_environment); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 619b94045..806433b0b 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -7,6 +7,7 @@ */ #include "test_framework.h" #include "cbm.h" +#include "foundation/platform.h" #include "foundation/constants.h" /* CBM_SZ_* */ #include "preprocessor.h" /* cbm_export_macro_candidates (#1989) */ #include "../src/foundation/compat.h" /* cbm_clock_gettime (wide-flat scaling guard) */ @@ -8157,7 +8158,42 @@ TEST(extract_walk_truncated_when_a_node_budget_is_set) { PASS(); } +#ifdef _WIN32 +/* The supervisor sets this path through cbm_setenv. Exercise the actual + * journal writer, which must read it as UTF-8 before calling cbm_fopen. */ +TEST(extraction_marker_round_trips_non_ascii_environment_path) { + char dir[512] = "/tmp/cbm-marker-XXXXXX"; + ASSERT_NOT_NULL(cbm_mkdtemp(dir)); + char marker[640]; + snprintf(marker, sizeof(marker), "%s/marker-\xce\x94-\xe4\xb8\x81.log", dir); + char saved[4096]; + const char *previous = cbm_safe_getenv("CBM_INDEX_MARKER_FILE", saved, sizeof(saved), NULL); + ASSERT_EQ(cbm_setenv("CBM_INDEX_MARKER_FILE", marker, 1), 0); + cbm_index_mark_start("synthetic.cpp"); + cbm_index_mark_done("synthetic.cpp"); + if (previous) { + (void)cbm_setenv("CBM_INDEX_MARKER_FILE", saved, 1); + } else { + (void)cbm_unsetenv("CBM_INDEX_MARKER_FILE"); + } + FILE *file = cbm_fopen(marker, "rb"); + char contents[128] = {0}; + if (file) { + (void)fread(contents, 1, sizeof(contents) - 1, file); + (void)fclose(file); + } + (void)cbm_unlink(marker); + (void)cbm_rmdir(dir); + ASSERT_NOT_NULL(file); + ASSERT_STR_EQ(contents, "S synthetic.cpp\nD synthetic.cpp\n"); + PASS(); +} +#endif + SUITE(extraction) { +#ifdef _WIN32 + RUN_TEST(extraction_marker_round_trips_non_ascii_environment_path); +#endif RUN_TEST(extract_compact_keeps_every_field_and_shrinks_the_arena); RUN_TEST(extract_compact_is_idempotent_and_survives_empty_results); RUN_TEST(extract_spill_round_trip_keeps_every_field); From fd9f1d83025e569039314436a6f780f284a69d99 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Wed, 21 Oct 2026 07:42:06 +0000 Subject: [PATCH 6/8] fix(ts-lsp): resolve calls through renamed named imports `import { foo as fooShared } from "./lib"; fooShared(1)` produced no CALLS edge to lib.foo, so trace_path(foo, inbound) missed the caller while plain named and namespace imports resolved. The TS LSP import map holds only the local binding, and every import-binding lookup spelled `.` (lib.fooShared), which names nothing. Read the file's own `import { a as b }` specifiers into a local -> exported-name map in all three LSP entry points (per-file and both cross-file paths), before the type passes, and look up the exported member wherever an import binding becomes a member QN: free calls, function values, identifier and `new` types, imported type names and JSX components. The lookup stays exact: a renamed import of a name the module does not export resolves to nothing instead of to a same-named export that matches the alias. `default as x` keeps its local-name lookup. The pipeline joins an LSP row to its call by the callee's leaf name, which for an alias is not the name written at the site. Calls and JSX tags through a renamed import are emitted as lsp_import_alias and function values as lsp_callable_value_reference, with the written name in `reason`, the reason-join strategies the Python, Go, Kotlin, C# and C resolvers already use for aliases. Fixes #2297 Signed-off-by: Gaurav Dubey --- internal/cbm/lsp/ts_lsp.c | 143 +++++++++++++++++++++++++++++++++----- internal/cbm/lsp/ts_lsp.h | 7 ++ tests/test_pipeline.c | 76 ++++++++++++++++++++ tests/test_ts_lsp.c | 129 ++++++++++++++++++++++++++++++++++ 4 files changed, 339 insertions(+), 16 deletions(-) diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index 8ee5e6642..27cf50ff2 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -261,8 +261,12 @@ static TSNode find_first_kind(TSNode node, const char *kind) { return null_node; } -static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy, - float confidence, TSNode site) { +/* `reason` carries the name written at the site when it differs from the + * callee's leaf (`bar()` through `import { foo as bar }`): the pipeline joins an + * alias strategy by that name instead of by the callee QN's last segment. */ +static void ts_emit_resolved_call_reason_at(TSLSPContext *ctx, const char *callee_qn, + const char *strategy, float confidence, + const char *reason, TSNode site) { if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return; CBMResolvedCall rc = {0}; @@ -270,7 +274,7 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c rc.callee_qn = callee_qn; rc.strategy = strategy ? strategy : "lsp_ts"; rc.confidence = confidence; - rc.reason = NULL; + rc.reason = reason; rc.kind = CBM_RESOLVED_INVOCATION; if (!ts_node_is_null(site)) { rc.site_start_byte = ts_node_start_byte(site); @@ -279,8 +283,14 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } -static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn, - const char *strategy, float confidence, TSNode site) { +static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy, + float confidence, TSNode site) { + ts_emit_resolved_call_reason_at(ctx, callee_qn, strategy, confidence, NULL, site); +} + +static void ts_emit_resolved_reference_reason(TSLSPContext *ctx, const char *callee_qn, + const char *strategy, float confidence, + const char *reason, TSNode site) { if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn || ts_node_is_null(site)) { return; @@ -290,13 +300,18 @@ static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn, rc.callee_qn = callee_qn; rc.strategy = strategy ? strategy : "lsp_ts_value_reference"; rc.confidence = confidence; - rc.reason = NULL; + rc.reason = reason; rc.kind = CBM_RESOLVED_CALL_REFERENCE; rc.site_start_byte = ts_node_start_byte(site); rc.site_end_byte = ts_node_end_byte(site); cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } +static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn, + const char *strategy, float confidence, TSNode site) { + ts_emit_resolved_reference_reason(ctx, callee_qn, strategy, confidence, NULL, site); +} + static void ts_emit_unresolved_call_at(TSLSPContext *ctx, const char *expr_text, const char *reason, TSNode site) { if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn) @@ -1417,6 +1432,19 @@ static bool ts_scope_has_binding(const CBMScope *scope, const char *name) { return ts_scope_binding_owner(scope, name) != NULL; } +/* The member an import binding names in its module: the exported name of a + * renamed named import (`foo` for `import { foo as bar }`, #2297), else the + * binding itself. Callers have already matched `local` to an import binding. */ +static const char *ts_import_export_name(const TSLSPContext *ctx, const char *local) { + if (!ctx || !local) + return local; + for (int i = 0; i < ctx->import_alias_count; i++) { + if (strcmp(ctx->import_alias_locals[i], local) == 0) + return ctx->import_alias_exports[i]; + } + return local; +} + /* Full QN of an imported symbol. An import value is normally the module QN * (symbol = module + "." + name), but may already be the symbol QN. When the * module QN ends in ".name" the spelling is ambiguous: `Foo.ts` exporting @@ -1455,8 +1483,9 @@ static const CBMRegisteredFunc *ts_exact_same_module_function(TSLSPContext *ctx, } /* Return a function only when the local import binding maps to one unambiguous - * module and that module contains the exact local-name QN. Aliases whose export - * name is unavailable in CBMImport deliberately fail closed and remain USAGE. */ + * module and that module contains the exact QN of the member the binding names + * (the exported name for `import { foo as bar }`). A renamed import of a name + * the module does not export fails closed and remains USAGE. */ static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, const char *name) { if (!ctx || !ctx->registry || !name || ts_scope_has_binding(ctx->current_scope, name)) { return NULL; @@ -1476,7 +1505,8 @@ static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, co if (!matched_module) { return NULL; } - const char *full_qn = ts_import_symbol_qn(ctx, matched_module, name); + const char *full_qn = + ts_import_symbol_qn(ctx, matched_module, ts_import_export_name(ctx, name)); return full_qn ? cbm_registry_lookup_func(ctx->registry, full_qn) : NULL; } @@ -1524,7 +1554,7 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name) { // Construct the full symbol QN using the same convention as exact // imported function-value resolution. - const char *full_qn = ts_import_symbol_qn(ctx, mqn, name); + const char *full_qn = ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, name)); // Try the symbol lookup precedence: registered func, then registered type. const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, full_qn); @@ -2079,7 +2109,8 @@ static const CBMType *ts_new_bare_class_type(TSLSPContext *ctx, const char *cnam bool import_matched = false; const char *import_module = ts_import_module_for_local(ctx, cname, &import_matched); if (import_module) - return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, import_module, cname)); + return cbm_type_named( + ctx->arena, ts_import_symbol_qn(ctx, import_module, ts_import_export_name(ctx, cname))); return cbm_type_named(ctx->arena, local_qn); } @@ -2558,7 +2589,8 @@ static const CBMType *resolve_type_with_imports(TSLSPContext *ctx, const CBMType continue; if (strcmp(lname, bare) != 0) continue; - return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, mqn, bare)); + return cbm_type_named(ctx->arena, + ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, bare))); } return t; } @@ -2925,12 +2957,19 @@ static void resolve_value_references_at(TSLSPContext *ctx, TSNode call_node) { } const CBMRegisteredFunc *target = ts_exact_same_module_function(ctx, name); const char *strategy = "lsp_ts_same_module_value_reference"; + const char *reason = NULL; if (!target) { target = ts_exact_imported_function(ctx, name); strategy = "lsp_ts_import_value_reference"; + /* `import { foo as bar }`: the site names `bar`, the target is `foo`. */ + if (target && ts_import_export_name(ctx, name) != name) { + strategy = "lsp_callable_value_reference"; + reason = name; + } } if (target) { - ts_emit_resolved_reference(ctx, target->qualified_name, strategy, 0.95f, arg); + ts_emit_resolved_reference_reason(ctx, target->qualified_name, strategy, 0.95f, reason, + arg); } } } @@ -3017,14 +3056,20 @@ static void resolve_call_at(TSLSPContext *ctx, TSNode call_node) { const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (lname && mqn && strcmp(lname, name) == 0) { - const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, name, args); + const char *member = ts_import_export_name(ctx, name); + const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, member, args); + if (f && member != name) { + ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias", + 0.95f, name, call_node); + return; + } if (f) { ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_import", 0.95f, call_node); return; } // Fall through to qualified_name fallback. - const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name); + const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, member); ts_emit_unresolved_call_at(ctx, qn, "import_symbol_not_in_registry", call_node); return; } @@ -3366,7 +3411,13 @@ static void resolve_jsx_element(TSLSPContext *ctx, TSNode element_node) { * per-file row deliberately keeps the file eligible for the * cross-file pass, where resolved import QNs and project defs are * available. */ - const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, tag_name); + const char *member = ts_import_export_name(ctx, tag_name); + const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, member); + if (f && member != tag_name) { + ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias", 0.95f, + tag_name, tag); + return; + } if (f) { ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_jsx_import", 0.95f, tag); return; @@ -3996,6 +4047,63 @@ void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *mo ctx->import_count = new_count; } +static void ts_lsp_add_import_alias(TSLSPContext *ctx, const char *local, const char *exported) { + int new_count = ctx->import_alias_count + 1; + const char **locals = + (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); + const char **exports = + (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); + if (!locals || !exports) + return; + for (int i = 0; i < ctx->import_alias_count; i++) { + locals[i] = ctx->import_alias_locals[i]; + exports[i] = ctx->import_alias_exports[i]; + } + locals[ctx->import_alias_count] = local; + exports[ctx->import_alias_count] = exported; + ctx->import_alias_locals = locals; + ctx->import_alias_exports = exports; + ctx->import_alias_count = new_count; +} + +/* Record each `import { foo as bar }` binding of the file (#2297). The import + * map holds only local bindings, so a renamed binding spelled `.bar`, + * named nothing, and every call through it lost its CALLS edge. Reading the + * file's own import statements serves the per-file and both cross-file entry + * points alike. `default as x` keeps today's local-name lookup (a default + * import binds no exported member name); string export names and identity + * aliases need no entry. Must run before the type passes, which resolve + * imported type names. */ +static void ts_lsp_collect_import_aliases(TSLSPContext *ctx, TSNode root) { + if (!ctx || !ctx->arena || ts_node_is_null(root) || ctx->import_count == 0) + return; + uint32_t kn = 0; + TSNode *kids = collect_children(ctx->arena, root, &kn); + for (uint32_t i = 0; i < kn; i++) { + if (!node_kind_is(kids[i], "import_statement")) + continue; + TSNode named = find_first_kind(find_first_kind(kids[i], "import_clause"), "named_imports"); + if (ts_node_is_null(named)) + continue; + uint32_t sn = 0; + TSNode *specs = collect_children(ctx->arena, named, &sn); + for (uint32_t j = 0; j < sn; j++) { + if (!node_kind_is(specs[j], "import_specifier")) + continue; + TSNode name = ts_node_child_by_field_name(specs[j], "name", TS_LSP_FIELD_LEN("name")); + TSNode alias = + ts_node_child_by_field_name(specs[j], "alias", TS_LSP_FIELD_LEN("alias")); + if (!node_kind_is(name, "identifier") || !node_kind_is(alias, "identifier")) + continue; + char *exported = node_text(ctx, name); + char *local = node_text(ctx, alias); + if (exported && local && strcmp(exported, "default") != 0 && + strcmp(exported, local) != 0) + ts_lsp_add_import_alias(ctx, local, exported); + } + } +} + // ── Stdlib seeds (Phase 5 will replace with generator) ──────────────────────── static const CBMType *tt_builtin(CBMArena *a, const char *n) { @@ -5787,6 +5895,7 @@ void cbm_run_ts_lsp(CBMArena *arena, CBMFileResult *result, const char *source, ts_lsp_add_import(&ctx, imp->local_name, imp->module_path); } } + ts_lsp_collect_import_aliases(&ctx, root); ast_sweep_shapes(&ctx, root, ®); rebuild_signatures_from_ast(&ctx, root, ®); @@ -6041,6 +6150,7 @@ void cbm_run_ts_lsp_cross_with_registry(CBMArena *arena, const char *source, int ts_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } + ts_lsp_collect_import_aliases(&ctx, root); ast_sweep_shapes(&ctx, root, &overlay); rebuild_signatures_from_ast(&ctx, root, &overlay); @@ -6257,6 +6367,7 @@ void cbm_run_ts_lsp_cross(CBMArena *arena, const char *source, int source_len, ts_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } + ts_lsp_collect_import_aliases(&ctx, root); ast_sweep_shapes(&ctx, root, ®); rebuild_signatures_from_ast(&ctx, root, ®); diff --git a/internal/cbm/lsp/ts_lsp.h b/internal/cbm/lsp/ts_lsp.h index 1096bea90..312ae985e 100644 --- a/internal/cbm/lsp/ts_lsp.h +++ b/internal/cbm/lsp/ts_lsp.h @@ -33,6 +33,13 @@ typedef struct { const char **import_module_qns; int import_count; + // Renamed named imports (`import { foo as bar }`): local binding -> the name + // the module exports. Parallel arrays of length import_alias_count; a + // binding absent here is exported under its local name. + const char **import_alias_locals; + const char **import_alias_exports; + int import_alias_count; + // File / surrounding context. const char *module_qn; // QN of this file's module const char *enclosing_func_qn; // current function being walked, NULL at module scope diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index bd76775f3..2a5923236 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -9282,6 +9282,81 @@ TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354) { PASS(); } +/* Issue #2297: a call through `import { foo as fooShared }` produced no CALLS + * edge, so trace_path(foo, inbound) missed the caller. End to end through the + * real pipeline: the renamed call, an app-local wrapper that shares the + * exported name, and a method on a renamed class import all reach the + * exported member, while the plain named import control keeps lsp_ts_import. */ +TEST(pipeline_ts_renamed_import_call_issue2297) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_ts_2297_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + + write_temp_file(tmp, "src/lib.ts", + "export function fooUniqueQrs(x: number): number {\n" + " return x + 1;\n" + "}\n" + "export function buildHealthUniqueQrs(input: { a: number }): number {\n" + " return input.a;\n" + "}\n" + "export class ConnUniqueQrs {\n" + " pingUniqueQrs(): number {\n" + " return 1;\n" + " }\n" + "}\n"); + write_temp_file(tmp, "src/plain.ts", + "import { fooUniqueQrs } from './lib';\n" + "export function callsPlain(): number {\n" + " return fooUniqueQrs(1);\n" + "}\n"); + write_temp_file(tmp, "src/renamed.ts", + "import { fooUniqueQrs as fooShared } from './lib';\n" + "export function callsRenamed(): number {\n" + " return fooShared(1);\n" + "}\n"); + write_temp_file(tmp, "src/wrapper.ts", + "import { buildHealthUniqueQrs as buildShared } from './lib';\n" + "export function buildHealthUniqueQrs(input: { a: number }): number {\n" + " return buildShared({ ...input });\n" + "}\n"); + write_temp_file(tmp, "src/klass.ts", + "import { ConnUniqueQrs as Connection } from './lib';\n" + "export function viaRenamedClass(): number {\n" + " return new Connection().pingUniqueQrs();\n" + "}\n"); + + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/ts_2297.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); + + bool plain = + cross_file_call_has_strategy(s, project, "callsPlain", "fooUniqueQrs", "lsp_ts_import"); + bool renamed = cross_file_call_has_strategy(s, project, "callsRenamed", "fooUniqueQrs", + "lsp_import_alias"); + bool wrapper = cross_file_call_has_strategy(s, project, "buildHealthUniqueQrs", + "buildHealthUniqueQrs", "lsp_import_alias"); + bool renamed_class = cross_file_call_has_strategy(s, project, "viaRenamedClass", + "pingUniqueQrs", "lsp_ts_method"); + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + + ASSERT_TRUE(plain); + ASSERT_TRUE(renamed); + ASSERT_TRUE(wrapper); + ASSERT_TRUE(renamed_class); + PASS(); +} + /* A query failure is distinct from a valid zero-edge result. */ static int ts514_pipeline_count(cbm_store_t *store, const char *sql, const char **values, int value_count) { @@ -27677,6 +27752,7 @@ SUITE(pipeline) { RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916); RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354); + RUN_TEST(pipeline_ts_renamed_import_call_issue2297); RUN_TEST(pipeline_ts_param_property_injection_issue514); RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge); RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge); diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index d4ed45c0c..a5531452d 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -2799,6 +2799,132 @@ TEST(tslsp_crossfile_no_def_match) { PASS(); } +/* Issue #2297: `import { foo as fooShared }` bound only the local alias, and + * every import lookup spelled `.`, which names nothing, so a call + * through a renamed import produced no CALLS edge. The binding resolves to the + * name the module exports, exactly like a plain named import. Driven through + * the production Tier-2 path (shared sealed registry + per-file overlay). */ +TEST(tslsp_crossfile_renamed_import_issue2297) { + CBMLSPDef defs[] = { + {.qualified_name = "test.lib.foo", + .short_name = "foo", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .return_types = "number"}, + {.qualified_name = "test.conn.Conn", + .short_name = "Conn", + .label = "Class", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.conn", + .method_names_str = "ping"}, + {.qualified_name = "test.conn.Conn.ping", + .short_name = "ping", + .label = "Method", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.conn", + .receiver_type = "test.conn.Conn"}, + {.qualified_name = "test.main.callsRenamed", + .short_name = "callsRenamed", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.viaRenamedClass", + .short_name = "viaRenamedClass", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.main"}, + }; + enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) }; + const char *imp_names[] = {"fooShared", "Connection"}; + const char *imp_qns[] = {"test.lib", "test.conn"}; + + CBMArena arena; + cbm_arena_init(&arena); + CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS); + ASSERT_NOT_NULL(reg); + + const char *src = "import { foo as fooShared } from './lib';\n" + "import { Conn as Connection } from './conn';\n" + "export function callsRenamed() { return fooShared(1); }\n" + "export function viaRenamedClass() { const c = new Connection(); c.ping(); }\n"; + CBMResolvedCallArray out = {0}; + cbm_run_ts_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.main", false, false, + false, reg, defs, NDEFS, imp_names, imp_qns, 2, NULL, &out); + /* The call site names the alias, so the join reads it from `reason`. */ + ASSERT_TRUE( + has_exact_resolved(&out, "test.main.callsRenamed", "test.lib.foo", "lsp_import_alias")); + ASSERT_TRUE(has_exact_resolved(&out, "test.main.viaRenamedClass", "test.conn.Conn.ping", + "lsp_ts_method")); + + cbm_arena_destroy(&arena); + PASS(); +} + +/* The export name is looked up exactly. A renamed import of a name the module + * does not export must not fall back to the alias: before #2297 the alias `foo` + * spelled `test.lib.foo` and bound this call to an unrelated export. */ +TEST(tslsp_crossfile_renamed_import_missing_export_issue2297) { + const char *source = "import { missing as foo } from './lib';\n" + "function go() { foo(1); }\n"; + CBMLSPDef defs[] = { + {.qualified_name = "test.main.go", + .short_name = "go", + .label = "Function", + .def_module_qn = "test.main"}, + {.qualified_name = "test.lib.foo", + .short_name = "foo", + .label = "Function", + .def_module_qn = "test.lib"}, + }; + const char *imp_names[] = {"foo"}; + const char *imp_qns[] = {"test.lib"}; + + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false, + defs, 2, imp_names, imp_qns, 1, NULL, &out); + + ASSERT_EQ(find_resolved_arr(&out, "go", "test.lib.foo"), -1); + + cbm_arena_destroy(&arena); + PASS(); +} + +/* `import { default as greet }` is a default import: it keeps resolving by its + * local name, as `import greet from` does. */ +TEST(tslsp_crossfile_default_as_import_issue2297) { + const char *source = "import { default as greet } from './greetings';\n" + "function go() { greet('hi'); }\n"; + CBMLSPDef defs[] = { + {.qualified_name = "test.main.go", + .short_name = "go", + .label = "Function", + .def_module_qn = "test.main"}, + {.qualified_name = "test.greetings.greet", + .short_name = "greet", + .label = "Function", + .def_module_qn = "test.greetings", + .return_types = "string"}, + }; + const char *imp_names[] = {"greet"}; + const char *imp_qns[] = {"test.greetings"}; + + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false, + defs, 2, imp_names, imp_qns, 1, NULL, &out); + + ASSERT_GTE(find_resolved_arr(&out, "go", "test.greetings.greet"), 0); + + cbm_arena_destroy(&arena); + PASS(); +} + /* ── More stdlib (Date, RegExp, Error, Math) ──────────────────────────────── */ TEST(tslsp_stdlib_date_toISOString) { @@ -5111,6 +5237,9 @@ SUITE(ts_lsp) { RUN_TEST(tslsp_crossfile_function_call); RUN_TEST(tslsp_crossfile_chain_through_return); RUN_TEST(tslsp_crossfile_no_def_match); + RUN_TEST(tslsp_crossfile_renamed_import_issue2297); + RUN_TEST(tslsp_crossfile_renamed_import_missing_export_issue2297); + RUN_TEST(tslsp_crossfile_default_as_import_issue2297); /* More stdlib */ RUN_TEST(tslsp_stdlib_date_toISOString); From c933d0bb4ce0b61bca95c477e536dda2069cb5b1 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Wed, 21 Oct 2026 19:17:12 +0000 Subject: [PATCH 7/8] 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); From 2a9796e0167bf3f5a0a9add55458a626d95949b1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 21 Oct 2026 22:09:32 +0200 Subject: [PATCH 8/8] fix(lsp): preserve overloads through imported TypeScript aliases Signed-off-by: Martin Vogel --- internal/cbm/lsp/ts_lsp.c | 38 +++++++----- internal/cbm/lsp/ts_lsp.h | 1 + tests/test_pipeline.c | 27 ++++++++- tests/test_ts_lsp.c | 122 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+), 18 deletions(-) diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index 27cf50ff2..25961d06f 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -2231,7 +2231,8 @@ static const CBMType *ts_signature_for_call(TSLSPContext *ctx, TSNode function, if (!registered) { const char *module_qn = ts_import_module_for_local(ctx, name, NULL); if (module_qn) - registered = ts_lookup_symbol_for_call(ctx, module_qn, name, args); + registered = ts_lookup_symbol_for_call(ctx, module_qn, + ts_import_export_name(ctx, name), args); } if (registered) return registered->signature; @@ -4048,22 +4049,27 @@ void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *mo } static void ts_lsp_add_import_alias(TSLSPContext *ctx, const char *local, const char *exported) { - int new_count = ctx->import_alias_count + 1; - const char **locals = - (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); - const char **exports = - (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); - if (!locals || !exports) - return; - for (int i = 0; i < ctx->import_alias_count; i++) { - locals[i] = ctx->import_alias_locals[i]; - exports[i] = ctx->import_alias_exports[i]; + if ((size_t)ctx->import_alias_count == ctx->import_alias_capacity) { + /* The arena retains old arrays: grow geometrically so collecting A + * aliases retains O(A) pointers instead of O(A squared). */ + size_t capacity = ctx->import_alias_capacity ? ctx->import_alias_capacity * 2 : 8; + const char **locals = + (const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *)); + const char **exports = + (const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *)); + if (!locals || !exports) + return; + for (int i = 0; i < ctx->import_alias_count; i++) { + locals[i] = ctx->import_alias_locals[i]; + exports[i] = ctx->import_alias_exports[i]; + } + ctx->import_alias_locals = locals; + ctx->import_alias_exports = exports; + ctx->import_alias_capacity = capacity; } - locals[ctx->import_alias_count] = local; - exports[ctx->import_alias_count] = exported; - ctx->import_alias_locals = locals; - ctx->import_alias_exports = exports; - ctx->import_alias_count = new_count; + ctx->import_alias_locals[ctx->import_alias_count] = local; + ctx->import_alias_exports[ctx->import_alias_count] = exported; + ctx->import_alias_count++; } /* Record each `import { foo as bar }` binding of the file (#2297). The import diff --git a/internal/cbm/lsp/ts_lsp.h b/internal/cbm/lsp/ts_lsp.h index 312ae985e..fab7fa578 100644 --- a/internal/cbm/lsp/ts_lsp.h +++ b/internal/cbm/lsp/ts_lsp.h @@ -39,6 +39,7 @@ typedef struct { const char **import_alias_locals; const char **import_alias_exports; int import_alias_count; + size_t import_alias_capacity; // File / surrounding context. const char *module_qn; // QN of this file's module diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 2a5923236..043c38eaa 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -9341,8 +9341,30 @@ TEST(pipeline_ts_renamed_import_call_issue2297) { cross_file_call_has_strategy(s, project, "callsPlain", "fooUniqueQrs", "lsp_ts_import"); bool renamed = cross_file_call_has_strategy(s, project, "callsRenamed", "fooUniqueQrs", "lsp_import_alias"); - bool wrapper = cross_file_call_has_strategy(s, project, "buildHealthUniqueQrs", - "buildHealthUniqueQrs", "lsp_import_alias"); + char wrapper_qn[512]; + char exported_qn[512]; + snprintf(wrapper_qn, sizeof(wrapper_qn), "%s.src.wrapper.buildHealthUniqueQrs", project); + snprintf(exported_qn, sizeof(exported_qn), "%s.src.lib.buildHealthUniqueQrs", project); + cbm_node_t wrapper_node = {0}; + cbm_node_t exported_node = {0}; + bool wrapper = false; + bool recursive = false; + if (cbm_store_find_node_by_qn(s, project, wrapper_qn, &wrapper_node) == CBM_STORE_OK && + cbm_store_find_node_by_qn(s, project, exported_qn, &exported_node) == CBM_STORE_OK) { + cbm_edge_t *edges = NULL; + int edge_count = 0; + cbm_store_find_edges_by_source_type(s, wrapper_node.id, "CALLS", &edges, &edge_count); + for (int i = 0; i < edge_count; i++) { + if (edges[i].target_id == exported_node.id && edges[i].properties_json && + strstr(edges[i].properties_json, "lsp_import_alias")) + wrapper = true; + if (edges[i].target_id == wrapper_node.id) + recursive = true; + } + cbm_store_free_edges(edges, edge_count); + } + cbm_node_free_fields(&wrapper_node); + cbm_node_free_fields(&exported_node); bool renamed_class = cross_file_call_has_strategy(s, project, "viaRenamedClass", "pingUniqueQrs", "lsp_ts_method"); @@ -9353,6 +9375,7 @@ TEST(pipeline_ts_renamed_import_call_issue2297) { ASSERT_TRUE(plain); ASSERT_TRUE(renamed); ASSERT_TRUE(wrapper); + ASSERT_FALSE(recursive); ASSERT_TRUE(renamed_class); PASS(); } diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index a5531452d..5a78fa79c 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -2771,6 +2771,125 @@ TEST(tslsp_crossfile_chain_through_return) { PASS(); } +/* The call target and its return signature must use the same exported name, + * even when the module also exports a function named like the local alias. */ +static int ts_renamed_import_return_chain(bool shared_registry) { + const char *source = "import { make as alias } from './lib';\n" + "function go() { alias().read(); }\n"; + CBMLSPDef defs[] = { + {.qualified_name = "test.main.go", + .short_name = "go", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.main"}, + {.qualified_name = "test.lib.Good", + .short_name = "Good", + .label = "Class", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .method_names_str = "read"}, + {.qualified_name = "test.lib.Good.read", + .short_name = "read", + .label = "Method", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .receiver_type = "test.lib.Good"}, + {.qualified_name = "test.lib.Bad", + .short_name = "Bad", + .label = "Class", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .method_names_str = "read"}, + {.qualified_name = "test.lib.Bad.read", + .short_name = "read", + .label = "Method", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .receiver_type = "test.lib.Bad"}, + {.qualified_name = "test.lib.make", + .short_name = "make", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .return_types = "test.lib.Good"}, + {.qualified_name = "test.lib.alias", + .short_name = "alias", + .label = "Function", + .lang = CBM_LANG_TYPESCRIPT, + .def_module_qn = "test.lib", + .return_types = "test.lib.Bad"}, + }; + enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) }; + const char *imp_names[] = {"alias"}; + const char *imp_qns[] = {"test.lib"}; + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + if (shared_registry) { + CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS); + ASSERT_NOT_NULL(reg); + cbm_run_ts_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", false, + false, false, reg, defs, NDEFS, imp_names, imp_qns, 1, + NULL, &out); + } else { + cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false, + defs, NDEFS, imp_names, imp_qns, 1, NULL, &out); + } + bool direct = has_exact_resolved(&out, "test.main.go", "test.lib.make", "lsp_import_alias"); + bool good = has_exact_resolved(&out, "test.main.go", "test.lib.Good.read", "lsp_ts_method"); + bool bad = has_exact_resolved(&out, "test.main.go", "test.lib.Bad.read", "lsp_ts_method"); + cbm_arena_destroy(&arena); + ASSERT_TRUE(direct); + ASSERT_FALSE(bad); + ASSERT_TRUE(good); + PASS(); +} + +TEST(tslsp_crossfile_renamed_import_return_chain) { + return ts_renamed_import_return_chain(false); +} + +TEST(tslsp_crossfile_renamed_import_return_chain_shared) { + return ts_renamed_import_return_chain(true); +} + +/* Keep the supplied import map at one entry: this measures the AST alias + * collector independently of the older import-map insertion path. */ +static size_t ts_import_alias_arena_bytes(int count) { + size_t capacity = (size_t)count * 48 + 64; + char *source = malloc(capacity); + if (!source) + return 0; + size_t len = (size_t)snprintf(source, capacity, "import { "); + for (int i = 0; i < count; i++) { + len += (size_t)snprintf(source + len, capacity - len, "%sf%d as a%d", i ? ", " : "", i, i); + } + len += (size_t)snprintf(source + len, capacity - len, " } from './lib';\n"); + const char *imp_names[] = {"a0"}; + const char *imp_qns[] = {"test.lib"}; + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + cbm_run_ts_lsp_cross(&arena, source, (int)len, "test.main", false, false, false, NULL, 0, + imp_names, imp_qns, 1, NULL, &out); + size_t bytes = cbm_arena_total(&arena); + cbm_arena_destroy(&arena); + free(source); + return bytes; +} + +TEST(tslsp_import_alias_arena_growth) { + size_t small = ts_import_alias_arena_bytes(1024); + size_t large = ts_import_alias_arena_bytes(2048); + ASSERT_GT(small, 0); + ASSERT_GT(large, small); + printf(" aliases arena bytes: 1024=%zu 2048=%zu ", small, large); + /* Linear source/AST work plus geometric storage stays below 3x; retaining + * two complete arrays at every insertion grows approximately 4x. */ + ASSERT(large < small * 3); + PASS(); +} + TEST(tslsp_crossfile_no_def_match) { /* Function that imports a symbol not in the def list — should resolve safely * (emit unresolved, no crash). */ @@ -5236,6 +5355,9 @@ SUITE(ts_lsp) { RUN_TEST(tslsp_scale_many_defs_no_crash_issue344); RUN_TEST(tslsp_crossfile_function_call); RUN_TEST(tslsp_crossfile_chain_through_return); + RUN_TEST(tslsp_crossfile_renamed_import_return_chain); + RUN_TEST(tslsp_crossfile_renamed_import_return_chain_shared); + RUN_TEST(tslsp_import_alias_arena_growth); RUN_TEST(tslsp_crossfile_no_def_match); RUN_TEST(tslsp_crossfile_renamed_import_issue2297); RUN_TEST(tslsp_crossfile_renamed_import_missing_export_issue2297);