Repository navigation
go: Wire (google/wire) dependency-injection bindings are not modeled, so interface calls can't be resolved per binary #2444
Description
What problem does this solve?
In Go codebases that use google/wire for dependency injection, the concrete type behind an interface is chosen at compile time in the Wire injector (wire.go with //go:build wireinject) and written out in the generated wire_gen.go. The graph has no view of that binding, so questions like "can binary X reach this concrete method?" can't be answered.
Example shape:
// store.go
type ObjectWriter interface{ WriteFile(ctx context.Context, name string, r io.Reader) error }
type localWriter struct{ root string }
func (w *localWriter) WriteFile(ctx context.Context, name string, r io.Reader) error { /* os.Create under w.root */ }
type s3Writer struct{ /* ... */ }
func (w *s3Writer) WriteFile(ctx context.Context, name string, r io.Reader) error { /* ... */ }
// handler.go
type Handler struct{ w ObjectWriter }
func (h *Handler) Upload(ctx context.Context, r io.Reader) error { return h.w.WriteFile(ctx, "x", r) }
// cmd/api/wire.go (//go:build wireinject)
var set = wire.NewSet(newLocalWriter, wire.Bind(new(ObjectWriter), new(*localWriter)), NewHandler)
func InitializeAPI() (*Handler, error) { wire.Build(set); return nil, nil }
// cmd/api/wire_gen.go (generated)
func InitializeAPI() (*Handler, error) {
w := newLocalWriter()
h := NewHandler(w)
return h, nil
}Today:
Handler.Uploadgets aCALLSedge to the interface memberObjectWriter.WriteFile, andlocalWriter.WriteFilegets anOVERRIDEedge to it.- An inbound
trace_pathfromlocalWriter.WriteFilereturns 0 callers, because the caller edge lands on the interface member andtrace_pathdoesn't hop back acrossOVERRIDE(this is the part Title: Go interface calls with multiple implementations do not produce candidate concrete CALLS edges #1114 covers). - Even with that hop, the graph can't tell which binary binds
ObjectWriterto*localWriterrather than*s3Writer. That fact lives only inwire.Bind(...)in the injector and in the constructor order inwire_gen.go.
In practice, for a monorepo with many cmd/* binaries sharing packages, the question "which services write to the local disk?" requires reading wire_gen.go by hand. The graph can show that a service constructs newLocalWriter, but not that its handlers dispatch to it.
Proposed solution
Wire-aware extraction for Go:
- Parse
wire.Bind(new(I), new(T))in injector files and provider sets, and emit an edge such asBINDSfrom the provider set / injector toT, withIrecorded as a property. That records a per-injector "interface I is satisfied by T here" fact. - Link injectors to their entry point:
wire_gen.go'sInitializeX→ the constructors it calls (these are ordinaryCALLStoday, so this mostly exists already), plusmain→InitializeX. - Binding-aware
trace_path: when an inbound or outbound traversal hits an interface member, an option such asresolve_bindings: truenarrows the candidate concrete implementers to those bound in the injector reachable from the traversal's entry point. Without the option, it falls back to all implementers (as Title: Go interface calls with multiple implementations do not produce candidate concrete CALLS edges #1114 proposes).
Even (1) alone would make "which binaries bind *localWriter" a single Cypher query.
Good OSS test beds:
google/wire's own_tutorialandinternal/wire/testdatagoogle/go-cloudsamples (the guestbook sample uses Wire injectors per cloud provider, binding the same interfaces to different concrete types per binary)
Alternatives considered
- Generic interface → all-implementers fan-out (Title: Go interface calls with multiple implementations do not produce candidate concrete CALLS edges #1114). Necessary but not sufficient. With several implementers it over-approximates, so every binary appears to reach every implementation.
- Relying on
CALLSedges intowire_gen.goconstructors. This shows what a binary constructs, but not that a given handler's interface call dispatches to it. Stitching the two together takes several hand-written Cypher queries per interface. - gopls / pointer analysis. Would resolve the dispatch precisely but is much heavier than parsing
wire.Bind/ provider sets.
Related: a method call on a function's return value (f(x).Method(...)) also produces no CALLS edge in our Go index. This is common with Wire-provided factory functions. Happy to file that separately if it isn't already tracked.
Confirmations
- I searched existing issues and this is not a duplicate. (Title: Go interface calls with multiple implementations do not produce candidate concrete CALLS edges #1114 covers interface → concrete fan-out; go: interface-typed variable assigned a concrete type across closures — CALLS target is the interface member (decision + fixture) #2038 covers interface-typed variables across closures; neither addresses DI binding.)
Activity
DeusData commentedon Oct 1, 2026
Thank you for the detailed example and for separating Wire binding information from general interface/override traversal. We have seen draft #2452 and the separate grouped-variable fix in #2453. A new BINDS relationship changes the graph contract and needs a focused design review before we can approve it. We need more time for that review; please keep #2452 as a draft in the meantime. This is an acknowledgement of the proposal, not an integration commitment, and the standalone extraction bug can be reviewed independently.
DeusData commentedon Oct 19, 2026
Thank you again for the Wire examples. The per-injector binding contract remains a design decision, and I have no new approval or integration timeline to report. The distinction from general interface fan-out and the separate extraction fix remains useful.