-
Notifications
You must be signed in to change notification settings - Fork 0
feat(plan): wire-style variable naming (field name, type fallback, cascade suffix) #7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1025d99
d347b32
ea4ef55
4d4e52a
87e5f01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -144,6 +144,8 @@ func Build(c ir.Container, idx *Index, opts Options) (Plan, []diag.Diag) { | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| renameOutputSteps(r.steps, outputs) | ||||||
|
|
||||||
| returnsErr := false | ||||||
| for _, s := range r.steps { | ||||||
| if s.Kind == StepKindProvider && s.Provider != nil && s.Provider.ReturnsError { | ||||||
|
|
@@ -612,6 +614,10 @@ func deriveInputName(t types.Type) string { | |||||
| if t == nil { | ||||||
| return "arg" | ||||||
| } | ||||||
| // Resolve Go 1.22+ type aliases (`type Client = valkey.Client`) so | ||||||
| // the alias's own name flows through instead of falling out to the | ||||||
| // "arg" sentinel. | ||||||
| t = types.Unalias(t) | ||||||
| if ptr, ok := t.(*types.Pointer); ok { | ||||||
| return deriveInputName(ptr.Elem()) | ||||||
| } | ||||||
|
|
@@ -623,6 +629,80 @@ func deriveInputName(t types.Type) string { | |||||
| return "arg" | ||||||
| } | ||||||
|
|
||||||
| // renameOutputSteps renames non-input steps that produce a container | ||||||
| // output to lowerFirst(field name). This puts the field's own identifier | ||||||
| // at the call site (`tx := tx.New(...)` for a `Tx` field, suffixed if it | ||||||
| // would shadow an existing step name). Steps that are not bound to any | ||||||
| // output keep the type-derived name picked at resolution time. | ||||||
| // | ||||||
| // Renaming runs in two passes so that an output whose desired base name | ||||||
| // is currently held by another step that is also about to be renamed can | ||||||
| // claim the now-vacated name without unnecessarily suffixing. Without | ||||||
| // this, a hypothetical swap (step A holds "foo" and wants "db", step B | ||||||
| // holds "db" and wants "foo") would land on `foo2`/`db` instead of the | ||||||
| // clean `foo`/`db`. | ||||||
| func renameOutputSteps(steps []Step, outputs []Output) { | ||||||
| used := make(map[string]bool, len(steps)) | ||||||
| for _, st := range steps { | ||||||
| used[st.VarName] = true | ||||||
| } | ||||||
|
|
||||||
| type pendingRename struct { | ||||||
| stepIdx int | ||||||
| base string | ||||||
| } | ||||||
| var renames []pendingRename | ||||||
| // A shared step (bound to more than one container field, e.g. when two | ||||||
| // fields request the same dependency) appears multiple times in | ||||||
| // outputs. Decide the rename for each step at the first valid output | ||||||
| // and skip any later occurrences so we don't queue the same step | ||||||
| // twice, which would leak the dropped candidate name into `used` and | ||||||
| // force unrelated steps onto a suffix. | ||||||
| decided := make(map[int]bool, len(outputs)) | ||||||
| for _, o := range outputs { | ||||||
| if o.StepIndex < 0 || o.StepIndex >= len(steps) { | ||||||
| continue | ||||||
| } | ||||||
| if decided[o.StepIndex] { | ||||||
| continue | ||||||
| } | ||||||
| s := &steps[o.StepIndex] | ||||||
| if s.Kind == StepKindInput { | ||||||
| decided[o.StepIndex] = true | ||||||
| continue | ||||||
| } | ||||||
| base := lowerFirst(o.FieldName) | ||||||
| if base == "" { | ||||||
| continue | ||||||
| } | ||||||
| if base == s.VarName { | ||||||
| // Existing name already lines up with this field; no rename | ||||||
| // needed even if later outputs would have picked a different | ||||||
| // base. | ||||||
| decided[o.StepIndex] = true | ||||||
| continue | ||||||
| } | ||||||
| delete(used, s.VarName) | ||||||
| renames = append(renames, pendingRename{stepIdx: o.StepIndex, base: base}) | ||||||
| decided[o.StepIndex] = true | ||||||
| } | ||||||
|
|
||||||
| for _, r := range renames { | ||||||
| pick := r.base | ||||||
| if used[pick] { | ||||||
| for i := 2; ; i++ { | ||||||
| try := fmt.Sprintf("%s%d", r.base, i) | ||||||
| if !used[try] { | ||||||
| pick = try | ||||||
| break | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| steps[r.stepIdx].VarName = pick | ||||||
| used[pick] = true | ||||||
| } | ||||||
| } | ||||||
|
Comment on lines
+644
to
+704
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of We can optimize this to func renameOutputSteps(steps []Step, outputs []Output) {
used := make(map[string]bool, len(steps))
for _, st := range steps {
used[st.VarName] = true
}
for _, o := range outputs {
if o.StepIndex < 0 || o.StepIndex >= len(steps) {
continue
}
s := &steps[o.StepIndex]
if s.Kind == StepKindInput {
continue
}
base := lowerFirst(o.FieldName)
if base == "" || base == s.VarName {
continue
}
delete(used, s.VarName)
pick := base
if used[pick] {
for i := 2; ; i++ {
try := fmt.Sprintf("%s%d", base, i)
if !used[try] {
pick = try
break
}
}
}
s.VarName = pick
used[pick] = true
}
}
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call — applied in
Comment on lines
+644
to
+704
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current implementation of Using a two-pass approach resolves this: first, identify all steps that will be renamed and free up their old names from the func renameOutputSteps(steps []Step, outputs []Output) {
used := make(map[string]bool, len(steps))
for _, st := range steps {
used[st.VarName] = true
}
type renameInfo struct {
base string
lastIndex int
}
renameInfos := make(map[int]renameInfo)
for i, o := range outputs {
if o.StepIndex < 0 || o.StepIndex >= len(steps) {
continue
}
s := &steps[o.StepIndex]
if s.Kind == StepKindInput {
continue
}
base := lowerFirst(o.FieldName)
if base == "" || base == s.VarName {
continue
}
renameInfos[o.StepIndex] = renameInfo{
base: base,
lastIndex: i,
}
}
for stepIdx := range renameInfos {
delete(used, steps[stepIdx].VarName)
}
for i, o := range outputs {
info, ok := renameInfos[o.StepIndex]
if !ok || info.lastIndex != i {
continue
}
s := &steps[o.StepIndex]
pick := info.base
if used[pick] {
for i := 2; ; i++ {
try := fmt.Sprintf("%s%d", info.base, i)
if !used[try] {
pick = try
break
}
}
}
s.VarName = pick
used[pick] = true
}
}
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied in var renames []pendingRename
for _, o := range outputs {
// …skip irrelevant outputs…
delete(used, s.VarName)
renames = append(renames, pendingRename{stepIdx: o.StepIndex, base: base})
}
for _, r := range renames {
pick := r.base
if used[pick] { /* suffix loop */ }
steps[r.stepIdx].VarName = pick
used[pick] = true
}Pass 1 vacates every old name first, pass 2 picks the new ones — so a swap ( |
||||||
|
|
||||||
| func varNameForEmbed(es embedSource, existing []Step) string { | ||||||
| // FieldName may be a dotted selector (e.g. "CommonInfra.DB") when the | ||||||
| // source comes from a promoted field; only the leaf segment is a valid | ||||||
|
|
@@ -651,11 +731,21 @@ func varNameForEmbed(es embedSource, existing []Step) string { | |||||
| } | ||||||
|
|
||||||
| func varNameForProvider(p *ir.Provider, existing []Step) string { | ||||||
| base := p.FuncName | ||||||
| if strings.HasPrefix(base, "New") && len(base) > 3 { | ||||||
| base = base[3:] | ||||||
| // Name the variable after what the call produces, not after the | ||||||
| // constructor function. `db.Open(...) *sql.DB` reads more naturally | ||||||
| // as `db := db.Open(...)` than `open := db.Open(...)`, and | ||||||
| // container-field-bound steps later get renamed once more to the | ||||||
| // destination field name. | ||||||
| base := deriveInputName(p.Result) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If a provider returns a type alias,
Suggested change
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applied in func deriveInputName(t types.Type) string {
if t == nil { return "arg" }
t = types.Unalias(t)
// …Pointer / Named handling as before…
}Covered by |
||||||
| if base == "arg" { | ||||||
| // Anonymous or unnamed result type — fall back to the function | ||||||
| // name for a less generic label than "arg". | ||||||
| base = p.FuncName | ||||||
| if strings.HasPrefix(base, "New") && len(base) > 3 { | ||||||
| base = base[3:] | ||||||
| } | ||||||
| base = lowerFirst(base) | ||||||
| } | ||||||
| base = lowerFirst(base) | ||||||
| if base == "" { | ||||||
| base = "v" | ||||||
| } | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When multiple container fields are bound to the same shared dependency (which resolves to a single step),
outputswill contain multiple entries pointing to the sameStepIndex.In the current implementation, this causes the step to be added to
renamesmultiple times with different base names (e.g.,fooandbar). During the renaming loop, the step'sVarNameis overwritten multiple times (the last one wins), but all intermediate candidate names are marked asusedin theusedmap.This leaks unused names (like
foo) into theusedmap, which can cause other unrelated steps that naturally want those names to be unnecessarily suffixed (e.g., forced to usefoo2).We can fix this by deduplicating the renames per
StepIndexwhile preserving the deterministic order of renaming.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Applied in
87e5f01, with a small extension over the suggested form. Same core fix — only the first valid candidate for a givenStepIndexis queued, so a step shared across two output fields no longer leaks its dropped candidate name intoused.The extra wrinkle: the gate also catches the "first base already matches the existing VarName" case, so a shared step whose current name aligns with the first field is left alone instead of being renamed to a different field's name on the second visit. Example:
Without the gate, the variable ends up as
backup, and the struct literal readsDB: backup, Backup: backup. With the gate, the variable staysdband aligns with at least the first field.Two regression tests added:
TestBuild_SharedStepDoesNotLeakCandidate(the leak you described — extraTwin *DBfield, makes sure an unrelatedBackupstep still gets its natural name) andTestBuild_SharedStepPreservesMatchingFieldName(the matching-base case above).