Skip to content

fix(pipeline): suppress weak short-name matches for Go selector calls - #1907

Open
ilyabrykau-orca wants to merge 1 commit into
DeusData:mainfrom
ilyabrykau-orca:fix/go-weak-selector-calls
Open

fix(pipeline): suppress weak short-name matches for Go selector calls#1907
ilyabrykau-orca wants to merge 1 commit into
DeusData:mainfrom
ilyabrykau-orca:fix/go-weak-selector-calls

Conversation

@ilyabrykau-orca

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #1906.

Extends the TS/JS receiver-aware weak-match guard (#592/#606) to Go: a selector call x.foo() whose receiver the Go LSP cannot type must not be bound by a receiver-blind short-name strategy to an arbitrary same-named project symbol (f.Close() on an *os.File → a project Close; on the repo measured in #1906 these strategies were 36% of all CALLS edges, and one 14-line stdlib-only helper had 3/3 false outbound edges).

Mirrors how TS/JS joined the Perl guard (#476):

  1. internal/cbm/extract_calls.c — flag Go call_expression with a selector_expression callee as is_method, same block as the TS/JS member_expression flag (Go has no this/super analog to exempt).
  2. src/pipeline/registry.c + pipeline.h — new cbm_go_suppress_weak_method_match(is_go, is_method, strategy, confidence). Drops suffix_match/fuzzy always; drops unique_name only when its confidence carries the import-unreachability penalty (< CONF_UNIQUE_NAME) — the stdlib-hijack shape. field_type_hint is deliberately kept: Go struct fields carry declared types, so the hint is receiver-aware there (lrp_go_s8_field_type_hint stays GREEN).
  3. src/pipeline/pass_calls.c / pass_parallel.c — feed the Go gate next to the TS/JS one. The drop defers to the emit path exactly like TS/JS, so service/route/HTTP/CONFIG edges stay main-identical. same_module, import_map, qualified_suffix, callee_suffix and all lsp_* strategies are untouched: typed receivers, same-package calls, and import-qualified calls resolve exactly as before.
  4. Tests (reproduce-first):
    • pipeline_go_receiver_suppresses_weak_method_edge — RED without the extractor flag (FAIL: ASSERT(!(cross_file_call_exists(s, project, "FileLen", "Close")))), GREEN with it. Asserts the stdlib f.Close() edge is gone while the typed same-package s.Close(), a bare local call, and an import-qualified cross-package call keep resolving. The fixture carries a go.mod — import reachability (the unique_name penalty) depends on it, like every real Go repo.
    • extract_go_selector_call_flags_is_method pins the extractor flag; the old flag-exempt contract test keeps its guard using Python as the exempt language.
    • go_suppress_drops_weak_selector_matches / go_suppress_keeps_typed_and_import_aware_matches unit-pin the drop-list, the 0.75-vs-0.375 unique_name split, and the lsp_* keeps.

Perf/memory: one ts_node_child_by_field_name + two strcmp per Go call site at extraction, one short-circuited predicate per resolved Go call, zero allocations, no new passes.

Known residual (documented in #1906, out of scope here): a Go file whose import table maps zero project packages has no penalty signal, so a lone same-named project symbol can still capture a stdlib method call there; same_module can still mis-bind interface-typed calls inside one package. Both need receiver typing, not name heuristics.

Checklist

  • Every commit is signed off (git commit -s) — required, CI rejects
    unsigned commits (DCO, see CONTRIBUTING.md)
  • Tests pass locally (scripts/test.sh — full leg, ASan+UBSan, "All tests passed")
  • Lint passes (git clang-format --diff HEAD clean on changed lines; clang-tidy/cppcheck via CI)
  • New behavior is covered by a test (reproduce-first for bug fixes)

@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (post-#1903). #1903 generalized the TS/JS guard into cbm_suppress_weak_member_match with per-site language gates — exactly the shape this PR anticipated — so the Go integration now composes with that helper at both call sites via its own predicate (cbm_go_suppress_weak_method_match; different drop-list: field_type_hint kept, unique_name only when import-unreachability-penalized). The flag-exempt extraction contract test moved from Go to Rust, since Python joined the flagged set in #1903 and Go joins here. Full scripts/test.sh leg green on the rebased stack.

@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Retriggered CI with a no-op amend (identical tree, new SHA — I lack rerun rights on this repo). The two failures on the previous run look environmental, not change-related: test_watcher.c:2526 index_call_count == 0, expected 1 on macos-15-intel (a watcher-timing probe; this PR touches no watcher code), and windows-guards dying in its own harness setup (SETUP FAIL: ASCII baseline did not index) before reaching any guard. No semantic changes; the local full venue leg is green.

@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-weak-selector-calls branch from 7f4f68c to f76971f Compare August 30, 2026 13:23
ilyabrykau-orca added a commit to ilyabrykau-orca/codebase-memory-mcp that referenced this pull request Aug 30, 2026
The READS/WRITES resolvers and the USAGE registry fallback hand bare
reference text to the short-name registry, which contains Field nodes -
so once Go struct fields exist (DeusData#1935), every Go local err := ... binds
whichever struct field is named err, project-wide: 21308 USAGE and 5191
WRITES onto Go fields on the measured repo, top target a test struct's
field T collecting 3013 edges. In Go that binding is impossible by
construction: a field is only reachable through a selector expression
(x.f), and selector references resolve on the LSP path - every
Field-targeted reference edge in the census carried dot-less text.

Add cbm_go_suppress_bare_field_ref() next to the DeusData#1928 predicate and
consult it at the same four sites (both READS/WRITES resolvers, both
USAGE registry fallbacks): drop the bind when the file is Go, the
target label is Field, and the reference text has no '.'. Go-gated
because C#/Java/C++/Python method bodies legitimately reference their
own members bare (cp_reads_writes_cs_static_field pins that shape).

Field-validated on the DeusData#1940 stack: USAGE onto Go fields 21308 -> 0,
WRITES 5191 -> 0; the only remaining field-targeted edges are 2466
CALLS, which are DeusData#1906/DeusData#1907's selector-guard territory. Reproduce-
first pipeline probes (sequential + parallel twins) were RED on the
stack without this commit.

Fixes DeusData#1942

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
ilyabrykau-orca added a commit to ilyabrykau-orca/codebase-memory-mcp that referenced this pull request Aug 30, 2026
The READS/WRITES resolvers and the USAGE registry fallback hand bare
reference text to the short-name registry, which contains Field nodes -
so once Go struct fields exist (DeusData#1935), every Go local err := ... binds
whichever struct field is named err, project-wide: 21308 USAGE and 5191
WRITES onto Go fields on the measured repo, top target a test struct's
field T collecting 3013 edges. In Go that binding is impossible by
construction: a field is only reachable through a selector expression
(x.f), and selector references resolve on the LSP path - every
Field-targeted reference edge in the census carried dot-less text.

Add cbm_go_suppress_bare_field_ref() next to the DeusData#1928 predicate and
consult it at the same four sites (both READS/WRITES resolvers, both
USAGE registry fallbacks): drop the bind when the file is Go, the
target label is Field, and the reference text has no '.'. Go-gated
because C#/Java/C++/Python method bodies legitimately reference their
own members bare (cp_reads_writes_cs_static_field pins that shape).

Field-validated on the DeusData#1940 stack: USAGE onto Go fields 21308 -> 0,
WRITES 5191 -> 0; the only remaining field-targeted edges are 2466
CALLS, which are DeusData#1906/DeusData#1907's selector-guard territory. Reproduce-
first pipeline probes (sequential + parallel twins) were RED on the
stack without this commit.

Fixes DeusData#1942

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
ilyabrykau-orca added a commit to ilyabrykau-orca/codebase-memory-mcp that referenced this pull request Aug 31, 2026
The READS/WRITES resolvers and the USAGE registry fallback hand bare
reference text to the short-name registry, which contains Field nodes -
so once Go struct fields exist (DeusData#1935), every Go local err := ... binds
whichever struct field is named err, project-wide: 21308 USAGE and 5191
WRITES onto Go fields on the measured repo, top target a test struct's
field T collecting 3013 edges. In Go that binding is impossible by
construction: a field is only reachable through a selector expression
(x.f), and selector references resolve on the LSP path - every
Field-targeted reference edge in the census carried dot-less text.

Add cbm_go_suppress_bare_field_ref() next to the DeusData#1928 predicate and
consult it at the same four sites (both READS/WRITES resolvers, both
USAGE registry fallbacks): drop the bind when the file is Go, the
target label is Field, and the reference text has no '.'. Go-gated
because C#/Java/C++/Python method bodies legitimately reference their
own members bare (cp_reads_writes_cs_static_field pins that shape).

Field-validated on the DeusData#1940 stack: USAGE onto Go fields 21308 -> 0,
WRITES 5191 -> 0; the only remaining field-targeted edges are 2466
CALLS, which are DeusData#1906/DeusData#1907's selector-guard territory. Reproduce-
first pipeline probes (sequential + parallel twins) were RED on the
stack without this commit.

Fixes DeusData#1942

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
ilyabrykau-orca added a commit to ilyabrykau-orca/codebase-memory-mcp that referenced this pull request Aug 31, 2026
The READS/WRITES resolvers and the USAGE registry fallback hand bare
reference text to the short-name registry, which contains Field nodes -
so once Go struct fields exist (DeusData#1935), every Go local err := ... binds
whichever struct field is named err, project-wide: 21308 USAGE and 5191
WRITES onto Go fields on the measured repo, top target a test struct's
field T collecting 3013 edges. In Go that binding is impossible by
construction: a field is only reachable through a selector expression
(x.f), and selector references resolve on the LSP path - every
Field-targeted reference edge in the census carried dot-less text.

Add cbm_go_suppress_bare_field_ref() next to the DeusData#1928 predicate and
consult it at the same four sites (both READS/WRITES resolvers, both
USAGE registry fallbacks): drop the bind when the file is Go, the
target label is Field, and the reference text has no '.'. Go-gated
because C#/Java/C++/Python method bodies legitimately reference their
own members bare (cp_reads_writes_cs_static_field pins that shape).

Field-validated on the DeusData#1940 stack: USAGE onto Go fields 21308 -> 0,
WRITES 5191 -> 0; the only remaining field-targeted edges are 2466
CALLS, which are DeusData#1906/DeusData#1907's selector-guard territory. Reproduce-
first pipeline probes (sequential + parallel twins) were RED on the
stack without this commit.

Fixes DeusData#1942

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed. This is the right shape, and the care in it is above the bar for this class of change. One interaction with your own #1913 needs measuring before either lands.

The guard itself

cbm_go_suppress_weak_method_match is exactly the form this repo wants: a pure per-language predicate gated on the file language, wired at both pass_calls.c and pass_parallel.c with the lockstep note, and — the part that matters most — it leaves same_module, import_map, qualified_suffix, callee_suffix and every lsp_* strategy alone. High-confidence paths are untouched.

Two decisions in it are better than the guard you mirrored:

Keeping field_type_hint for Go. The TS/JS guard drops it as a heuristic; you kept it with a reason — a Go struct field carries a declared type, so the hint is receiver-aware there rather than a guess. That is the right distinction and lrp_go_s8_field_type_hint holds you to it.

Dropping unique_name only when penalized. This is the subtle one, and the comment earns it: an unpenalized unique_name target sits inside the caller's import closure, so dropping it would kill genuinely-typed lone-candidate calls that never reach the field-type-hint upgrade. Gating on confidence < CONF_UNIQUE_NAME rather than on the strategy name alone is what makes this a receiver-blindness guard instead of a blunt recall cut.

Documenting the residual — a file whose import table maps zero project packages has no penalty signal — instead of implying the class is closed, is the right way to leave it.

The measurement I want, and it is about this PR plus #1913

These two are in the same stack and both narrow Go call resolution, and I do not think either PR's numbers capture what they do together.

Walk one call through both:

  1. Today, a Go method call can resolve via same_module at confidence 0.90.
  2. fix(extract): receiver-qualify Go method QNs #1913 receiver-qualifies method QNs, so resolve_same_module's module_qn + "." + callee_name candidate no longer matches — that call falls through to resolve_name_lookup.
  3. resolve_name_lookup may return suffix_match.
  4. This PR drops suffix_match outright for Go selector calls.

So a call that was a confident edge before the stack becomes no edge at all after it — and neither PR sees that on its own. #1913 measures the false edges it removes; this one measures the weak edges it removes. Neither measures the calls that arrive at step 4 only because of step 2.

I have asked #1913 for a Go CALLS census before/after with a strategy histogram. What I want here is the same measurement taken on the stack as a whole, not slice by slice: Go edge count and strategy histogram on main, then with #1907 alone, then with #1907+#1913. If the compounding loss is small, that settles it and both merge on the numbers. If a meaningful population lands in that gap, it is much cheaper to know now than after a reindex.

The per-language census with its dumped per-edge sets is the instrument, and the Go leg is the baseline.

None of this is a doubt about the direction. f.Close() on an *os.File binding to a project Close is plainly wrong, 36% of CALLS edges arriving through receiver-blind strategies is plainly too many, and a 14-line stdlib-only helper with 3/3 false outbound edges is a good demonstration. I want the stack's net effect on the record, not just each slice's gross.

Status

ms is DIRTY — this and both stack members above it (#1913, #1915) have conflicts. main was briefly broken today and was repaired by #1993, so this is a good moment to rebase the whole stack.

Cleared. Approved on shape, pending the stack-level numbers.

@DeusData DeusData added bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker. labels Sep 1, 2026
A Go selector call x.foo() whose receiver the Go LSP cannot type falls
through to the generic registry resolver, which binds it by bare short
name to an arbitrary same-named project symbol. Stdlib calls are the
worst case: f.Close() on an *os.File gets a CALLS edge to whatever
project Close wins candidate ranking (measured on a real Go repo:
confidence 0.11, 15 candidates; suffix_match + unique_name were 36% of
all CALLS edges, and one 14-line stdlib-only function got 3 out of 3
false outbound edges).

Extend the TS/JS receiver-aware guard (DeusData#592/DeusData#606) to Go:

- extract_calls.c: flag Go call_expression with a selector_expression
  callee as is_method, mirroring the TS/JS member_expression flag.
- registry.c: add cbm_go_suppress_weak_method_match. Unlike the TS/JS
  drop-list, field_type_hint is KEPT (Go struct fields carry declared
  types, so the hint is receiver-aware — lrp_go_s8_field_type_hint),
  and unique_name is dropped only when its confidence carries the
  import-unreachability penalty (the stdlib-hijack shape); an
  unpenalized lone candidate inside the caller's import closure never
  enters the field-type-hint upgrade and must survive.
- pass_calls.c / pass_parallel.c: feed the Go gate next to the TS/JS
  one; the drop still defers to the emit path so service/route/HTTP
  edges stay main-identical.

Reproduce-first: pipeline_go_receiver_suppresses_weak_method_edge is
RED without the extractor flag (the f.Close -> project Close edge
exists) and GREEN with it; typed same-package calls, bare local calls
and import-qualified cross-package calls still resolve. The old
extraction contract test used Go as the flag-exempt language — Python
takes that role, and extract_go_selector_call_flags_is_method pins the
new behavior.

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-weak-selector-calls branch from f76971f to 651ae3c Compare September 2, 2026 07:48
@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Only conflict was test insertion-anchor drift in tests/test_pipeline.c (both merges added tests at the same anchor); every source file auto-merged. Full scripts/test.sh (ASan+UBSan) is green on the rebased stack head (#1936, which contains this commit).

@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Stack-level census is up — full table and per-edge migration matrix posted on #1913 (they share one instrument run, so the numbers live in one place). The short version for the interaction you asked about:

The step-2→step-4 cascade is small. Across the #1913 leg, suffix_match grows by only 25 keys (155 → 180) — the calls that fall out of same_module under receiver-qualified QNs do not arrive at the suffix fallback in any volume, so this PR's drop-list removes almost nothing that #1913 newly routed there. Of #1907's 929 same_module edges: 108 stay, 444 re-land (mostly field_type_hint — receiver-typed, and #1936 later re-judges those by owning segment), 377 vanish — and the vanished population is the untyped-receiver binds #1913 exists to stop trusting.

The compounding loss the numbers did surface was elsewhere: a third QN site (go_lsp's enclosing-function QN) that #1913 had not moved, which cost method-body callers their LSP caller join (568 lsp_type_dispatch + 105 lsp_direct keys in the first measurement). Fixed inside #1913 with a reproduce-first test; with the fix the stack's LSP strategies end above the pre-stack baseline (lsp_direct 8896 → 9124, lsp_type_dispatch 2551 → 2640 at the stack head).

Whole-stack net: 21018 → 13815 Go CALLS, of which −6403 is this PR's weak-edge removal and −1019 is #1936's substring-hint removal; receiver-blind strategy share drops 43% → 10.5%. Stack rebased and force-pushed, full scripts/test.sh green on the head.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working parsing/quality Graph extraction bugs, false positives, missing edges priority/high Needs near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Go: weak short-name strategies fabricate CALLS edges for selector calls (stdlib receivers hijack project symbols)

2 participants