fix(go-lsp): cross-file multi-value and named results type their variables

The cross-file pass handed every registrar the raw result text of a
def (pass_lsp_cross.c: `dst->return_types = src->return_type`). The Go
registrar splits that string on '|' (go_lsp.c split_pipe_types), so it
got `(*Encoder, error)` as one piece. That piece did not parse, and a
cross-file callee with more than one result, or with a named result
`(e *Encoder)`, had no usable return type. As a result,
`e, err := enc.NewEncoder()`, `h, _ := newHead()` and
`_, d := diag.Run()` all left the variable untyped, and calls on it fell
to name guesses. Same-file calls were fine, because they read the AST.

Go defs now pass the parsed result list (extract_go_multi_return),
'|'-joined: `*Encoder|error`, and `*Encoder` for the named result. Other
languages keep the single raw string.

The persisted LSP surface carries this string, so SURFACE_CODEC_VERSION
goes from 2 to 3: existing caches re-extract Go surfaces once. The
literal version-2 surfaces in the codec tests move to 3, so that they
keep testing the envelope rule they name rather than the version
mismatch.

Tests: pipeline_go_multi_return_receiver_is_typed (+_parallel). A tuple
result, a named result, a tuple-bound local reached through a field,
and a named slice type each must bind the right method with an lsp_*
strategy and none of its two same-named decoys.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-10-19 02:54:41 +02:00
1 parent 658639ec56
commit 36bce46e68
3 files changed
+189 -17

No files matched your search

