Skip to content

fix(pipeline): bind field_type_hint by owning QN segment, not substring - #1936

Open
ilyabrykau-orca wants to merge 4 commits into
DeusData:mainfrom
ilyabrykau-orca:fix/go-field-type-hint-owner-segment
Open

fix(pipeline): bind field_type_hint by owning QN segment, not substring#1936
ilyabrykau-orca wants to merge 4 commits into
DeusData:mainfrom
ilyabrykau-orca:fix/go-field-type-hint-owner-segment

Conversation

@ilyabrykau-orca

@ilyabrykau-orca ilyabrykau-orca commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #1927.

What

try_field_type_hint() (parallel resolver) accepted any candidate whose qualified name merely contained the hinted type name as a substring. A single-letter receiver — f.Close(), hint "F" — matched almost every same-named candidate (the method name itself included) and rebound a call whose resolution had already failed to an arbitrary project symbol at confidence 0.85, presented as field_type_hint, the one weak strategy the Go selector guard (#1906) deliberately trusts.

This PR requires the candidate's owning dot-segment — the segment immediately before the method — to equal the hinted type (or its I-prefixed interface form): fth_owner_segment_is() replaces the two strstr() calls.

Measured effect

On the real ~1150-file Go+C repo from #1927, measured with this exact change on the #1906/#1909/#1910 stack (the same table is in the issue body):

strategy before after
field_type_hint 1484 356 (−76%)
single-character receivers 376 0
Go → C/C++ targets 34 0
every other strategy, incl. all lsp_* unchanged

Nothing relocated: the removed edges were fabricated on top of already-failed resolutions, so removing the substring anchor leaves nothing behind. 63 event.Get* edges moved to the interface that declares the method instead of an arbitrary implementation.

Tests

  • Reproduce-first: pipeline_go_parallel_field_hint_requires_owner_segment (parallel-path fixture, ≥50 files). RED before the fix on this branch: FAIL tests/test_pipeline.c:4941: ASSERT(!(cross_file_call_exists(s, project, "Lookup", "Close"))) — the stdlib f.Close() bound to FileStore.Close because "F" ⊂ the QN. GREEN after.
  • Negative probe (single-char receiver → zero project edges) and positive control (finder.Find()Finder.Find still hinted) in the same test.
  • lrp_go_s8_field_type_hint stays green — its repo/Repo fixture is segment-exact, the case the hint exists for.
  • Full scripts/test.sh venue leg (ASan+UBSan, all suites + contract steps): green. git clang-format --diff: clean.

Stacking

Stacked on #1907#1913#1915 (same pattern as those PRs): the test's absence assertions rely on #1907's Go selector guard downstream of the hint, and Go owner segments are receiver-qualified by #1913. The tip commit is the only new change; it applies to main independently.

#1932 tracks the family; #1927 is the direct consequence of #1907/#1913 moving weak-strategy mass into field_type_hint.

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>
A Go method's QN was the flat package form (proj.pkg.method) — the
receiver was ignored, so every same-name method in a package collided
on one QN and the graph upsert kept exactly one node. Measured on a
real Go repo: 20 Process/Name methods across 15 files kept 2 nodes;
9 different Task() methods fused into one chimera node carrying all
nine bodies' call edges; 19 structs pointed DEFINES_METHOD at a single
shared method node; a _test.go mock Close outranked the production
Close in the dedupe tie-break. The upsert's own comment calls
kind-disambiguated QNs 'the real cure'.

Qualify the QN with the receiver type (proj.pkg.Recv.method), the same
shape as Go interface members and the C++ out-of-line method path
right below it in extract_func_def:

- extract_defs.c: def.qualified_name = parent_class + name whenever
  the receiver type resolves; go_receiver_type_name becomes the shared
  cbm_go_receiver_type_name (exported via helpers.h) so both sides of
  the contract use one formula.
- extract_unified.c (compute_func_qn): mirror branch for
  method_declaration, so method-body calls keep exact source
  attribution instead of degrading to File-node fallback
  (calls_find_source).
- Consumers already agree: pxc_build_lsp_def passes the def QN and
  parent_class (receiver_type) verbatim into the Go LSP registries,
  and check_go_class_implements explicitly supports class-qualified
  method QNs (its path (b)).

Side effect: resolve_same_module's exact module.name hash no longer
matches concrete methods, which kills the conf-0.9 false edges where
an interface-typed call bound to an unrelated same-package method.

Fixes DeusData#1909

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
Go allows any number of init() functions per package — even several in
one file — and all of them run at start-up. On the flat QN
(proj.pkg.init) they all collided and the graph upsert kept ONE node
per package, silently dropping the rest: measured on a real Go repo, an
event-source package registering each decoder in its own file's init()
collapsed 21 init functions into one node (29 in source, 7 in the
graph), erasing the whole registration pattern.

Disambiguate with the DeusData#495 cfg-twin pattern: fold the file basename
and line into the QN (proj.pkg.init#a.go:L5). Calling init explicitly
is illegal in Go, so nothing ever joins on the plain QN; the
call-scope side (compute_func_qn) mirrors the exact formula so
init-body calls keep their source attribution instead of degrading to
the calls_find_source File fallback.

Reproduce-first: pipeline_go_multi_init_nodes_survive is RED without
the def-side change (node count 1, expected 2) and GREEN with it;
extract_go_multiple_init_disambiguated pins two same-file inits to
distinct suffixed QNs.

Fixes DeusData#1910

Signed-off-by: Ilya Brykau <ilya.brykau@orca.security>
@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

CI retrigger (no-op amend, identical tree — no rerun rights): the failing job was test-windows CLANG64 2/2 at test_subprocess.c:688 ASSERT(ready) in subprocess_windows_job_object_cancellation_quiesces_descendant_tree — a Windows Job-Object readiness probe with no overlap with this PR's diff (parallel resolver + pipeline test only). No semantic changes.

try_field_type_hint accepted any candidate whose qualified name merely
CONTAINED the hinted type name. A single-letter receiver (f.Close(),
hint "F") matched almost every same-named candidate - the method name
itself included - and rebound a call whose resolution had already
failed to an arbitrary project symbol at confidence 0.85, presented as
the one weak strategy the Go selector guard deliberately trusts.

Require the candidate's owning dot-segment - the segment immediately
before the method - to EQUAL the hinted type (or its I-prefixed
interface form). The intended variable-named-after-its-type case keeps
resolving (lrp_go_s8_field_type_hint stays green); the fabricated
bindings lose their only anchor and fall back to the weak short-name
strategies the guards already handle.

Measured on a real ~1150-file Go repo (with the DeusData#1906/DeusData#1909/DeusData#1910
fixes applied): field_type_hint 1484 -> 356 CALLS edges, the
single-character-receiver class 376 -> 0, Go->C/C++ targets 34 -> 0,
and no other strategy absorbed the removed edges. 63 edges moved to
the interface that declares the method instead of an arbitrary
implementation.

Fixes DeusData#1927

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

Copy link
Copy Markdown
Contributor Author

CI retrigger (no-op amend): test-windows-guards again, and this run's log shows the whole Windows harness down — SETUP FAIL: permanent daemon did not start for the crash check, index did not run, ASCII control did not index via CLI — before any guard executed. Tracked with a run matrix in #1952. No semantic changes.

@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-field-type-hint-owner-segment branch from 0f2024c to 6aac10c Compare August 30, 2026 20:26
@DeusData

Copy link
Copy Markdown
Owner

Heads-up: this went red-to-merge because #1937 (fcd8bfc1) and #1940 (61de19bb) landed, not because of anything you changed.

Checked with git merge-tree against current main: the conflict is exactly one file, tests/test_pipeline.c, and it is insertion-anchor drift — both merges added tests to it. Every source file in your diff auto-merges cleanly.

Re-anchor your new test and its RUN_TEST line and the rebase is done. Ping when it is up.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(pipeline): field_type_hint binds by receiver variable-name substring, not declared type — 1484 edges at conf 0.85 on one Go repo

2 participants