fix(pipeline): field_type_hint binds by receiver variable-name substring, not declared type
Status: fix open — PR #1936 (owning-segment equality, fth_owner_segment_is; stacked on #1907→#1913→#1915). Measured: field_type_hint 1484 → 356, single-character receivers 376 → 0, Go→C/C++ 34 → 0, nothing relocated to other strategies. Other strategies before/after: suffix_match 113 → 114, unique_name 1573 → 1573, same_module 224 → 224, all lsp_* 10672 → 10672; CALLS total 14316 → 13189 — the 1128 removed edges were fabricated on top of already-failed resolutions, so nothing absorbed them.
What the code does
try_field_type_hint() (src/pipeline/pass_parallel.c, parallel resolver only): split callee_name at the first . → receiver identifier text; strip _/m_, uppercase the first letter → type_name; also build iface_name = "I"+type_name; then accept the first same-named candidate where strstr(candidate_qn, type_name) or strstr(candidate_qn, iface_name) hits, overwriting the resolution at confidence 0.85 as field_type_hint.
That is a raw substring test on the QN — no declared type, field type, or LSP type is ever read. For a single-letter receiver (t, f, w, c, m) type_name is one capital letter and matches almost any QN, the method name included ("F" ⊂ "…FileStore.Close").
The Go weak-match guard (#1906/#1907) deliberately keeps field_type_hint on the stated premise that it is receiver-aware for Go. The implementation does not meet that premise; the pinning test (lrp_go_s8_field_type_hint, repo/Repo) passes for the wrong reason (its GREEN path is lsp_cross).
Damage measured (~1150-file Go+C repo)
|
v0.10.8 |
with #1906/#1909/#1913/#1915 fixes |
| CALLS total |
19653 |
14316 |
field_type_hint |
493 (2.5%) |
1484 (10.4%) |
| single-character receiver |
— |
376 |
The share tripled because #1906/#1913 collapsed suffix_match/same_module: calls that used to land in a visibly weak 0.30 bucket now present at 0.85. Concrete wrong binds: err.Error → MSIError.Error (61 edges, "Err" ⊂ "MSIError"), f.Close → a bpf2go-generated …PtraceObjects.Close (51), w.Write → a test-only failingWriter.Write (40), t.Name → a C++ mock field (20). 34 edges cross into C/C++.
Minimal reproduction
// a.go
package app
type Tracker struct{ n int }
func (tr *Tracker) Name() string { return "tracker" }
// a_test.go
package app
import "testing"
func TestSomething(t *testing.T) {
if t.Name() == "" { t.Fatal("no name") }
}
t.Name() is (*testing.T).Name — external. Expected: no CALLS edge. Actual: edge to app.Tracker.Name at 0.85 ("T" ⊂ QN).
Detection recipe
SELECT json_extract(properties,'$.strategy') AS s, count(*)
FROM edges WHERE type='CALLS' GROUP BY 1 ORDER BY 2 DESC;
-- the indefensible core: single-character receivers
SELECT count(*) FROM edges
WHERE type='CALLS'
AND json_extract(properties,'$.strategy')='field_type_hint'
AND json_extract(properties,'$.callee') GLOB '?.*'
AND json_extract(properties,'$.callee') NOT GLOB '*.*.*';
Fix
Landed option (1) of the original three: the candidate's owning dot-segment — the segment immediately before the method — must equal type_name or iface_name. Measured: kills every row above, keeps the variable-named-after-its-type case, moves 63 event.Get* edges onto the interface that declares the method, and the single-character class dies without a minimum-length gate. Residual: scanner.Scan (16 edges, owner genuinely named Scanner) — only a declared-type lookup (option 3, design-sized) can reject that; deliberately out of scope.
Related: #1906/#1907 (the guard whose premise this fixes), #1942 (same class on the reference side), #725, #1114; tracked in #1932.
fix(pipeline):
field_type_hintbinds by receiver variable-name substring, not declared typeStatus: fix open — PR #1936 (owning-segment equality,
fth_owner_segment_is; stacked on #1907→#1913→#1915). Measured:field_type_hint1484 → 356, single-character receivers 376 → 0, Go→C/C++ 34 → 0, nothing relocated to other strategies. Other strategies before/after:suffix_match113 → 114,unique_name1573 → 1573,same_module224 → 224, alllsp_*10672 → 10672; CALLS total 14316 → 13189 — the 1128 removed edges were fabricated on top of already-failed resolutions, so nothing absorbed them.What the code does
try_field_type_hint()(src/pipeline/pass_parallel.c, parallel resolver only): splitcallee_nameat the first.→ receiver identifier text; strip_/m_, uppercase the first letter →type_name; also buildiface_name = "I"+type_name; then accept the first same-named candidate wherestrstr(candidate_qn, type_name)orstrstr(candidate_qn, iface_name)hits, overwriting the resolution at confidence 0.85 asfield_type_hint.That is a raw substring test on the QN — no declared type, field type, or LSP type is ever read. For a single-letter receiver (
t,f,w,c,m)type_nameis one capital letter and matches almost any QN, the method name included ("F"⊂"…FileStore.Close").The Go weak-match guard (#1906/#1907) deliberately keeps
field_type_hinton the stated premise that it is receiver-aware for Go. The implementation does not meet that premise; the pinning test (lrp_go_s8_field_type_hint,repo/Repo) passes for the wrong reason (its GREEN path islsp_cross).Damage measured (~1150-file Go+C repo)
field_type_hintThe share tripled because #1906/#1913 collapsed
suffix_match/same_module: calls that used to land in a visibly weak 0.30 bucket now present at 0.85. Concrete wrong binds:err.Error→MSIError.Error(61 edges,"Err"⊂"MSIError"),f.Close→ a bpf2go-generated…PtraceObjects.Close(51),w.Write→ a test-onlyfailingWriter.Write(40),t.Name→ a C++ mock field (20). 34 edges cross into C/C++.Minimal reproduction
t.Name()is(*testing.T).Name— external. Expected: no CALLS edge. Actual: edge toapp.Tracker.Nameat 0.85 ("T"⊂ QN).Detection recipe
Fix
Landed option (1) of the original three: the candidate's owning dot-segment — the segment immediately before the method — must equal
type_nameoriface_name. Measured: kills every row above, keeps the variable-named-after-its-type case, moves 63event.Get*edges onto the interface that declares the method, and the single-character class dies without a minimum-length gate. Residual:scanner.Scan(16 edges, owner genuinely namedScanner) — only a declared-type lookup (option 3, design-sized) can reject that; deliberately out of scope.Related: #1906/#1907 (the guard whose premise this fixes), #1942 (same class on the reference side), #725, #1114; tracked in #1932.