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

Commit ba86489

Browse files
authored
Merge pull request #6 from mickamy/fix/var-name-collision
fix(emit): rename local vars and params that collide with import aliases
2 parents 89fb6f3 + 8ec97c8 commit ba86489

4 files changed

Lines changed: 135 additions & 29 deletions

File tree

example/returns/injector_gen.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/emit/emit.go

Lines changed: 79 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,8 @@ func collectImports(im *Imports, p plan.Plan) {
100100
func writeContainer(w io.Writer, im *Imports, p plan.Plan) error {
101101
name := p.ConstructorName
102102
structName := p.Container.StructName
103-
paramSig := formatParams(im, p.Inputs)
103+
ns := assignNames(im, p)
104+
paramSig := formatParams(im, p.Inputs, ns)
104105
retSig := im.QualifyType(p.ReturnType)
105106

106107
fmt.Fprintf(w, "// %s initializes dependencies and constructs %s.\n", name, structName)
@@ -110,29 +111,86 @@ func writeContainer(w io.Writer, im *Imports, p plan.Plan) error {
110111
fmt.Fprintf(w, "func %s(%s) %s {\n", name, paramSig, retSig)
111112
}
112113

113-
wrote, err := writeSteps(w, im, p, retSig)
114+
wrote, err := writeSteps(w, im, p, ns, retSig)
114115
if err != nil {
115116
return err
116117
}
117118

118-
writeStructLiteral(w, p, wrote)
119+
writeStructLiteral(w, p, ns, wrote)
119120
if p.ReturnsError {
120121
fmt.Fprint(w, ", nil")
121122
}
122123
fmt.Fprint(w, "\n}\n\n")
123124

124125
if p.EmitMust {
125-
writeMustVariant(w, im, p)
126+
writeMustVariant(w, im, p, ns)
126127
}
127128
return nil
128129
}
129130

131+
// names holds the final, collision-free identifiers used when rendering one
132+
// plan. Indices line up with plan.Inputs / plan.Steps.
133+
type names struct {
134+
inputs []string
135+
steps []string
136+
}
137+
138+
// assignNames picks variable and parameter names for the plan that do not
139+
// collide with imported package aliases. Each name is derived from the
140+
// plan-level name (Input.Name / Step.VarName) and suffixed with a counter
141+
// when a collision is detected. Steps that reference an input share that
142+
// input's renamed identifier so the function signature and the body stay
143+
// in sync.
144+
func assignNames(im *Imports, p plan.Plan) names {
145+
taken := make(map[string]bool, len(im.used)+3)
146+
for a := range im.used {
147+
taken[a] = true
148+
}
149+
// Reserve identifiers the generated body always uses.
150+
taken["err"] = true
151+
taken["v"] = true
152+
taken["_"] = true
153+
154+
pick := func(base string) string {
155+
if base == "" || base == "_" {
156+
base = "v"
157+
}
158+
if !taken[base] {
159+
taken[base] = true
160+
return base
161+
}
162+
for i := 2; ; i++ {
163+
try := fmt.Sprintf("%s%d", base, i)
164+
if !taken[try] {
165+
taken[try] = true
166+
return try
167+
}
168+
}
169+
}
170+
171+
ns := names{
172+
inputs: make([]string, len(p.Inputs)),
173+
steps: make([]string, len(p.Steps)),
174+
}
175+
for i, in := range p.Inputs {
176+
ns.inputs[i] = pick(in.Name)
177+
}
178+
for i, s := range p.Steps {
179+
if s.Kind == plan.StepKindInput {
180+
ns.steps[i] = ns.inputs[s.InputIndex]
181+
continue
182+
}
183+
ns.steps[i] = pick(s.VarName)
184+
}
185+
return ns
186+
}
187+
130188
// writeSteps emits step lines and reports whether any visible code was
131189
// written. Pure-input plans (e.g. a container whose only inputs are
132190
// non-blank inject:"arg" fields) produce no step lines, and the caller
133191
// uses the bool to decide whether to leave a blank line before the
134192
// struct literal.
135-
func writeSteps(w io.Writer, im *Imports, p plan.Plan, retSig string) (bool, error) {
193+
func writeSteps(w io.Writer, im *Imports, p plan.Plan, ns names, retSig string) (bool, error) {
136194
zeroExpr := "nil"
137195
if !isNilable(p.ReturnType) {
138196
// For non-nilable return types we need an explicit zero value;
@@ -141,30 +199,29 @@ func writeSteps(w io.Writer, im *Imports, p plan.Plan, retSig string) (bool, err
141199
}
142200

143201
wrote := false
144-
for _, s := range p.Steps {
202+
for i, s := range p.Steps {
145203
switch s.Kind {
146204
case plan.StepKindInput:
147205
// The input is already a function parameter — nothing to emit.
148206
case plan.StepKindEmbedField:
149-
in := p.Inputs[s.InputIndex]
150-
fmt.Fprintf(w, "\t%s := %s.%s\n", s.VarName, in.Name, s.EmbedFieldName)
207+
fmt.Fprintf(w, "\t%s := %s.%s\n", ns.steps[i], ns.inputs[s.InputIndex], s.EmbedFieldName)
151208
wrote = true
152209
case plan.StepKindProvider:
153210
if s.Provider == nil {
154211
return wrote, fmt.Errorf("provider step %q has nil Provider", s.VarName)
155212
}
156213
args := make([]string, 0, len(s.ArgSteps))
157214
for _, idx := range s.ArgSteps {
158-
args = append(args, p.Steps[idx].VarName)
215+
args = append(args, ns.steps[idx])
159216
}
160217
call := im.QualifyProvider(s.Provider)
161218
if s.Provider.ReturnsError {
162-
fmt.Fprintf(w, "\t%s, err := %s(%s)\n", s.VarName, call, strings.Join(args, ", "))
219+
fmt.Fprintf(w, "\t%s, err := %s(%s)\n", ns.steps[i], call, strings.Join(args, ", "))
163220
fmt.Fprint(w, "\tif err != nil {\n")
164221
fmt.Fprintf(w, "\t\treturn %s, err\n", zeroExpr)
165222
fmt.Fprint(w, "\t}\n")
166223
} else {
167-
fmt.Fprintf(w, "\t%s := %s(%s)\n", s.VarName, call, strings.Join(args, ", "))
224+
fmt.Fprintf(w, "\t%s := %s(%s)\n", ns.steps[i], call, strings.Join(args, ", "))
168225
}
169226
wrote = true
170227
}
@@ -189,56 +246,51 @@ func isNilable(t types.Type) bool {
189246
return false
190247
}
191248

192-
func writeStructLiteral(w io.Writer, p plan.Plan, leadingBlank bool) {
249+
func writeStructLiteral(w io.Writer, p plan.Plan, ns names, leadingBlank bool) {
193250
if leadingBlank {
194251
fmt.Fprint(w, "\n")
195252
}
196253
fmt.Fprintf(w, "\treturn &%s{\n", p.Container.StructName)
197254
for _, o := range p.Outputs {
198-
v := p.Steps[o.StepIndex].VarName
199-
fmt.Fprintf(w, "\t\t%s: %s,\n", o.FieldName, v)
255+
fmt.Fprintf(w, "\t\t%s: %s,\n", o.FieldName, ns.steps[o.StepIndex])
200256
}
201257
fmt.Fprint(w, "\t}")
202258
}
203259

204260
// writeMustVariant emits MustNewX, which delegates to NewX and panics on
205261
// error.
206-
func writeMustVariant(w io.Writer, im *Imports, p plan.Plan) {
262+
func writeMustVariant(w io.Writer, im *Imports, p plan.Plan, ns names) {
207263
name := "Must" + p.ConstructorName
208-
paramSig := formatParams(im, p.Inputs)
264+
paramSig := formatParams(im, p.Inputs, ns)
209265
retSig := im.QualifyType(p.ReturnType)
210266

211267
fmt.Fprintf(w,
212268
"// %s initializes dependencies and constructs %s or panics on failure.\n",
213269
name, p.Container.StructName)
214270
fmt.Fprintf(w, "func %s(%s) %s {\n", name, paramSig, retSig)
215271
if p.ReturnsError {
216-
fmt.Fprintf(w, "\tv, err := %s(%s)\n", p.ConstructorName, formatArgs(p.Inputs))
272+
fmt.Fprintf(w, "\tv, err := %s(%s)\n", p.ConstructorName, formatArgs(ns))
217273
fmt.Fprint(w, "\tif err != nil {\n")
218274
fmt.Fprint(w, "\t\tpanic(err)\n")
219275
fmt.Fprint(w, "\t}\n")
220276
fmt.Fprint(w, "\treturn v\n")
221277
} else {
222-
fmt.Fprintf(w, "\treturn %s(%s)\n", p.ConstructorName, formatArgs(p.Inputs))
278+
fmt.Fprintf(w, "\treturn %s(%s)\n", p.ConstructorName, formatArgs(ns))
223279
}
224280
fmt.Fprint(w, "}\n\n")
225281
}
226282

227-
func formatParams(im *Imports, inputs []plan.Input) string {
283+
func formatParams(im *Imports, inputs []plan.Input, ns names) string {
228284
if len(inputs) == 0 {
229285
return ""
230286
}
231287
parts := make([]string, 0, len(inputs))
232-
for _, in := range inputs {
233-
parts = append(parts, in.Name+" "+im.QualifyType(in.Type))
288+
for i, in := range inputs {
289+
parts = append(parts, ns.inputs[i]+" "+im.QualifyType(in.Type))
234290
}
235291
return strings.Join(parts, ", ")
236292
}
237293

238-
func formatArgs(inputs []plan.Input) string {
239-
parts := make([]string, 0, len(inputs))
240-
for _, in := range inputs {
241-
parts = append(parts, in.Name)
242-
}
243-
return strings.Join(parts, ", ")
294+
func formatArgs(ns names) string {
295+
return strings.Join(ns.inputs, ", ")
244296
}

internal/emit/emit_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,38 @@ type Container struct {
377377
}
378378
}
379379

380+
func TestEmit_VarNameDoesNotShadowImportedPackage(t *testing.T) {
381+
t.Parallel()
382+
383+
// Single-file fixture where the result type's lowercased name ("db",
384+
// "context") collides with a top-level identifier that mimics a
385+
// package import: declaring a local `db` would break any later
386+
// db.Open(...) call. Inject the reserved aliases into the Imports
387+
// tracker by hand to mirror the multi-file scenario.
388+
src := `package myapp
389+
type DB struct{}
390+
type Context struct{}
391+
func NewDB() *DB { return nil }
392+
func NewContext() Context { return Context{} }
393+
type Container struct {
394+
DB *DB ` + "`inject:\"with=NewDB\"`" + `
395+
Ctx Context ` + "`inject:\"\"`" + `
396+
}
397+
`
398+
p := buildPlan(t, src)
399+
400+
// Reserve "db" and "context" as if those were import aliases visible
401+
// to the generated file. The assignNames pass must avoid them.
402+
im := emit.New("myapp", "db", "context")
403+
got := emit.RenderForTest(t, im, p)
404+
405+
for _, bad := range []string{"\tdb :=", "\tcontext :="} {
406+
if strings.Contains(got, bad) {
407+
t.Errorf("generated body shadows reserved alias (matched %q):\n%s", bad, got)
408+
}
409+
}
410+
}
411+
380412
func TestEmit_NonBlankArgStoresInField(t *testing.T) {
381413
t.Parallel()
382414

internal/emit/export_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package emit
2+
3+
import (
4+
"bytes"
5+
"testing"
6+
7+
"github.com/mickamy/injector/internal/plan"
8+
)
9+
10+
// RenderForTest collects imports for p into the supplied tracker (whose
11+
// reserved aliases the caller has already set up) and renders just the
12+
// container constructor body. It is meant for tests that need to inject
13+
// reserved import names without round-tripping through Emit.
14+
func RenderForTest(t *testing.T, im *Imports, p plan.Plan) string {
15+
t.Helper()
16+
collectImports(im, p)
17+
var buf bytes.Buffer
18+
if err := writeContainer(&buf, im, p); err != nil {
19+
t.Fatalf("writeContainer: %v", err)
20+
}
21+
return buf.String()
22+
}

0 commit comments

Comments
 (0)