Skip to content

Code Analysis Go Interface Candidates

Tu Shaokun edited this page Nov 8, 2026 · 2 revisions

Go interface-mediated candidates for a changed concrete method

Validated in Draft, not Labs: #1285, head 209f4857c4b746a4f0ac6fd88bdd449ae2d2e080, starts at #1284's source branch codex/code-analysis-go-gopls-eval from 59f1a80058c5e5fd8859be960b436a05a799d8e7 and targets that branch. No approval, merge, release, installation or labs/code-analysis update.

The next source step

#1284 bound interface-typed calls to captured interface declarations, but a change question for a concrete method still reported only direct callers: Echo *context.RealIP showed 3 positions instead of 9, client_golang *Registry.Gather 47 instead of 56.

This round attaches the interface-mediated positions to the changed implementation while keeping them separate from direct calls:

  • knownReferences: direct calls on the changed method, unchanged.
  • interfaceCandidates: calls whose static receiver is an interface that the changed type's complete method set satisfies.

Each candidate carries the chain call site → interface method declaration → satisfying concrete type and method, the valueSatisfies/pointerSatisfies flags, the other captured implementations, and runtimeTarget: "not-determined". Every candidate has a reason; no candidate is presented as a definite runtime caller.

Example (Echo client_ip_security_test.go:37):

call site → Context.RealIP (context.go:46:2) → *context.RealIP (context.go:298)
             value does not satisfy; *context does

Fixed samples and results

Source-first expectations were recorded in expectations.md before implementation. Afero did not participate in prior Go rule design (Cobra, Chi, gRPC, Echo, client_golang).

Study Repository and commits Changed definition Interface Direct before → after Candidates Wrong Missed
A labstack/echo fa780cd → ebbb855 (*context).RealIP context.go:298 Context.RealIP context.go:46:2 3 → 3 6 0 0
B prometheus/client_golang 7c438e9 → 8f7c887 (*Registry).Gather registry.go:455 Gatherer.Gather registry.go:173:2 47 → 47 9 0 0
C spf13/afero effad5e → 8d2a95c (*RegexpFs).OpenFile regexpfs.go:118 Fs.OpenFile afero.go:74:2 0 → 0 38 0 0

Build scope: root module only, Darwin/arm64, cgo disabled, no custom tags, tests included, Go 1.27.2. Afero's nested modules (sftpfs/, gcsfs/) were not loaded.

Client's 47 direct references exceed the normal 40-item display bound: knownReferences.total stays 47 while 40 are listed and 7 omitted. The omitted seven are not missing bindings.

What the candidates prove and what they do not

Method-set rules, verified with go/types:

  • A type is an edge only if its whole method set implements the interface. HttpFs has a matching OpenFile(name string, flag int, perm os.FileMode) (File, error), but its Open returns http.File, so it does not implement Fs and is absent.
  • T and *T stay distinct. context does not satisfy Context; *context does. RegexpFs and other pointer-receiver implementations are valueSatisfies: false, pointerSatisfies: true; OsFs/FromIOFS are value-and-pointer.
  • Sibling interfaces with the same method spelling stay separate. Client calls through TransactionalGatherer, and direct calls to Gatherers.Gather/GathererFunc.Gather, are not candidates for *Registry.Gather.
  • Two types implementing the same interface both appear. Afero: *RegexpFs and *MemMapFs both satisfy Fs; 36 of the 38 candidates are Fs-typed calls.
  • Methods promoted from an embedded interface field are explicit alternatives. Afero's Afero and regexpStatErrorFs satisfy Fs, but their OpenFile is promoted, not a captured concrete body (reason: promoted-interface-method). Two *afero.Afero wrapper calls (tarfs/tarfs_test.go:310,324) bind with receiverPromotion: embedded-interface.
  • Candidates are deduplicated by snapshot and call fact. A call appears once even when the method implements several interfaces.

Not proven:

  • Runtime assignment. Every interface-typed call is a candidate, even when a local value is visibly a different implementation. runtimeTarget is always not-determined.
  • Interfaces with external or anonymous embeds, generic interfaces, generic concrete types and type aliases are skipped and counted in interfaceEvidence. Client has one incomplete delegator interface, afero one incomplete File interface (io embeds). Absence is unknown, not non-implementation.
  • Function variables, anonymous interfaces, type-parameter receivers and uncaptured third-party bodies are outside candidate coverage.

gopls comparison

gopls v0.24.0 was an independent oracle only, not a dependency. Archify still uses the built-in Go helper with no language-server process.

Study gopls concrete refs direct + candidates gopls interface refs interface-only extras gopls implementations captured concrete methods
A 9 3 + 6 = 9 9 0 1 1
B 56 47 + 9 = 56 61 5 3 3
C 38 0 + 38 = 38 41 3 11 11

The concrete-reference sets agree in every study. Interface-method references are broader because they include direct calls to sibling implementations: five in B (Gatherers.Gather, GathererFunc.Gather) and three in C (*CopyOnWriteFs, *MemMapFs). Those are direct implementation calls, not interface receivers, and correctly stay outside interfaceCandidates. Captured edges and gopls agree on 1/3/11 concrete implementation methods; afero's two promoted wrappers are additional captured candidates, and no gopls-only concrete implementation was found.

Compatibility and display

  • Go adapter revision bumped 4 → 5; go-source-direct-v3 adds interfaceImplementations edges and languageScope.interfaceEvidence.
  • A capture without the new evidence shows coverage: "unavailable", total: null, and the direct references already captured; it is not shown as zero candidates. The change question reports interface-implementation-evidence-unavailable and marks the side partial.
  • Direct call binding is unchanged.

Validation states

  • Local: npm test 19/19; code-analysis suite 424/424 with zero skips. New tests cover direct/candidate separation, no runtime claim, full method-set rejection (HttpFs-style), T/*T flags, sibling interfaces, promoted wrappers, package-test calls and the old-capture unavailable path.
  • Source CLI: the fixed benchmark exits 0 for A/B/C with all source-first positions, wrong = 0, missed = 0, no duplicate candidates.
  • Source identity: every displayed direct call and all 15 previous-round candidates match the source answer on repository version, target file, declaration anchor and owning type; all 38 Afero candidates match too. The same-named Gatherer.Gather -> TransactionalGatherer.Gather swap is rejected, as are the four independent field controls. Per-item tables: identity-recheck.md.
  • ZIP: unpacked archify.zip SHA256 835c60ed21a21b16cf2be50528f24c07644539cf039be6dc56804143fd6a53de produces a byte-identical normalized summary.json to the source CLI. The extracted tree needs the normal Code Analysis dependencies; the ZIP does not vendor them.
  • Remote: CI run 42459007509 succeeded at 209f4857c (test, full-regression, browser-regression, Windows portability, package smoke, zip-freshness, website; deploy jobs skipped by design).
  • Integration: Draft only; no merge, approval, release, installation or Labs update.

Reproduction: python3 benchmarks/code-analysis-go-interface-candidates/run.py with the source or ZIP CLI, the #1284 baseline CLI at 59f1a8005, preserved snapshots at the fixed commits with a readable Git identity, and optional gopls. evidence/summary.json contains the per-position classification and negative-control result; identity-recheck.md itemizes direct calls and candidates.

Decision input: keep improving the built-in parser. Method-set edges answer "which interface positions should I inspect and why" without claiming runtime identity; adopting gopls is not required.

Clone this wiki locally