Skip to content

fix(extract): receiver-qualify Go method QNs - #1913

Open
ilyabrykau-orca wants to merge 22 commits into
DeusData:mainfrom
ilyabrykau-orca:fix/go-method-receiver-qn
Open

ilyabrykau-orca wants to merge 22 commits into
DeusData:mainfrom
ilyabrykau-orca:fix/go-method-receiver-qn

Conversation

@ilyabrykau-orca

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #1909. Stacked on #1907 (shares the Go test scaffolding in tests/test_extraction.c; the first commit here is #1907's — review only the last commit).

Go concrete methods carried a flat package QN (proj.pkg.method, receiver ignored), so every same-name method in a package collided on one QN and the graph upsert kept exactly one node — on the repo measured in #1909: 20 Process/Name methods → 2 nodes, 9 Task() methods fused into one chimera node carrying all nine bodies' call edges, 19 structs DEFINES_METHOD-linked to a single shared node, and a _test.go mock outranking the production method in the dedupe tie-break. The upsert's own comment calls kind-disambiguated QNs "the real cure".

The change — same shape as Go interface members and the C++ out-of-line method path directly below it in extract_func_def:

  1. internal/cbm/extract_defs.c — when the receiver type resolves, def.qualified_name = parent_class + "." + name (proj.pkg.Storage.Close); go_receiver_type_name becomes the shared cbm_go_receiver_type_name (declared in helpers.h, following the cbm_cpp_out_of_line_parent_class precedent) so both sides of the contract use one formula.
  2. internal/cbm/extract_unified.c (compute_func_qn) — mirror branch for Go method_declaration, so method-body calls keep exact source attribution instead of degrading to the calls_find_source File-node fallback.
  3. No resolver changes needed — consumers already agree:
    • pxc_build_lsp_def passes the def QN and parent_class (→ receiver_type) verbatim into the Go LSP registries, so lsp_type_dispatch / lsp_embed_dispatch / interface-satisfaction emissions follow the new QN automatically (all lrp_go_s* probes stay GREEN, untouched).
    • check_go_class_implements explicitly supports class-qualified method QNs (its path (b) reconstructs <ClassQN>.<method>; its comment describes the flat QN as the anomaly).

