Skip to content

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

Description

@ilyabrykau-orca

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.ErrorMSIError.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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    parsing/qualityGraph extraction bugs, false positives, missing edges

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions