From 255463126650ab6deab3e62aab441b71479704f1 Mon Sep 17 00:00:00 2001 From: News Date: Wed, 23 Sep 2026 08:21:53 +0900 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 2a9796e0167bf3f5a0a9add55458a626d95949b1 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 21 Oct 2026 22:09:32 +0200 Subject: [PATCH 4/5] 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); From 4b03f7afea9bacaa9368f6b0da086f8e51bd7cb8 Mon Sep 17 00:00:00 2001 From: Gaurav Dubey Date: Thu, 22 Oct 2026 12:54:35 +0000 Subject: [PATCH 5/5] fix(go-lsp): follow a type alias declared in another file of the package `CBMRegisteredType.alias_of` is followed by the method lookup (go_lookup_field_or_method_depth), but only the per-file AST scan ever set it, and that scan only sees the file it walks. The def-driven registration that builds the cross-file registries had no alias target to copy, because neither CBMDefinition nor CBMLSPDef carried one. So a `type FeatureSet = bitmap` in one file was invisible to `reqs.Set()` in another. Since receiver-qualified method QNs (#1913), the call no longer resolves by name coincidence either, and it got no CALLS edge at all. The Go extractor now records the aliased type text of a `type A = B` (CBMDefinition.alias_target). pass_lsp_cross copies it into CBMLSPDef, and both cross-file registrars (cbm_run_go_lsp_cross and cbm_go_build_cross_registry) qualify it against the def's module and set alias_of. Only a bare identifier target is qualified, which is a type in the same package. A qualified, pointer, composite or generic target is left unset rather than guessed. A defined type `type A B` is a type_spec, so it never gets an alias target and keeps its own method set. The persisted LSP surface carries the new field ("al"), so SURFACE_CODEC_VERSION goes from 3 to 4: existing caches re-extract once, and an incremental run sees the same alias as a full one. The literal version-3 surfaces in the codec tests move to 4. Tests: golsp_crossfile_type_alias_method (+_prebuilt) check both registries, with a same-named decoy and a defined-type negative. pipeline_go_crossfile_type_alias_method (+_parallel) index the three-file package from the issue. Before the fix, Use had no CALLS edge to Set. Fixes #2033 Signed-off-by: Gaurav Dubey --- internal/cbm/cbm.h | 6 ++ internal/cbm/extract_defs.c | 9 +++ internal/cbm/lsp/go_lsp.c | 23 ++++++ internal/cbm/lsp/go_lsp.h | 4 ++ internal/cbm/result_compact.c | 1 + src/pipeline/lsp_surface.c | 8 ++- src/pipeline/pass_lsp_cross.c | 3 + src/pipeline/pipeline.c | 2 +- tests/test_go_lsp.c | 123 ++++++++++++++++++++++++++++++++ tests/test_pipeline.c | 127 ++++++++++++++++++++++++++++++---- 10 files changed, 288 insertions(+), 18 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index ade41fea5..dd1559c1b 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -313,6 +313,12 @@ typedef struct { * change of all, store_impact.h follow_variant_keys). NULL on every other * definition. Tail field: zero-init stays valid. */ const char *variant_key; + /* Go only (#2033): the raw aliased type text of an identity alias + * `type A = B` ("B"). Carried to the cross-file registries so a method + * call on A from another file follows the alias to B's method set. NULL + * for a defined type `type A B` (its method set is its own) and in every + * other language. Tail field: zero-init stays valid. */ + const char *alias_target; } CBMDefinition; /* Argument captured from a call expression */ diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index 55596732d..df8a3c418 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -8299,6 +8299,15 @@ static void emit_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s def.decl_namespace = cs_type_decl_namespace(a, node, ctx->source); } def.base_classes = extract_base_classes(a, node, ctx->source, ctx->language); + // Go identity alias `type A = B` (#2033): keep the aliased type text so the + // cross-file registries can set alias_of. A defined type `type A B` is a + // type_spec and never gets here with this kind. + if (ctx->language == CBM_LANG_GO && strcmp(kind, "type_alias") == 0) { + TSNode target = ts_node_child_by_field_name(node, TS_FIELD("type")); + if (!ts_node_is_null(target)) { + def.alias_target = cbm_node_text(a, target, ctx->source); + } + } def.decorators = extract_decorators(a, node, ctx->source, ctx->language, spec); def.docstring = extract_docstring(ctx, node, name); diff --git a/internal/cbm/lsp/go_lsp.c b/internal/cbm/lsp/go_lsp.c index 34fad4b9e..dd9572747 100644 --- a/internal/cbm/lsp/go_lsp.c +++ b/internal/cbm/lsp/go_lsp.c @@ -2705,6 +2705,27 @@ static const char **split_pipe_strings(CBMArena *a, const char *text) { return idx > 0 ? arr : NULL; } +/* QN of the type an identity alias `type A = B` names, for the def-driven + * cross-file registries (#2033). The per-file AST scan sets alias_of only for + * the file it walks, so an alias declared in another file of the package + * reaches a call only through here. Only a bare identifier is qualified (B + * declared in A's package); a qualified, pointer, composite or generic target + * stays NULL rather than guessed. */ +static const char *go_alias_target_qn(CBMArena *arena, const char *target, + const char *def_module_qn) { + if (!target || !target[0] || !def_module_qn || !def_module_qn[0]) { + return NULL; + } + for (const unsigned char *p = (const unsigned char *)target; *p; p++) { + bool ident = (*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || + (*p >= '0' && *p <= '9') || *p == '_' || *p >= 0x80; + if (!ident) { + return NULL; + } + } + return cbm_arena_sprintf(arena, "%s.%s", def_module_qn, target); +} + // Helper: parse "|"-separated "name:type" field definitions and populate a registered type. // Format: "Binder:Binder|Name:string|Count:int" // type_text is resolved relative to def_module_qn. @@ -3273,6 +3294,7 @@ void cbm_run_go_lsp_cross(CBMArena *arena, const char *source, int source_len, if (rt.is_interface && d->method_names_str && d->method_names_str[0]) { rt.method_names = split_pipe_strings(arena, d->method_names_str); } + rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod); cbm_registry_add_type(®, rt); @@ -3569,6 +3591,7 @@ CBMTypeRegistry *cbm_go_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i if (rt.is_interface && d->method_names_str && d->method_names_str[0]) { rt.method_names = split_pipe_strings(arena, d->method_names_str); } + rt.alias_of = go_alias_target_qn(arena, d->alias_target, def_mod); cbm_registry_add_type(reg, rt); GO_NOTE_NEW_TYPES(); if (d->field_defs && d->field_defs[0]) { diff --git a/internal/cbm/lsp/go_lsp.h b/internal/cbm/lsp/go_lsp.h index 5874cbeef..9e742a715 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -102,6 +102,10 @@ typedef struct { /* Callable identity (#2061): signature suffix of this def's graph QN; * qualified_name stays the base QN. NULL = none (always, until enabled). */ const char *sig; + /* Go only (#2033): raw aliased type text of `type A = B` ("B"), qualified + * by the registrar against def_module_qn like return_types. NULL = not an + * identity alias. */ + const char *alias_target; } CBMLSPDef; // Parse source, build registry from defs + stdlib, run LSP. diff --git a/internal/cbm/result_compact.c b/internal/cbm/result_compact.c index 76606ac51..46bf6eae1 100644 --- a/internal/cbm/result_compact.c +++ b/internal/cbm/result_compact.c @@ -279,6 +279,7 @@ static void cr_walk_def(cr_ctx_t *c, CBMDefinition *d) { cr_list(c, &d->extra_props); cr_str(c, &d->decl_namespace); cr_str(c, &d->variant_key); + cr_str(c, &d->alias_target); } static void cr_walk_call(cr_ctx_t *c, CBMCall *call) { diff --git a/src/pipeline/lsp_surface.c b/src/pipeline/lsp_surface.c index 308fcf959..f8d43ff69 100644 --- a/src/pipeline/lsp_surface.c +++ b/src/pipeline/lsp_surface.c @@ -34,8 +34,10 @@ #include "yyjson/yyjson.h" /* 3: Go defs carry their parsed result list `|`-joined in "ret" (F-G8 D1); - * a version-2 surface holds the raw result text and must be re-extracted. */ -enum { SURFACE_CODEC_VERSION = 3 }; + * a version-2 surface holds the raw result text and must be re-extracted. + * 4: Go identity aliases carry their target in "al" (#2033); a version-3 + * surface has no alias targets and must be re-extracted. */ +enum { SURFACE_CODEC_VERSION = 4 }; /* Labels the incremental name registry serves that pxc_map_label does NOT * carry into the CBMLSPDef set. Their (name, qn, label) triple must still @@ -394,6 +396,7 @@ static char *surface_file_to_json(const CBMFileResult *result, CBMLanguage langu yyjson_mut_obj_add_bool(doc, o, "ir", d->is_rust_impl_relation); yyjson_mut_obj_add_bool(doc, o, "ab", d->is_abstract); add_str_array_or_null(doc, o, "dec", d->decorators, -1); + add_str_or_null(doc, o, "al", d->alias_target); yyjson_mut_arr_add_val(lsp, o); } yyjson_mut_obj_add_val(doc, root, "lsp", lsp); @@ -670,6 +673,7 @@ int cbm_lsp_surface_defs_from_json(CBMArena *arena, const char *defs_json, CBMLS d->is_abstract = yyjson_get_bool(yyjson_obj_get(o, "ab")); int dec_count = 0; d->decorators = arena_str_array(arena, yyjson_obj_get(o, "dec"), true, &dec_count); + d->alias_target = arena_str_or_null(arena, yyjson_obj_get(o, "al")); if (!d->qualified_name || !d->short_name || !d->label) { /* A def the writer could not have produced: qn/sn/label are * unconditionally present in build_lsp_def output. Corrupt row. */ diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 5aa955112..a057f9a7b 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -715,6 +715,9 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch } } dst->lang = lang; + dst->alias_target = (lang == CBM_LANG_GO && src->alias_target) + ? cbm_arena_strdup(arena, src->alias_target) + : NULL; dst->decorators = NULL; if (src->decorators) { int n = 0; diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index 698d9dc7a..c03bf11c1 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -2148,7 +2148,7 @@ static void log_result_census(const char *tag, CBMFileResult **cache, int file_c str_def_misc += census_len(def->route_path) + census_len(def->route_method) + census_len(def->impl_trait) + census_len(def->http_base_url) + census_list(def->extra_props) + census_len(def->decl_namespace) + - census_len(def->variant_key); + census_len(def->variant_key) + census_len(def->alias_target); } for (int c = 0; c < r->calls.count; c++) { const CBMCall *call = &r->calls.items[c]; diff --git a/tests/test_go_lsp.c b/tests/test_go_lsp.c index e852f4aca..b79101c07 100644 --- a/tests/test_go_lsp.c +++ b/tests/test_go_lsp.c @@ -1378,6 +1378,127 @@ TEST(golsp_crossfile_aliased_field_requal) { PASS(); } +/* #2033: an identity alias declared in another file of the same package. + * `type FeatureSet = bitmap` lives in one file, `bitmap.Set` in a second, the + * call `reqs.Set` in a third, so none of it is in the calling file's AST and + * only the defs can carry the alias. A defined type (`type Defined bitmap`) + * must not inherit bitmap's methods. The decoy `other.Set` keeps a name-only + * match from passing. Both cross-file registries are checked: the per-file + * one and the prebuilt one shared by the parallel resolve. */ +static CBMLSPDef go_crossfile_alias_defs[] = { + {.qualified_name = "test.main.bitmap", + .short_name = "bitmap", + .label = "Class", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.bitmap.Set", + .short_name = "Set", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.bitmap"}, + {.qualified_name = "test.main.FeatureSet", + .short_name = "FeatureSet", + .label = "Type", + .def_module_qn = "test.main", + .alias_target = "bitmap"}, + {.qualified_name = "test.main.Defined", + .short_name = "Defined", + .label = "Class", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.Framework", + .short_name = "Framework", + .label = "Struct", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.Framework.NewFeatureSet", + .short_name = "NewFeatureSet", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.Framework", + .return_types = "FeatureSet"}, + {.qualified_name = "test.main.Framework.NewDefined", + .short_name = "NewDefined", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.Framework", + .return_types = "Defined"}, + {.qualified_name = "test.main.other", + .short_name = "other", + .label = "Struct", + .def_module_qn = "test.main"}, + {.qualified_name = "test.main.other.Set", + .short_name = "Set", + .label = "Method", + .def_module_qn = "test.main", + .receiver_type = "test.main.other"}, +}; + +static const char *go_crossfile_alias_source = "package main\n\n" + "func useAlias(f *Framework) {\n" + "\treqs := f.NewFeatureSet()\n" + "\treqs.Set(3)\n}\n\n" + "func useDefined(f *Framework) {\n" + "\td := f.NewDefined()\n" + "\td.Set(3)\n}\n"; + +static int go_crossfile_alias_check(const CBMResolvedCallArray *out) { + int idx = find_resolved_arr_confident(out, "useAlias", ".Set"); + if (idx < 0 || strcmp(out->items[idx].callee_qn, "test.main.bitmap.Set") != 0) { + printf(" resolved calls (%d):\n", out->count); + for (int i = 0; i < out->count; i++) { + const CBMResolvedCall *rc = &out->items[i]; + printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", + rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", + rc->confidence); + } + } + ASSERT_GTE(idx, 0); + ASSERT_STR_EQ(out->items[idx].callee_qn, "test.main.bitmap.Set"); + /* Reached through alias_of, so the emitter reports it the way it reports + * a same-file alias today (lsp_embed_dispatch): typed, not a name guess. */ + ASSERT_TRUE(strncmp(out->items[idx].strategy, "lsp_", 4) == 0); + ASSERT_TRUE(strcmp(out->items[idx].strategy, "lsp_unresolved") != 0); + ASSERT_TRUE(out->items[idx].confidence >= 0.9f); + /* A defined type has its own (empty) method set. */ + ASSERT_EQ(find_resolved_arr_confident(out, "useDefined", "bitmap.Set"), -1); + return 0; +} + +TEST(golsp_crossfile_type_alias_method) { + int n = (int)(sizeof(go_crossfile_alias_defs) / sizeof(go_crossfile_alias_defs[0])); + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + cbm_run_go_lsp_cross(&arena, go_crossfile_alias_source, (int)strlen(go_crossfile_alias_source), + "test.main", go_crossfile_alias_defs, n, NULL, NULL, 0, NULL, &out); + if (go_crossfile_alias_check(&out) != 0) { + cbm_arena_destroy(&arena); + return 1; + } + + cbm_arena_destroy(&arena); + PASS(); +} + +TEST(golsp_crossfile_type_alias_method_prebuilt) { + int n = (int)(sizeof(go_crossfile_alias_defs) / sizeof(go_crossfile_alias_defs[0])); + CBMArena arena; + cbm_arena_init(&arena); + CBMResolvedCallArray out = {0}; + + CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, go_crossfile_alias_defs, n); + ASSERT_NOT_NULL(reg); + cbm_run_go_lsp_cross_with_registry(&arena, go_crossfile_alias_source, + (int)strlen(go_crossfile_alias_source), "test.main", reg, + NULL, NULL, 0, NULL, &out); + if (go_crossfile_alias_check(&out) != 0) { + cbm_arena_destroy(&arena); + return 1; + } + + cbm_arena_destroy(&arena); + PASS(); +} + TEST(golsp_crossfile_local_interface_single_impl) { const char *source = "package main\n\n" @@ -1542,6 +1663,8 @@ SUITE(go_lsp) { RUN_TEST(golsp_crossfile_interface_dispatch); RUN_TEST(golsp_crossfile_interface_field_chain); RUN_TEST(golsp_crossfile_aliased_field_requal); + RUN_TEST(golsp_crossfile_type_alias_method); + RUN_TEST(golsp_crossfile_type_alias_method_prebuilt); RUN_TEST(golsp_crossfile_map_index); RUN_TEST(golsp_crossfile_stdlib_interface); RUN_TEST(golsp_crossfile_local_interface_single_impl); diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 75e5cb43d..abe61ad8a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -10470,6 +10470,101 @@ TEST(pipeline_go_multi_return_receiver_is_typed_parallel) { return go_multi_return_case(52); } +/* #2033: an identity alias declared in one file is followed from another file + * of the same package. `type FeatureSet = bitmap` (featureset.go), the method + * `bitmap.Set` (bitmap.go) and the call `reqs.Set(3)` (framework.go) are three + * files, so the alias reaches the call only through the cross-file defs. The + * same-package decoy `other.Set` means the call cannot bind by name. Before the + * fix the call had no CALLS edge at all. pad_files > 0 forces the parallel + * resolver and its prebuilt registry. */ +static int go_crossfile_type_alias_case(int pad_files) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_alias_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_temp_file(tmp, "go.mod", "module example.com/fx\n\ngo 1.22\n"); + write_temp_file(tmp, "fs/bitmap.go", + "package fs\n" + "\n" + "type bitmap []uint64\n" + "\n" + "func (b bitmap) Set(i int) { b[i/64] |= 1 << (i % 64) }\n"); + write_temp_file(tmp, "fs/featureset.go", + "package fs\n" + "\n" + "type FeatureSet = bitmap\n"); + write_temp_file( + tmp, "fs/framework.go", + "package fs\n" + "\n" + "type Framework struct{}\n" + "\n" + "func (f *Framework) NewFeatureSet() FeatureSet { return make(FeatureSet, 1) }\n" + "\n" + "func Use(f *Framework) {\n" + "\treqs := f.NewFeatureSet()\n" + "\treqs.Set(3)\n" + "}\n"); + write_temp_file(tmp, "fs/other.go", + "package fs\n" + "\n" + "type other struct{}\n" + "\n" + "func (o other) Set(i int) {}\n"); + for (int i = 0; i < pad_files; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "filler/filler%d.go", i); + snprintf(body, sizeof(body), "package filler\n\nfunc Filler%d() int { return %d }\n", i, i); + write_temp_file(tmp, name, body); + } + char *saved = NULL; + if (pad_files > 0) { + char *old_workers = getenv("CBM_WORKERS"); + saved = old_workers ? strdup(old_workers) : NULL; + cbm_setenv("CBM_WORKERS", "4", 1); + } + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/go_alias.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int rc = p ? cbm_pipeline_run(p) : -1; + if (pad_files > 0) { + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + } + ASSERT_NOT_NULL(p); + ASSERT_EQ(rc, 0); + const char *project = cbm_pipeline_project_name(p); + cbm_store_t *s = cbm_store_open_path(db_path); + ASSERT_NOT_NULL(s); + + int right = named_edge_to_file_count(s, project, "CALLS", "Use", "Set", "fs/bitmap.go"); + int decoy = named_edge_to_file_count(s, project, "CALLS", "Use", "Set", "fs/other.go"); + bool typed = cross_file_call_has_strategy(s, project, "Use", "Set", "\"strategy\":\"lsp_"); + if (right != 1 || decoy != 0 || !typed) { + printf(" Use -> Set: right=%d decoy=%d lsp=%d\n", right, decoy, typed); + FAIL("call through a cross-file type alias not bound to the aliased type's method"); + } + + cbm_store_close(s); + cbm_pipeline_free(p); + th_rmtree(tmp); + PASS(); +} + +TEST(pipeline_go_crossfile_type_alias_method) { + return go_crossfile_type_alias_case(0); +} + +TEST(pipeline_go_crossfile_type_alias_method_parallel) { + return go_crossfile_type_alias_case(52); +} + TEST(pipeline_go_rw_usage_never_cross_into_c) { /* #1928: USAGE and WRITES/READS resolve through the same short-name * registry as CALLS but never consulted the #725 cross-language guard. @@ -25311,7 +25406,7 @@ TEST(pipeline_python_namespace_change_admission) { } int body = -9, signature = -9, alias = -9, order = -9; int empty = -9, literal = -9, unknown = -9, deletion = -9, transition = -9; - const char *non_python = "{\"v\":3,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}"; + const char *non_python = "{\"v\":4,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}"; if (complete) { body = cbm_lsp_surface_python_change(json[0], json[1]); signature = cbm_lsp_surface_python_change(json[0], json[2]); @@ -25442,24 +25537,24 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) { * it re-extracts in full. */ const char *bad[] = { "{\"v\":1,\"lsp\":[]}", - "{\"v\":3,\"lang\":1,\"lsp\":[]}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"lang\":1.0,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"v\":3,\"lang\":0,\"lsp\":[],\"py\":null}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[]}", + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"lang\":1.0,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"v\":4,\"lang\":0,\"lsp\":[],\"py\":null}", + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":3,\"failure\":2,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\\u0000suffix\",\"status\":2," "\"failure\":0,\"events\":[]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":999,\"d\":0,\"s\":0,\"r\":0,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", - "{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":8,\"d\":0,\"s\":1,\"r\":0,\"f\":0,\"l\":4294967296," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}", @@ -25479,7 +25574,7 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) { cbm_arena_destroy(&arena); } const char *unknown = - "{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1," + "{\"v\":4,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":2,\"lang\":1," "\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[" "{\"k\":7,\"d\":0,\"s\":0,\"r\":7,\"f\":0,\"l\":0," "\"n\":null,\"m\":null,\"i\":null,\"a\":[]}]}}"; @@ -25507,7 +25602,7 @@ TEST(pipeline_lsp_surface_large_object_array_decode) { const size_t capacity = (size_t)count * 192 + 64; char *json = malloc(capacity); ASSERT_NOT_NULL(json); - size_t used = (size_t)snprintf(json, capacity, "{\"v\":3,\"lang\":0,\"py\":null,\"lsp\":["); + size_t used = (size_t)snprintf(json, capacity, "{\"v\":4,\"lang\":0,\"py\":null,\"lsp\":["); for (int i = 0; i < count; i++) { int n = snprintf(json + used, capacity - used, "%s{\"qn\":\"pkg.f%d\",\"sn\":\"f%d\",\"lb\":\"Function\"," @@ -25597,10 +25692,10 @@ TEST(pipeline_lsp_surface_persisted_and_body_edit_invariant) { ASSERT_TRUE(row_a->surface_sha && row_a->surface_sha[0]); if (round == 0) { snprintf(sha_baseline, sizeof(sha_baseline), "%s", row_a->surface_sha); - /* The row is the versioned codec envelope (codec version 3) with + /* The row is the versioned codec envelope (codec version 4) with * the def present. */ snprintf(json_probe, sizeof(json_probe), "%.20s", row_a->defs_json); - ASSERT_TRUE(strstr(row_a->defs_json, "\"v\":3") != NULL); + ASSERT_TRUE(strstr(row_a->defs_json, "\"v\":4") != NULL); ASSERT_TRUE(strstr(row_a->defs_json, "surface_probe") != NULL); } else if (round == 1) { ASSERT_STR_EQ(row_a->surface_sha, sha_baseline); @@ -28168,6 +28263,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge); RUN_TEST(pipeline_go_multi_return_receiver_is_typed); RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel); + RUN_TEST(pipeline_go_crossfile_type_alias_method); + RUN_TEST(pipeline_go_crossfile_type_alias_method_parallel); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c); RUN_TEST(pipeline_go_rw_usage_never_cross_into_c_parallel); RUN_TEST(pipeline_rust_call_never_binds_another_language);