fix(extract): give each Go init() its own QN - #1915
Conversation
|
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. What that means for this PR, concretely:
Things that will genuinely speed it up whenever review does happen:
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. |
b22b1a2 to
4e03c2b
Compare
|
Reviewed the last commit. The diagnosis is right, the safety argument holds, and I have one substantive concern about the suffix itself. The diagnosis is confirmed by the schema. Your safety claim holds, and I checked it both ways. Nothing in The precedents you cite are real. The concern: the suffix is not stable, and both precedents areThis is the part I would like you to weigh, because it is the one property those two precedents share that yours does not. The Rust cfg twin derives its suffix from the
GoogleTest derives its from the macro arguments —
The file basename alone will not do it — your own I am raising this rather than ruling on it — you may have considered ordinals and rejected them for a reason I cannot see from here. If so, say why and I will take it. AlsoThe whole stack is currently Thanks for measuring the loss rather than describing it — "21 decoder-registration |
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>
4e03c2b to
fcbd12e
Compare
|
Rebased onto current main as part of the 1907→1913→1915→1936 stack; only test insertion-anchor drift in tests/test_pipeline.c, re-anchored. Full scripts/test.sh green on the stack head. |
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>
fcbd12e to
adff62e
Compare
|
You are right, and the ordinal is strictly better — implemented, not argued with. I had not weighed the incremental path when I chose the line number: on a full rebuild the line suffix is merely ugly, but under delta-merge the QN is node identity, so an insertion above any Now: The test now pins the property you asked for, not just distinctness: exact QNs Branch force-pushed with the stack rebase; full scripts/test.sh green on the stack head. |
What does this PR do?
Fixes #1910. Stacked on #1913 (which stacks on #1907) — same extraction area and shared test scaffolding; review only the last commit.
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 in #1910: 21 decoder-registrationinits in one package → 1 node; 29 in the repo → 7 nodes).The change, following the #495 Rust cfg-twin pattern (and the #1266 GoogleTest name-derivation precedent):
internal/cbm/extract_defs.c— suffix the QN with the file basename and line:proj.pkg.init#a.go:L5. Callinginitexplicitly is illegal in Go, so nothing ever joins on the plain QN — no resolution path is affected.internal/cbm/extract_unified.c(compute_func_qn) — mirror the exact formula, so init-body calls keep their source attribution instead of degrading to thecalls_find_sourceFile-node fallback.Tests (reproduce-first):
pipeline_go_multi_init_nodes_survive— two files, one package, oneinit()each: RED without the def-side change (ic == 1, expected 2), GREEN with it.extract_go_multiple_init_disambiguated— twoinits in ONE file get distinct suffixed QNs (the line component disambiguates within a file).Full
scripts/test.shleg green (ASan+UBSan, "All tests passed");git clang-format --diffclean on changed lines.Checklist
git commit -s) — required, CI rejectsunsigned commits (DCO, see CONTRIBUTING.md)
scripts/test.sh— full leg, ASan+UBSan, "All tests passed")git clang-format --diffclean on changed lines; clang-tidy/cppcheck via CI)