+3 -1
View File
@@ -33,7 +33,9 @@
#include "pipeline/worker_pool.h"
#include "yyjson/yyjson.h"
enum { SURFACE_CODEC_VERSION = 2 };
/* 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 };
/* Labels the incremental name registry serves that pxc_map_label does NOT
* carry into the CBMLSPDef set. Their (name, qn, label) triple must still
+11 -4
View File
@@ -513,10 +513,17 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
src->decl_namespace ? cbm_arena_strdup(arena, src->decl_namespace) : NULL;
}
dst->is_interface = (strcmp(label, "Interface") == 0 || strcmp(label, "Protocol") == 0);
/* Single return-type string. The per-language registrars split on '|'
* for multi-return languages (Go); single-return languages just see one
* piece, which is what's already stored. */
dst->return_types = src->return_type ? cbm_arena_strdup(arena, src->return_type) : NULL;
/* The return-type string. The Go registrar splits it on '|' (go_lsp.c
* split_pipe_types), so Go gets the PARSED result list, joined: `(*T,
* error)` arrives as `*T|error` and a named result `(t *T)` as `*T`. The
* raw result text it used to get did not parse, so every variable bound
* by a cross-file multi-value or named-result call (`t, err := pkg.New()`)
* stayed untyped (F-G8 D1). Other languages keep the single raw string. */
if (lang == CBM_LANG_GO && src->return_types && src->return_types[0]) {
dst->return_types = pxc_join_pipe(arena, src->return_types);
} else {
dst->return_types = src->return_type ? cbm_arena_strdup(arena, src->return_type) : NULL;
}
/* Languages whose cross registrars read embedded_types as QNs get their
* bases resolved against the project registry; everyone else keeps the
* raw source spelling their own registrar already knows how to handle. */
+175 -12
View File
@@ -8819,6 +8819,167 @@ static void write_go_c_ref_guard_fixture(const char *tmp, int pad_files) {
}
}
/* F-G8 D1: a variable bound by a CROSS-file multi-value or named-result call
* is typed. The cross-file registrar received the raw result text
* `(*Encoder, error)`, which did not parse, so `e, err := enc.NewEncoder()`
* left e untyped and e.EncodeTuple() fell to a name guess. Every target
* method has two same-named decoys, so a guess cannot pass. pad_files > 0
* forces the parallel resolver. */
static void write_go_multi_return_fixture(const char *tmp, int pad_files) {
write_temp_file(tmp, "go.mod", "module example.com/fx\n\ngo 1.22\n");
write_temp_file(tmp, "enc/enc.go",
"package enc\n"
"\n"
"type Encoder struct{ n int }\n"
"\n"
"func NewEncoder() (*Encoder, error) { return &Encoder{}, nil }\n"
"\n"
"func NewNamed() (e *Encoder) { e = &Encoder{}; return }\n"
"\n"
"func (e *Encoder) EncodeTuple() int { return e.n }\n"
"\n"
"func (e *Encoder) EncodeNamed() int { return e.n }\n");
write_temp_file(tmp, "diag/diag.go",
"package diag\n"
"\n"
"type Diagnostic struct{ msg string }\n"
"\n"
"type Diagnostics []Diagnostic\n"
"\n"
"func (d Diagnostics) HasErrorsSlice() bool { return len(d) > 0 }\n"
"\n"
"func Run() (int, Diagnostics) { return 0, nil }\n");
write_temp_file(tmp, "app/types.go",
"package app\n"
"\n"
"type isolation struct{}\n"
"\n"
"func (i *isolation) LowWatermarkField() int { return 0 }\n"
"\n"
"type Head struct{ iso *isolation }\n"
"\n"
"func newHead() (*Head, error) { return &Head{iso: &isolation{}}, nil }\n");
write_temp_file(tmp, "app/app.go",
"package app\n"
"\n"
"import (\n"
"\t\"example.com/fx/diag\"\n"
"\t\"example.com/fx/enc\"\n"
")\n"
"\n"
"func UseTuple() int {\n"
"\te, err := enc.NewEncoder()\n"
"\t_ = err\n"
"\treturn e.EncodeTuple()\n"
"}\n"
"\n"
"func UseNamed() int {\n"
"\te := enc.NewNamed()\n"
"\treturn e.EncodeNamed()\n"
"}\n"
"\n"
"func UseField() int {\n"
"\th, _ := newHead()\n"
"\treturn h.iso.LowWatermarkField()\n"
"}\n"
"\n"
"func UseSlice() bool {\n"
"\t_, d := diag.Run()\n"
"\treturn d.HasErrorsSlice()\n"
"}\n");
write_temp_file(tmp, "decoy/decoy.go",
"package decoy\n"
"\n"
"type D1 struct{}\n"
"\n"
"type D2 struct{}\n"
"\n"
"func (D1) EncodeTuple() int { return 0 }\n"
"func (D2) EncodeTuple() int { return 0 }\n"
"func (D1) EncodeNamed() int { return 0 }\n"
"func (D2) EncodeNamed() int { return 0 }\n"
"func (D1) LowWatermarkField() int { return 0 }\n"
"func (D2) LowWatermarkField() int { return 0 }\n"
"func (D1) HasErrorsSlice() bool { return false }\n"
"func (D2) HasErrorsSlice() bool { return false }\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);
}
}
static int go_multi_return_case(int pad_files) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_multiret_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_go_multi_return_fixture(tmp, pad_files);
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_multiret.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);
static const struct {
const char *caller;
const char *method;
const char *file;
} want[] = {
{"UseTuple", "EncodeTuple", "enc/enc.go"}, /* e, err := enc.NewEncoder() */
{"UseNamed", "EncodeNamed", "enc/enc.go"}, /* named result (e *Encoder) */
{"UseField", "LowWatermarkField", "app/types.go"}, /* h, _ := newHead(); h.iso.X() */
{"UseSlice", "HasErrorsSlice", "diag/diag.go"}, /* _, d := diag.Run() */
};
for (size_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) {
int right = named_edge_to_file_count(s, project, "CALLS", want[i].caller, want[i].method,
want[i].file);
int decoy = named_edge_to_file_count(s, project, "CALLS", want[i].caller, want[i].method,
"decoy/decoy.go");
bool typed = cross_file_call_has_strategy(s, project, want[i].caller, want[i].method,
"\"strategy\":\"lsp_");
if (right < 1 || decoy != 0 || !typed) {
printf(" %s -> %s: right=%d decoy=%d lsp=%d\n", want[i].caller, want[i].method, right,
decoy, typed);
FAIL("multi-value / named-result receiver not typed");
}
}
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
TEST(pipeline_go_multi_return_receiver_is_typed) {
return go_multi_return_case(0);
}
TEST(pipeline_go_multi_return_receiver_is_typed_parallel) {
return go_multi_return_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.
@@ -21988,7 +22149,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\":2,\"lang\":0,\"lsp\":[],\"reg\":[],\"py\":null}";
const char *non_python = "{\"v\":3,\"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]);
@@ -22115,22 +22276,22 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) {
* Even an empty LSP array must validate its namespace envelope. */
const char *bad[] = {
"{\"v\":1,\"lsp\":[]}",
"{\"v\":2,\"lang\":1,\"lsp\":[]}",
"{\"v\":2,\"lang\":1,\"lsp\":[],\"py\":null}",
"{\"v\":2,\"lang\":1.0,\"lsp\":[],\"py\":null}",
"{\"v\":2,\"v\":2,\"lang\":0,\"lsp\":[],\"py\":null}",
"{\"v\":2,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
"{\"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\":1,\"lang\":1,"
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":3,\"failure\":2,\"events\":[]}}",
"{\"v\":2,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1,"
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":99,\"lang\":1,"
"\"module\":\"p.m\",\"path\":\"m.py\",\"status\":2,\"failure\":0,\"events\":[]}}",
"{\"v\":2,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
"\"module\":\"p.m\",\"path\":\"m.py\\u0000suffix\",\"status\":2,"
"\"failure\":0,\"events\":[]}}",
"{\"v\":2,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"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\":2,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"lang\":1,"
"{\"v\":3,\"lang\":1,\"lsp\":[],\"py\":{\"v\":1,\"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\":[]}]}}",
@@ -22150,7 +22311,7 @@ TEST(pipeline_python_namespace_codec_rejects_non_authoritative_rows) {
cbm_arena_destroy(&arena);
}
const char *unknown =
"{\"v\":2,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":1,\"lang\":1,"
"{\"v\":3,\"lang\":1,\"lsp\":[],\"reg\":[],\"py\":{\"v\":1,\"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\":[]}]}}";
@@ -22177,7 +22338,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\":2,\"lang\":0,\"py\":null,\"lsp\":[");
size_t used = (size_t)snprintf(json, capacity, "{\"v\":3,\"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\","
@@ -24830,6 +24991,8 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed_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);