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 ef86c332d..4236c0742 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -8317,6 +8317,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 3de20c0d2..3f053218d 100644 --- a/internal/cbm/lsp/go_lsp.c +++ b/internal/cbm/lsp/go_lsp.c @@ -2711,6 +2711,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. @@ -3279,6 +3300,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); @@ -3576,6 +3598,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 3b504d007..37a0b787a 100644 --- a/internal/cbm/lsp/go_lsp.h +++ b/internal/cbm/lsp/go_lsp.h @@ -105,6 +105,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 ed8af87d9..a0a769fd1 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 56e00e202..2727e41d1 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -2559,7 +2559,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 900c3c882..1f0d58087 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", NULL, 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", NULL, + 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" @@ -1669,6 +1790,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 ce60edf58..b2c6a888a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -11266,6 +11266,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. @@ -26281,7 +26376,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]); @@ -26412,24 +26507,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\":[]}]}}", @@ -26449,7 +26544,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\":[]}]}}"; @@ -26477,7 +26572,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\"," @@ -26567,10 +26662,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); @@ -29148,6 +29243,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_go_multi_init_nodes_survive); RUN_TEST(pipeline_go_init_lsp_edges_per_file); RUN_TEST(pipeline_go_init_lsp_edges_per_file_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);