Skip to content

Go: build-tag twin files collide on one QN — the stub can shadow the real implementation (Rust cfg got #495) #1911

Description

@ilyabrykau-orca

Status: draft implementation-proposal open — PR #1946 (marked ready on maintainer design ack). Minimal scope as proposed here: per-file τ folded into func/method QNs, #495 #-suffix style; τ-aware resolution is the follow-up. Sole-constrained-variant callers keep their edges; twin-pair callers from unconstrained files resolve as ambiguous (honest) instead of binding the lexicographically-smallest stub. Receiver qualification (#1913) does not separate twins — generated-twin evidence below.

Version

codebase-memory-mcp 0.10.8 (also reproduced on a source build @ d706c33)

Platform

macOS (Apple Silicon)

Install channel

Built from source

Binary variant

standard

What happened, and what did you expect?

Go build-constrained twin files (//go:build unix / //go:build !unix, GOOS/GOARCH filename suffixes) legally define the same symbols in one package. The indexer ignores the constraints; the upsert keeps one node per QN by the deterministic smallest-file-path rule. Measured:

  • hostmirror_other.go (2-line stub) beat hostmirror_unix.go (63-line real implementation) because o < u: get_code_snippet returns the stub and all 25 inbound CALLS landed on the stub;
  • same for a nofollow_unix.go/nofollow_other.go constant pair;
  • one generated GOARCH twin pair loses 39 of 40 nodes — 100% of the losing file;
  • the dropped twin leaves no trace: both files parse clean, so index_status/check_index_coverage report nothing.

Expected: both build variants exist as nodes — exactly what #495 already does for Rust #[cfg(...)] twins.

Reproduction

// mirror_unix.go
//go:build unix

package mirror

func MirrorConfig(path string) (string, error) {
	dst := path + ".bak"
	// ... real work ...
	return dst, nil
}
// mirror_other.go
//go:build !unix

package mirror

func MirrorConfig(path string) (string, error) { return "", nil }

Actual: one node, file_path = mirror_other.go (stub wins); the real implementation has no node. Expected: two nodes, one per constraint.

Proposed design (Go analog of #495)

Per-file τ, resolved once: the compacted //go:build expression when present, else the GOOS/GOARCH filename suffix. Fold τ into that file's def QNs (proj.pkg.MirrorConfig#unix), mirrored on the call-scope side. Same-config cross-file calls stay exact by trying module.name#τ (caller's τ) before the plain key; calls from unconstrained files degrade to the multi-candidate path — the honest answer, and strictly better than today's silent bind-everyone-to-the-stub.

Generated-twin evidence (bpf2go, measured with #1913/#1915 applied)

bpf2go emits one Go file per (program, target) pair, gated by //go:build GOARCH/endianness lines. Two files in the measured repo are GOARCH twins with an identical set of 21 top-level declarations: the arm64 twin kept 43 nodes, the x86 twin kept 1 (its __file__ node) — 39 symbol nodes lost, 100% of the losing file, invisible to parse_partial. Directory-level: 66 Close methods in source, 63 in the graph — the missing 3 are exactly the x86 twin's, and receiver-qualified QNs cannot separate them because the twins are twins all the way down; only a build-constraint discriminator in the QN can. Scale note: adding a second build target — a routine change — multiplies the loss by the number of programs, which argues for a cheap, automatic discriminator rather than an opt-in.

Related: #495, #1909, #1910 (same silent-upsert-collapse family); tracked in #1932.

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

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