Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2985,6 +2985,29 @@ static const cbm_gbuf_node_t *find_source_node(const cbm_gbuf_t *gbuf, const cha
/* Field type hint resolution for obj.Method() with multiple candidates.
* Strips C# field prefixes (_ / m_), capitalizes to get type name, and
* checks if TypeName.Method or ITypeName.Method exists among candidates. */
/* #1927: the hint may only bind a candidate whose OWNING dot-segment — the
* segment immediately before the method name — EQUALS the hinted type. The
* previous raw strstr() accepted the hinted name anywhere in the QN, so a
* single-letter receiver (`f.Close()` → hint "F") matched nearly every
* candidate ("F" ⊂ "…FileStore.Close", the method name included) and rebound
* a call whose resolution had already failed to an arbitrary project symbol
* at PP_FIELD_HINT_CONF. Segment equality keeps the intended
* variable-named-after-its-type case (`repo.Find()` → `Repo.Find`,
* lrp_go_s8_field_type_hint) and picks the candidate whose owner actually
* carries the name instead of whichever QN contains the letters. */
static bool fth_owner_segment_is(const char *candidate_qn, const char *type_name) {
const char *last_dot = strrchr(candidate_qn, '.');
if (!last_dot || last_dot == candidate_qn) {
return false;
}
const char *seg_start = last_dot;
while (seg_start > candidate_qn && seg_start[-1] != '.') {
seg_start--;
}
size_t seg_len = (size_t)(last_dot - seg_start);
return seg_len > 0 && strncmp(seg_start, type_name, seg_len) == 0 && type_name[seg_len] == '\0';
}

static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const char *callee_name,
int64_t source_id) {
if (!res->qualified_name || res->candidate_count <= SKIP_ONE) {
Expand Down Expand Up @@ -3024,7 +3047,8 @@ static void try_field_type_hint(resolve_ctx_t *rc, cbm_resolution_t *res, const
int cand_count = 0;
cbm_registry_find_by_name(rc->registry, method, &cands, &cand_count);
for (int ci = 0; ci < cand_count; ci++) {
if (strstr(cands[ci], type_name) || strstr(cands[ci], iface_name)) {
if (fth_owner_segment_is(cands[ci], type_name) ||
fth_owner_segment_is(cands[ci], iface_name)) {
const cbm_gbuf_node_t *better = cbm_gbuf_find_by_qn(rc->main_gbuf, cands[ci]);
if (better && better->id != source_id) {
res->qualified_name = cands[ci];
Expand Down
217 changes: 217 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -11266,6 +11266,221 @@ TEST(pipeline_go_bare_init_binds_nothing_parallel) {
PASS();
}

TEST(pipeline_go_parallel_field_hint_requires_owner_segment) {
/* #1927: try_field_type_hint (parallel resolver only) matched the hinted
* type name as a raw SUBSTRING of the candidate QN. A single-letter
* receiver — `f.Close()` on an *os.File — hints "F", and "F" is a
* substring of "…FileStore.Close", so a stdlib call was rebound to an
* arbitrary project method at PP_FIELD_HINT_CONF and presented as
* field_type_hint (the one weak strategy the Go selector guard trusts).
* The hint must fire only when the candidate's OWNING segment — the
* dot-segment immediately before the method — EQUALS the hinted type;
* otherwise the resolution stays a weak short-name match and the Go
* guard (#1906) drops it. >= 50 files forces pass_parallel.c, the only
* path that runs try_field_type_hint(). */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_fth_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}

write_temp_file(tmp, "go.mod", "module example.com/fxhint\n\ngo 1.22\n");
/* Two project methods named Close → the registry resolves f.Close() as an
* ambiguous short-name match (candidate_count 2), the shape the hint
* upgrades. FileStore's QN carries the capital F the buggy substring
* latches onto; Conn is the second candidate. */
write_temp_file(tmp, "filestore/filestore.go",
"package filestore\n"
"\n"
"type FileStore struct{ open bool }\n"
"\n"
"func (fs *FileStore) Close() {\n"
"\tfs.open = false\n"
"}\n");
write_temp_file(tmp, "conn/conn.go",
"package conn\n"
"\n"
"type Conn struct{ live bool }\n"
"\n"
"func (cn *Conn) Close() {\n"
"\tcn.live = false\n"
"}\n");
/* Two project methods named Find → same ambiguity, but here the receiver
* variable IS named after its type (`finder`), the case the hint exists
* for: the owning segment of Finder.Find equals the hinted "Finder". */
write_temp_file(tmp, "finder/finder.go",
"package finder\n"
"\n"
"type Finder struct{ n int }\n"
"\n"
"func NewFinder() *Finder { return &Finder{n: 1} }\n"
"\n"
"func (fi *Finder) Find(id int) int {\n"
"\treturn fi.n + id\n"
"}\n");
write_temp_file(tmp, "locator/locator.go",
"package locator\n"
"\n"
"type Locator struct{ n int }\n"
"\n"
"func (lo *Locator) Find(id int) int {\n"
"\treturn lo.n - id\n"
"}\n");
write_temp_file(tmp, "app/app.go",
"package app\n"
"\n"
"import (\n"
"\t\"os\"\n"
"\n"
"\txf \"example.com/fxhint/finder\"\n"
")\n"
"\n"
"func Lookup(path string) int64 {\n"
"\tf, err := os.Open(path)\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\tdefer f.Close()\n"
"\tst, err := f.Stat()\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\treturn st.Size()\n"
"}\n"
"\n"
"func UseFinder(id int) int {\n"
"\tfinder := xf.NewFinder()\n"
"\treturn finder.Find(id)\n"
"}\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "pad/filler%d.go", i);
snprintf(body, sizeof(body), "package pad\n\nfunc filler%d() int { return %d }\n", i, i);
write_temp_file(tmp, name, body);
}

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_fth.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);

/* (1) Reproduce-first: RED before the fix. The substring hint rebinds the
* stdlib f.Close() to FileStore.Close ("F" ⊂ QN) at high confidence and
* the Go guard keeps it. With segment equality the upgrade is refused,
* the match stays suffix_match, and the Go guard drops it. */
ASSERT_FALSE(cross_file_call_exists(s, project, "Lookup", "Close"));
/* (2) The intended hint case survives: `finder` names its type, so the
* owning segment of Finder.Find equals the hint. */
ASSERT_TRUE(cross_file_call_exists(s, project, "UseFinder", "Find"));

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

static int count_calls_with(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, const char *fragment);

TEST(pipeline_csharp_parallel_field_hint_requires_owner_segment) {
/* #1927, non-Go twin. Go no longer runs try_field_type_hint (e554a2ab),
* but every other language on the parallel path still does, and C# is the
* hint's intended consumer (it strips the _ / m_ field prefixes). C#
* method names are PascalCase, so a one-letter receiver hints a letter
* that every candidate QN already contains in the METHOD name: `w.Write()`
* hints "W", and a raw substring test accepts "...FileStore.Write" and
* "...Conn.Write" alike, whichever the registry lists first. The hint must
* bind only a candidate whose OWNING segment equals the hinted type.
* >= 50 files forces pass_parallel.c, the only path with the hint. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_cs_fth_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}

/* Two project methods named Write: an untyped receiver resolves as an
* ambiguous short-name match (candidate_count 2), the shape the hint
* upgrades. */
write_temp_file(tmp, "Storage/FileStore.cs",
"namespace Storage {\n"
" public class FileStore {\n"
" private int written;\n"
" public void Write(int v) { written += v; }\n"
" public void Put(int v) { written = v; }\n"
" }\n"
"}\n");
write_temp_file(tmp, "Net/Conn.cs",
"namespace Net {\n"
" public class Conn {\n"
" private int sent;\n"
" public void Write(int v) { sent += v; }\n"
" }\n"
"}\n");
/* Two project methods named Put, one owned by Store: the receiver `store`
* names its type, the case the hint exists for. FileStore's QN also
* contains "Store", so only segment equality picks Store.Put whatever
* the candidate order. */
write_temp_file(tmp, "Repo/Store.cs",
"namespace Repo {\n"
" public class Store {\n"
" private int held;\n"
" public void Put(int v) { held = v; }\n"
" }\n"
"}\n");
write_temp_file(tmp, "App/Worker.cs",
"namespace App {\n"
" public class Worker {\n"
" public void Log(System.IO.StreamWriter w) {\n"
" w.Write(1);\n"
" }\n"
" public void Keep() {\n"
" var store = Services.Resolve();\n"
" store.Put(2);\n"
" }\n"
" }\n"
"}\n");
for (int i = 0; i < 52; i++) {
char name[64];
char body[192];
snprintf(name, sizeof(name), "Pad/Filler%d.cs", i);
snprintf(body, sizeof(body),
"namespace Pad {\n public class Filler%d {\n"
" public int Get%d() { return %d; }\n }\n}\n",
i, i, i);
write_temp_file(tmp, name, body);
}

char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/cs_fth.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);

/* (1) Reproduce-first: RED before the fix. "W" is a substring of every
* Write candidate, so the old hint upgraded the StreamWriter call to an
* arbitrary project Write at PP_FIELD_HINT_CONF. No owner segment is "W",
* so the fixed hint must not fire at all. */
ASSERT_FALSE(cross_file_call_has_strategy(s, project, "Log", "Write", "field_type_hint"));
/* (2) The intended hint case binds the owner it names, never FileStore. */
ASSERT_EQ(count_calls_with(s, project, ".Keep", ".Store.Put", "field_type_hint"), 1);
ASSERT_EQ(count_calls_with(s, project, ".Keep", ".FileStore.Put", NULL), 0);

cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}

/* Fixture for the #1928 cross-language reference-guard probes (sequential and
* parallel twins). pad_files > 0 adds filler files to push the index over the
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
Expand Down Expand Up @@ -30015,6 +30230,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_parallel_field_hint_requires_owner_segment);
RUN_TEST(pipeline_csharp_parallel_field_hint_requires_owner_segment);
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);
Expand Down
Loading