Skip to content
This repository was archived by the owner on Aug 13, 2026. It is now read-only.

Commit 88562bc

Browse files
committed
fix(plan): exclude the container's own constructor from by-type lookup
1 parent 0dfba16 commit 88562bc

2 files changed

Lines changed: 65 additions & 6 deletions

File tree

internal/plan/plan.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,14 @@ func Build(c ir.Container, idx *Index, opts Options) (Plan, []diag.Diag) {
111111
diags = append(diags, emDiags...)
112112

113113
r := &resolver{
114-
idx: idx,
115-
inputs: inputs,
116-
overrides: overrides,
117-
embeds: embeds,
118-
stepByKey: map[string]int{},
119-
active: map[string]bool{},
114+
idx: idx,
115+
inputs: inputs,
116+
overrides: overrides,
117+
embeds: embeds,
118+
stepByKey: map[string]int{},
119+
active: map[string]bool{},
120+
selfPkgPath: c.PkgPath,
121+
selfFuncName: constructorName,
120122
}
121123

122124
var outputs []Output
@@ -175,6 +177,15 @@ type resolver struct {
175177
steps []Step
176178
stepByKey map[string]int
177179
active map[string]bool
180+
181+
// selfPkgPath and selfFuncName identify this container's own
182+
// generated constructor. They are used to filter the by-type lookup
183+
// so a container whose `inject:"returns"` declares an interface that
184+
// matches an unrelated provider does not see its own previously
185+
// emitted constructor as a candidate (a self-loop that would also
186+
// produce a spurious "multiple providers" error).
187+
selfPkgPath string
188+
selfFuncName string
178189
}
179190

180191
// embedSource describes one exported field of an inject:"embed" input that
@@ -192,6 +203,24 @@ func (r *resolver) resolveField(f ir.Field) (int, []diag.Diag) {
192203
return r.resolveByType(f.Type, f.Pos, "field "+f.Name)
193204
}
194205

206+
// excludeSelfProvider drops the container's own previously generated
207+
// constructor from the candidate list so a `inject:"returns"` field does
208+
// not pick itself up via type lookup. Callers that name a provider
209+
// explicitly via inject:"with=..." are not affected.
210+
func (r *resolver) excludeSelfProvider(candidates []*ir.Provider) []*ir.Provider {
211+
if r.selfFuncName == "" {
212+
return candidates
213+
}
214+
kept := candidates[:0:0]
215+
for _, c := range candidates {
216+
if c.PkgPath == r.selfPkgPath && c.FuncName == r.selfFuncName {
217+
continue
218+
}
219+
kept = append(kept, c)
220+
}
221+
return kept
222+
}
223+
195224
func (r *resolver) resolveByType(want types.Type, pos token.Position, parent string) (int, []diag.Diag) {
196225
tk := TypeKey(want)
197226

@@ -210,6 +239,7 @@ func (r *resolver) resolveByType(want types.Type, pos token.Position, parent str
210239
}
211240

212241
candidates := r.idx.LookupByType(want)
242+
candidates = r.excludeSelfProvider(candidates)
213243
if len(candidates) == 0 {
214244
return -1, []diag.Diag{
215245
diag.Errorf(pos, "no provider for %s (required by %s)", TypeString(want), parent),

internal/plan/plan_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,35 @@ type Container struct {
344344
}
345345
}
346346

347+
func TestBuild_SelfGeneratedProviderIgnored(t *testing.T) {
348+
t.Parallel()
349+
350+
// A `inject:"returns"` container whose previously generated
351+
// constructor is now visible to the provider scan must not pick that
352+
// constructor up as a candidate when resolving its own field, or it
353+
// would loop forever. The unrelated, non-self provider remains usable.
354+
src := `package test
355+
type Greeter interface{ Greet() string }
356+
type greeterImpl struct{}
357+
func (greeterImpl) Greet() string { return "" }
358+
func NewGreeterImpl() Greeter { return greeterImpl{} }
359+
360+
// Pretend this came from a previous generation of NewWrapper.
361+
func NewWrapper() Greeter { return nil }
362+
363+
type wrapper struct {
364+
service Greeter ` + "`inject:\"returns\"`" + `
365+
}
366+
`
367+
p, ds := build(t, src, "wrapper", plan.Options{})
368+
if diag.HasErrors(ds) {
369+
t.Fatalf("expected the self provider NewWrapper to be filtered out, got %v", ds)
370+
}
371+
if len(p.Steps) != 1 || p.Steps[0].Provider == nil || p.Steps[0].Provider.FuncName != "NewGreeterImpl" {
372+
t.Errorf("expected NewGreeterImpl to be the only provider step; got %+v", p.Steps)
373+
}
374+
}
375+
347376
func TestBuild_FieldNameTakenForcesSuffix(t *testing.T) {
348377
t.Parallel()
349378

0 commit comments

Comments
 (0)