Side effect (also #1909): resolve_same_module's exact module.name hash no longer matches concrete methods, which removes the confidence-0.9 false edges where an interface-typed call (pipeline.Process(event)) bound to an unrelated same-package method.

Tests:

  • extract_go_method_receiver_qualified_qn — two same-name methods on different receivers get distinct, receiver-qualified QNs with matching parent_class; free functions keep the flat QN.
  • extract_go_no_filename_in_module_qn updated: the method expectation becomes proj.myapp.db.Conn.Query (its actual contract — no filename segment in the QN — still asserted).
  • Full scripts/test.sh leg green (ASan+UBSan, "All tests passed"), including all lsp_resolution_probe Go scenarios and the fix(pipeline): suppress weak short-name matches for Go selector calls #1907 pipeline test, with zero probe changes.

Note for maintainers: this changes extracted QNs for Go methods, so existing graphs need a reindex to pick up the new shape (flagged in #1909 per CONTRIBUTING's indexing-change rule).

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 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 (stacked on the rebased #1907). No semantic changes; full test leg green. Post-fix field numbers for this change are in the #1906 census comment (method-node recovery incl. generic receivers: #1906 (comment)).

@DeusData

DeusData commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Reviewed the last commit. The bug is real and badly worth fixing, the shape of the fix is right, and there is one measurement I want before it lands.

The problem is as bad as you say. store.c:257 declares UNIQUE(project, qualified_name) and the node upsert is ON CONFLICT(project, qualified_name) DO UPDATE SET, so a flat proj.pkg.method genuinely makes every same-name method in a package one row. Nine Task() methods fusing into a single node carrying all nine bodies' call edges is not a cosmetic count problem — that node is a chimera whose outgoing edges are attributed to code that never made them. And a _test.go mock outranking the production method in the dedupe tie-break is the kind of thing that quietly poisons every answer downstream.

Receiver-qualifying is the right cure, matching the Go interface-member and C++ out-of-line paths already in extract_func_def, and lifting go_receiver_type_name to a shared cbm_go_receiver_type_name so both sides of the contract compute one formula is exactly right — two copies of that formula would drift.

The measurement I want

You describe this as a side effect, in one line:

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

I think that line is doing more work than it looks. resolve_same_module (registry.c:790) builds its candidate as module_qn + "." + callee_name. Before this change a call to Close inside proj.pkg hit proj.pkg.Close at confidence 0.90. After it, the method is proj.pkg.Storage.Close and that candidate misses — and the suffix retry at :795 builds the same shape, so it misses too.

So same_module stops resolving every Go concrete-method call, not only the wrong ones. The true ones do not disappear; they fall through to resolve_name_lookup, which picks a winner by import distance project-wide at a much lower confidence. That is the failure mode this repo has been bitten by before: a blocked high-confidence match can lose a correct edge and mint a wrong one in the same step, and the net can be negative even when every individual removal was justified.

You measured the false edges removed. I would like the other half measured too, and there is already an instrument for it — the per-language CALLS census with its dumped per-edge sets. The Go leg of that is the right baseline. Concretely, what I am asking for is:

  • Go CALLS edge count before and after, and
  • the strategy histogram — specifically how many edges moved out of same_module and where they landed.

If the answer is "the LSP type-dispatch path already covers them, so the demotions are few and the removals are many", that settles it and the PR merges on the strength of the numbers rather than on the argument. If a meaningful number of true edges demote to import-distance guessing, that is worth knowing before this is in main rather than after.

The lrp_go_s* probes staying green is real evidence and I am not dismissing it — but they are a fixed scenario set, and the population this affects is every Go method call in a repo without LSP coverage on the receiver.

Two smaller things

This shifts a baseline other PRs are being judged against. There is an open cluster of call-resolution PRs that we have decided to judge against one shared census precisely because whichever lands first makes the rest unreadable on their own numbers. A change to Go method QNs moves that baseline for the Go leg. Not an objection — just something I would rather sequence deliberately than discover afterwards.

Your reindex note is right and needs to travel further than the PR. A QN-shape change means existing graphs pick up the new shape only on rebuild, and that belongs in the release notes, not only in #1909. I will make sure it gets there.

Status

ms is DIRTY — this and both its stack neighbours (#1907, #1915) have conflicts. main was broken for a few hours today and was repaired an hour ago by #1993, so now is a good moment to rebase the stack.

Thanks for the measurements you did bring — "20 methods → 2 nodes, 19 structs linked to one shared node" is what turns this from a plausible tidy-up into an obvious defect.

@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
@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-method-receiver-qn branch from 12d50c0 to a6b1a69 Compare September 2, 2026 07:48
@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Rebased onto current main as part of the 1907→1913→1915→1936 stack; no source conflicts, test anchors re-anchored. Full scripts/test.sh green on the stack head.

@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-method-receiver-qn branch from a6b1a69 to dbba757 Compare September 2, 2026 10:01
@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Here is the measurement — and it caught something real, so thank you for insisting on it.

The census (orca-runtime-sensor, Go→Go CALLS, isolated one-shot indexes)

main +#1907 +#1907+#1913 full stack (+#1915+#1936)
total 21018 14615 14812 13815
lsp_direct 8896 8896 9130 9124
lsp_type_dispatch 2551 2551 2640 2640
lsp_interface_dispatch 274 274 287 287
same_module 931 931 117 145
field_type_hint 676 676 1205 186
unique_name 2944 940 936 936
suffix_match 4554 155 180 180

Per-edge migration of #1907's 929 same_module keys (caller + callee-name identity, so chimera retargeting doesn't count as loss): 108 kept same_module (receiver still resolvable flat), 444 re-landed (308 field_type_hint, 97 lsp_strategy_cross_file, 39 unique_name), 377 lost entirely. The lost population is exactly the calls whose receiver cannot be typed — the ones same_module was binding at 0.90 by package-coincidence, which is this PR's thesis. The LSP strategies keep ≥96% of their edges and end above the pre-#1913 baseline. Net caller→callee pairs across the leg: +41.

What the first run of this census caught

My first measurement showed lsp_type_dispatch losing 568 edges and lsp_direct 105 — every sample was a method-body caller ((r *Repo) Install calling r.helper()). Root cause: go_lsp.c's process_function still set the enclosing-function QN to the bare package.func form; its own comment declared the contract ("must be the QN the textual call events and the defs pass produce"), and this PR had moved that shared form to package.Type.method on the other two sides but not this one. Every LSP resolution sourced from inside a method lost its caller join in cbm_pipeline_find_lsp_resolution and fell back — or died.

Amended into this PR (it is the same one-formula claim): process_function now receiver-qualifies via the shared cbm_go_receiver_type_name, with a reproduce-first pipeline test (pipeline_go_method_caller_keeps_lsp_join: same-receiver method call with a colliding decoy method in another package, RED without the go_lsp hunk, GREEN with it). The table above is measured with the fix; without it the leg read 14156 total with lsp_direct at 8263.

One upstream test adopted the new shape: parallel_go_cross_package_field_chain_resolves matched edges by the flat handler.PlaceOrder fragment; the edge it guards is intact — OrderHandler.PlaceOrder → OrderService.PlaceOrder, still lsp_type_dispatch at 0.95, now receiver-qualified on both endpoints (which also confirms the cross-package chain path under the join fix) — so its fragments moved to the qualified form.

On the baseline-shift point: agreed — these numbers are from the stack in its merge order (#1907 → #1913 → #1915 → #1936), so whichever leg lands first, the others' columns above stay the readable reference.

Full scripts/test.sh green on the amended stack head; branch force-pushed with the rebase.

@DeusData

DeusData commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Census received, and thank you for insisting on measuring rather than arguing it — it caught exactly the thing I was worried about.

Where this stands: the result changes the shape of Go method qualified names (pkg.Type.method where main has pkg.method) and drops 377 same_module edges in exchange for the LSP-side gains. That is a graph-contract decision, not a review nit, and the standing rule here is that call-resolution changes are judged per language against one census rather than one PR at a time. So this is now a direction call on our side, with your census as the primary input — I will bring the answer back to this thread.

Please hold any further rebase of the stack (#1907 → #1913 → #1915 → #1936) until that call is made. main has moved again since your last one and the stack conflicts, but rebasing now would cost you a pass that may need redoing depending on the decision. Nothing else is needed from you meanwhile.

@DeusData

DeusData commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Thank you for holding the rebase while we measured this ourselves — here is the result, and the decision.

We ran our own census on kubernetes/kubernetes @ 8c078bc3 with four builds (the merge-base, #1907 alone, #1907+#1913, the whole stack), diffed the full Go CALLS edge sets, and hand-classified 150 sampled edges against the source.

build Go CALLS what changed
base 318,635
#1907 alone 237,347 (−25.5%) purely subtractive; all suffix_match/unique_name; 80% of sampled removed edges were false (os.ReadFile → a struct field, fmt.Errorf → a test fake, time.Sleep → *SleepAction)
+ #1913 254,821 +32,007 Method nodes the base had fused into one per name (we found base edges whose recorded call lines lie past end-of-file — the fusion bug in the flesh). On the lost side 18/25 sampled drops were false and 3 were actively corrected.
whole stack 235,519 #1936's last commit halves field_type_hint, lifting gained-edge precision from 48% to 64%

Decision: the direction is accepted. Please rebase #1907 first, then #1913 onto main; #1915 and #1936 follow once those two are in. We'll take each through the normal gates as it goes green.

Two things from the measurement, so you have the full picture:

  1. We could not reproduce the 377. Our same_module true-loss for fix(extract): receiver-qualify Go method QNs #1913 vs base is 8,584 (net −7,207 after 1,377 gained), and no counting definition we tried — same-file only, same-package-dir only, bare callees, distinct targets — lands anywhere near 377. Could you say which corpus and which exact query produced it? It would tell us whether one of the two instruments is off.
  2. Five defect shapes surfaced in the fix(extract): receiver-qualify Go method QNs #1913 sample that are not visible at the 377 level. Two are precision regressions (a cross-package StatusStrategy.ValidateUpdate suffix collision; metav1.Object.SetNamespace rebinding to meta.MetadataAccessor.SetNamespace with the wrong arity — seen twice) and three are recall losses (type-alias receivers such as type FeatureSet = bitmap; an interface-typed var holding a local fake; build-tag duplicate methods defeating unique_name). None of these block your PRs — we are taking them ourselves as follow-ups on top of fix(extract): receiver-qualify Go method QNs #1913, with the census as the test fixture source, and will credit this work in them.

The full dumps stay on our side if you want any of the sampled edges with file:line.

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>
@ilyabrykau-orca
ilyabrykau-orca force-pushed the fix/go-method-receiver-qn branch from dbba757 to da76194 Compare September 15, 2026 20:13
@ilyabrykau-orca

Copy link
Copy Markdown
Contributor Author

Rebased onto current main behind #1907 (stack order per your decision). Conflicts were test-anchor drift only: the extraction fixture now sits after main's new Python local-binding tests, and the parallel field-chain assertion keeps the receiver-qualified fragments (OrderHandler.PlaceOrder → .service.OrderService.PlaceOrder). Full scripts/test.sh green locally (ASan+UBSan), git clang-format --diff clean. The 377-instrument answer is two comments up.

@DeusData

Copy link
Copy Markdown
Owner

Heads-up: I cancelled this PR's CI run on purpose a minute ago. It was sharing the hosted-runner pool with tonight's v0.11.0 release build, and the release needed the slots. Nothing about the PR is wrong. I will re-run the full CI as soon as the release is out, so any red you see right now is the cancellation, not your change. Thanks for your patience, and for the steady stream of Go fixes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…hod-receiver-qn

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

@DeusData DeusData left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, @ilyabrykau-orca. We've merged the updated #1907 head, which carries current main, into this branch from our side, keeping both sets of tests in tests/test_pipeline.c. Please pull before your next push.

The QN shape matches the 15 September direction. Methods become pkg.Recv.method, generic receivers are unwrapped, and free functions stay flat. On kubernetes/apiserver, Go Method nodes go from 3,248 to 4,700 and now equal the DEFINES_METHOD count: the fused-method bug is gone. Every production hunk is bound by a test (we neutered each one: the extract_defs, extract_unified and go_lsp hunks each turn tests red). The merge result passes 962 tests plus the 3,199 in the other Go-fixture suites, and lint-ci is clean.

Two asks:

  1. Please bump CBM_INDEX_FORMAT_VERSION to 2 (src/store/store.h, with a line like 2 = Go method QNs are receiver-qualified (#1913)). The comment in store.h asks for a bump whenever the QN scheme of an already-written graph changes, and this is that case. We reproduced the consequence: an index built on main and then refreshed incrementally with this branch ends up mixed. It has 768 receiver-qualified and 2,490 flat Go method nodes, and the old fused nodes keep their stale edges. With the bump, an old index goes through the full-reindex path once. tests/test_index_format.c uses the constant symbolically, so no test changes should be needed. We'll cover the one-time rebuild in the release notes.
  2. Tighten pipeline_go_method_caller_keeps_lsp_join. It asserts Install → helper by name only, but the fixture deliberately contains other.Decoy.helper, so a wrong binding to the decoy would still pass. Asserting the target file (svc/helper.go) or QN (…svc.Repo.helper) would pin it, ideally with an lsp_* strategy.

Nits, optional:

  • src/pipeline/pass_semantic.c around line 264 still describes Go methods as carrying a flat QN.
  • go_lsp.c builds the QN by concatenation, while the other two sites use cbm_fqn_compute_source_lang. They agree today, and the tests hold them together.

On the question we raised on 1 September, how the two changes interact: on the same corpus, #1907 plus #1913 lose about 74 true same-package edges (about 0.45% of Go CALLS). With same_module missing, those fall to unique_name, which penalises a same-package callee as not import-reachable, and #1907's guard then drops them. We think the right fix is small: treat the caller's own package as import-reachable. That can be a follow-up on top of this stack and doesn't block it.

Thank you. Once the bump and the assertion are in, this is ready.

Brings the branch up to date with main (989 commits since the
2026-09-28 merge). Four conflicts, all additive on both sides:

- pipeline.h / registry.c: main added cbm_suppress_weak_import_bound_call
  (DeusData#2127) at the same anchor as cbm_go_suppress_weak_method_match; both
  kept.
- pass_calls.c / pass_parallel.c: drop_plain_call now chains the Go
  selector guard and main's Python import-binding guard, in the same
  order in both resolvers.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Review follow-up for DeusData#1907 (asks from the 2026-09-28 review):

- pipeline_go_receiver_parallel_suppresses_weak_method_edge: parallel
  twin of pipeline_go_receiver_suppresses_weak_method_edge. The guard is
  wired into pass_parallel.c as well as pass_calls.c, but the four-file
  fixture only runs the sequential resolver, so the parallel hunk was
  bound by no test. The fixture is shared (write_go_receiver_fixture) and
  padded with 52 Go files past MIN_FILES_FOR_PARALLEL, with
  CBM_WORKERS=4, in the shape of the Python parallel twin.
- registry.c: cbm_go_suppress_weak_method_match builds on the shared
  weak_short_name_strategy list and narrows it in two places only
  (field_type_hint kept, unique_name only when penalized). Behaviour is
  unchanged; weak_call_guards_share_one_drop_list now binds the Go guard
  to the shared list too.
- test_extraction.c: the flag-exempt comment names the current flag set
  (Perl, Python, TS/JS/TSX and Go) and Rust as the exempt language.

Refs DeusData#1906

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
…hod-receiver-qn

Brings the branch up to date with the updated DeusData#1907 head, which carries
main 9f85f10 and the DeusData#1907 review follow-up. One conflict in
tests/test_pipeline.c, at the shared anchor: keep this branch's
pipeline_go_method_caller_keeps_lsp_join and the shared
write_go_receiver_fixture plus its sequential and parallel probes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Review follow-up for DeusData#1913 (asks from the 2026-09-28 review), plus one
finding from the review of the updated head:

- store.h: CBM_INDEX_FORMAT_VERSION 3 -> 4. Every Go method QN changes
  (pkg.method -> pkg.Recv.method), so an index built before this change
  and refreshed incrementally would mix flat and receiver-qualified
  method nodes, with the old fused nodes keeping their stale edges. The
  bump routes such an index through one full rebuild.
- pipeline_go_method_caller_keeps_lsp_join: the by-name assertion would
  also pass for a binding onto the decoy other.Decoy.helper. The test now
  pins the edge by QN suffix on both ends (.svc.Repo.Install ->
  .svc.Repo.helper) with an lsp_* strategy, and asserts no edge to the
  decoy.
- extract_defs.c: the receiver-qualified def QN is Go-only. The templ
  grammar is Go-derived and reaches the same receiver branch, but its
  call-scope QN (compute_func_qn) stays flat, so a templ method would get
  a def QN no call names and its calls would degrade to the File node.
  extract_templ_method_qn_matches_call_scope pins def QN == call scope.
- pass_semantic.c: the check_go_class_implements comment no longer
  describes Go method QNs as flat.

Refs DeusData#1909

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_fqn_compute_source_lang returns NULL only when the module QN cannot be
built (arena exhaustion). The receiver-qualified branches then passed that
NULL to "%s". Both sides now fall back to the flat QN together, so the def
and its calls still agree. Allocation-failure path only; no change on any
normal run. (Raised in our security review of the updated head.)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The parallel resolver re-picks a multi-candidate registry match through
try_field_type_hint. That helper capitalizes the receiver VARIABLE name
and accepts any candidate whose QN contains it as a substring: `t.Run`
on a *testing.T became `T` and bound a project `TBench.Run`. The
sequential resolver has no such step. The Go selector guard treated
field_type_hint as receiver-aware and kept it, so the same weak pick it
drops in sequential mode survived in parallel mode under a new name.

On caddy + prometheus, parallel and sequential CALLS diverged by 1,684
(this branch) and 5,560 (with receiver-qualified method QNs, which put
type names into more QNs). At base the gap is 84. SCIP precision of Go
field_type_hint is 0.016 at base.

Go no longer takes the hint. The guard keeps field_type_hint on the
shared weak list, and the fixture of both twins now pins the
`t.Run` case. A typed field call (lrp_go_s8, `s.repo.Find()`) resolves
through the Go LSP and is unaffected.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DeusData#1913 makes Go method nodes receiver-qualified (<dir>.<Recv>.<Method>).
The Go doc-link model still let every method claim the flat node
<dir>.<name>:
- doclink_go.c go_scan_method pushed a claim record;
- doc_links_go.c go_add_claim put it in the flat table.

So a type, function or value that shares its name with a method in its
directory became a graph_gap ("lost its node to the method"), although
the graph now gives it that node. Base reports the same graph_gap for
this shape, because there the method really did take the node. So this
completes DeusData#1913's QN scheme for one of its consumers; it is not a
regression DeusData#1913 causes.

- A method record now carries w `-`: it claims no flat name.
- The resolver no longer enters methods into the flat table. Methods are
  found through their receiver (the member table), which was already the
  first lookup.
- The flat fallback in go_method_node is now unreachable. It stays, with
  a comment saying so.

Corpus, before = this branch's tree, par x2, r1 == r2:
- terraform: graph_gap rows 9 -> 0, doc-link MENTIONS 528 -> 536 (+8,
  -0). All 8 new edges were judged correct: e.g. [AbsProviderConfig]
  now lands on the type, not on the method
  RootProviderConfig.AbsProviderConfig.
- vault, prometheus: unchanged.
- prometheus incremental == full for MENTIONS.
- The 3 -> 4 index-format bump already forces a full rebuild, so no
  stored scope blob mixes the two meanings.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
)

main's Go doc-link tests were written against flat Go method QNs, where
`(A) Close` and `(B) Close` shared one node `alpha.Close`. That is the
fused-node defect DeusData#1913 removes. The expectations move to the new scheme,
and each changed assertion pins an edge onto, or a count of, the
receiver-qualified node (production: the previous commit and DeusData#1913):

- sources: RefM's source is p.pkg.S.M, not p.pkg.M.
- forms: [Widget.Run] lands on alpha.Widget.Run, [beta.Thing.Do] on
  beta.Thing.Do.
- plain_text, struct_fields: Run's doc is the source alpha.Widget.Run.
- lookup_order: two alpha.Run sources and beta.Promoted named no node
  any more, so they held vacuously. They are now alpha.Widget.Run and
  beta.Embedded.Promoted. The alias links land on beta.Thing.Do and
  alpha.Deep10.End.
- node_owner, the positive contract that replaces the fused case:
  - two Method nodes, alpha.A.Close and alpha.B.Close, and none named
    alpha.Close;
  - [A.Close] and [B.Close] are edges, not graph_gap rows;
  - each method's own [Target] is an edge from its own doc line (9 and
    12), with no unresolved row;
  - the type Open keeps alpha.Open: [Open] is the type (no graph_gap),
    [B.Open] is the method, and UsesOpen has 2 targets.
- scope_blob:
  - method records carry w `-`;
  - swapping two methods of one name no longer changes the blob;
  - a func/type pair swap still does (new case);
  - the k1-k3 records follow (a method neither keeps nor contests a
    name);
  - a new k4 keeps the "two on one line contest it" rule covered with a
    var and a const.
- incremental_equals_full:
  - [b.T.M], [b.W.Name] and [b.V.Name] all resolve, and U.M's [Target]
    is an edge;
  - removing T.M breaks only [b.T.M];
  - swapping V.Name and W.Name moves nothing and is now an ordinary
    closure repair instead of a forced full rebuild.
- scope_delta_rules:
  - a method/function swap is LOCAL;
  - removing the method of such a pair is LOCAL with the name "Open" (a
    member left);
  - new func/type cases keep the GLOBAL keeper-change rules covered for
    swap and removal.
- workers_agree: [p01.T3.Close] now resolves (exact edge, no row). The
  graph_gap reason stays covered by the named Shared assertion: one
  function declared by every file of a package, which is still a gap.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, @ilyabrykau-orca. Receiver-qualified Go method QNs fix the fused-method bug at its root, and your census on this thread is what let us take the direction decision on numbers. As on #1907, we finished the review asks on your branch ourselves so you do not need another rebase. Your commit is unchanged; ours sit on top, signed off:

  1. Merged the updated fix(pipeline): suppress weak short-name matches for Go selector calls #1907 head, which carries current main (5f66c867) and the field-type-hint fix described below. The only conflict was the shared test anchor in tests/test_pipeline.c, and both sides are kept.
  2. CBM_INDEX_FORMAT_VERSION 3 -> 4 (src/store/store.h; main had reached 3 since the review asked for 2), with the line 4 #1913: Go method QNs are receiver-qualified. An index written before this change now goes through one full rebuild instead of mixing flat and receiver-qualified method nodes. We will state the one-time rebuild in the release notes.
  3. Tighter pipeline_go_method_caller_keeps_lsp_join. The edge is now pinned by QN on both ends (…svc.Repo.Install -> …svc.Repo.helper) with an lsp_* strategy, and the test asserts that nothing binds the decoy other.Decoy.helper.
  4. One finding from our review of the updated head, fixed here. The templ grammar is Go-derived (method_declaration with a receiver), so templ methods reached the same receiver branch in extract_defs.c. They got a receiver-qualified def QN, while templ's call-scope QN (compute_func_qn) stays flat, so calls inside a templ method named a QN no node carried. The rewrite is now Go-only, and extract_templ_method_qn_matches_call_scope pins def QN == call scope.
  5. A small hardening, also from that review. cbm_fqn_compute_source_lang returns NULL only when the arena is exhausted, and both receiver-qualified branches passed that NULL to %s. Both sides now fall back to the flat QN together, so a def and its calls still agree. This touches the allocation-failure path only.
  6. The stale flat-QN comment in pass_semantic.c is updated. We left the go_lsp.c concatenation as it is: it matches the other two sites, and the tests hold all three together.
  7. The Go doc-link layer from main now follows the new QN scheme. It is a consumer of method QNs too.
    • Its tests had pinned the fused flat node ((A) Close and (B) Close sharing alpha.Close). They now pin each receiver-qualified method node and its edge, so doc links to same-named methods of different receivers resolve.
    • Its model still let every method claim the flat node <dir>.<name>. A type, function or value sharing its name with a method in the same directory was therefore reported as a graph gap. main reports the same gap, because there the method really did take that node. So this completes your change rather than fixing anything it broke.
    • Methods no longer claim a flat name (doclink_go.c, doc_links_go.c). On terraform, the 9 graph-gap rows become 8 correct doc-link edges, e.g. [AbsProviderConfig] now lands on the type rather than the method RootProviderConfig.AbsProviderConfig. vault and prometheus are unchanged.

Your change is what turns the Go LSP's type dispatch into graph edges. On the 13 measured repos, the calls that scip-go confirms as correct lsp_type_dispatch edges go from 20,642 to 30,750, and lsp_direct from 56,924 to 60,105. Precision rises from 0.900 (#1907 alone) to 0.944.

How we measured the stack

Each of these was indexed on 13 Go repos with the parallel resolver, and on caddy + prometheus with the sequential one as well:

Every repo was indexed twice per binary, and both runs were identical for every edge type. Precision and recall come from scip-go 0.2.7 as an independent oracle. Precision is counted only on the 6 repos where scip-go also records external references (on the other 7 it records none, so a call bound to the wrong project function would look correct there). Recall covers all 13.

build precision (6 repos) recall (13 repos)
main 0.632 0.925
#1907 0.864 0.889
#1907 + fix 0.900 0.888
#1907 + #1913 0.845 0.893
#1907 + #1913 + fix 0.944 0.873

Per repo, main -> the full stack with the fix:

repo precision recall
caddy 0.547 -> 0.820 0.842 -> 0.904
golang/tools 0.485 -> 0.910 0.942 -> 0.921
grpc-go 0.621 -> 0.914 0.897 -> 0.869
terraform 0.703 -> 0.981 0.894 -> 0.820
helm 0.611 -> 0.937 0.901 -> 0.922
prometheus 0.647 -> 0.970 0.929 -> 0.878

The honest part is the recall trade. Overall recall goes from 0.925 to 0.873. The largest drops are vault (0.906 -> 0.792, −11.4 points) and terraform (−7.4 points). Most of what the guard removes was wrong, but not all of it.

The true calls it loses are method calls on values whose receiver type the Go LSP does not infer: field chains (c.router.Route), method-return chains (client.Sys().Mount) and locals bound by := (runner.Stop). A weak short-name guess happened to get these right, and the guard drops such guesses by design. Of the pairs newly missed on vault, terraform, caddy and prometheus, 2 out of 4,381 are package-qualified calls; all the rest have this shape.

One caveat on reading those numbers: #1913 changes what the oracle can match. Receiver-qualified QNs let scip-go map 20,912 more caller/callee pairs (237,806 vs 216,894), so recall only compares within one QN scheme. Winning those calls back is on us, not on you. Receiver typing in the Go LSP (field chains, method-return chains, := from project constructors) is our own follow-up.

One more change: the parallel resolver's field-type hint for Go

Running both resolvers side by side showed that the stack made them disagree. Here is CALLS from the parallel / sequential resolver:

build caddy prometheus
main 9,964 / 9,955 38,206 / 38,067
#1907 7,814 / 7,680 30,050 / 28,406
#1907 + #1913 8,391 / 7,578 30,014 / 25,142
#1907 + fix 7,682 / 7,680 28,498 / 28,406
#1907 + #1913 + fix 7,580 / 7,578 25,220 / 25,142

The cause was ours, not yours. In the review asks we kept field_type_hint out of your guard as "receiver-aware". It is not. try_field_type_hint runs only in the parallel resolver. It capitalizes the receiver variable name and accepts any candidate whose QN contains it, so t.Run(...) on a *testing.T became T and bound a project TBench.Run. The sequential resolver dropped the same weak pick through your guard.

Receiver-qualified QNs (#1913) put type names into many more QNs, which made the hint fire far more often. The typed field call in lrp_go_s8 (s.repo.Find()) never needed the hint; the Go LSP resolves it.

The fix (e554a2ab, on #1907) has two parts:

  • Go no longer takes the hint.
  • The guard treats field_type_hint as weak again, like the shared list.

Both pipeline twins now assert that t.Run binds no project Run, and the parallel twin fails without the fix. On scip-go's call sites, the fix costs and removes the following:

on top of correct calls lost wrong calls removed
#1907 17 3,927
#1907 + #1913 2,048 12,434

No Go field_type_hint edges are left.

Thank you again for the care in this stack, and especially for chasing down the third QN site on your own.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	tests/test_registry.c
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Merged current main (1346ac4e, which now includes #2681 and #2686) so the branch is mergeable again. The only conflict was the test registration list in tests/test_registry.c, and both sides are kept. Affected suites are green locally; CI is re-running.

Two PDF doc-link tests named a Go method by its flat QN. With DeusData#1913 a
Go method's QN carries its receiver, so the lookups found no node.
The resolver is unchanged: for a Go method, rel(parent_class).name now
equals the QN, so the method binds through its QN, like a member of
any other language.

- pdf_links_pipeline: page 1 names `server.Handler.Serve`; the edge
  target pkg.server.Serve becomes pkg.server.Handler.Serve. Same
  assertions: pdf_qn syntax, unique tier.
- pdf_qn_method_through_owner, page 2: `time.Provider.Now` binds
  pkg.time.Provider.Now (was pkg.time.Now), syntax pdf_qn.
- pdf_qn_method_through_owner, page 1: `time.Now` must not bind the
  method. The target moves to pkg.time.Provider.Now too. Against the
  old flat QN, no node exists any more, so the zero count would have
  passed vacuously. The page 2 assertion proves that the node exists.

The ownerless-QN rule in doc_links_pdf.c stays: a templ method keeps
the flat package QN while it carries parent_class (extract_defs.c only
receiver-qualifies CBM_LANG_GO). Its comment no longer says that Go
methods have no receiver.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings in main 658639e (DeusData#2685, Rust generic-impl call scope). One
conflict in tests/test_extraction.c: main's new F-R4 test and its
helper sit right before the flag-exempt test, whose comment this
branch rewrote (Go now sets is_method, so the guarded language is
Rust). Both are kept: main's block, then this branch's comment.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Brings in the base branch with main 658639e merged (DeusData#2685, Rust
generic-impl call scope), so this branch merges cleanly into main
again. No conflicts.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

CI caught two PDF doc-link tests (tests/test_doc_links_pdf.c:1092, :1175) that still named a Go method by its flat QN (pkg.server.Serve, pkg.time.Now). With the receiver-qualified scheme that node is pkg.server.Handler.Serve / pkg.time.Provider.Now. aa4775f updates the three lookups. That includes the time.Now must-not-bind check, which would otherwise have passed vacuously against a node that no longer exists. The PDF resolver needs no change: a Go method now binds through its QN like any other member. The rule for owner-less QNs stays for templ methods, and its comment is updated. The branch also takes the refreshed #1907 base (current main 658639e). The whole doc-link family (15 suites, 282 tests) is green locally; CI is re-running.

This branch has not been deployed

No deployments
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: method QNs ignore the receiver — every same-name method in a package collapses onto one node

2 participants