Skip to content

Commit d25423f

Browse files
Add "did you mean?" suggestions for variable reference typos
When a ${...} variable reference fails to resolve, the error now suggests the closest valid path using Levenshtein distance. The algorithm walks the entire path greedily, correcting multiple segments in one suggestion. Co-authored-by: Isaac
1 parent d151b00 commit d25423f

14 files changed

Lines changed: 820 additions & 63 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
bundle:
2+
name: did-you-mean
3+
4+
variables:
5+
my_cluster_id:
6+
default: "abc-123"
7+
8+
resources:
9+
jobs:
10+
my_job:
11+
name: "test-${var.my_clster_id}"

acceptance/bundle/variables/did-you-mean/out.test.toml

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Error: reference does not exist: ${var.my_clster_id}. Did you mean ${var.my_cluster_id}?
2+
3+
Name: did-you-mean
4+
Target: default
5+
Workspace:
6+
User: [USERNAME]
7+
Path: /Workspace/Users/[USERNAME]/.bundle/did-you-mean/default
8+
9+
Found 1 error
10+
11+
Exit code: 1
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
$CLI bundle validate

bundle/config/mutator/resolve_variable_references.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var defaultPrefixes = []string{
4444
}
4545

4646
var artifactPath = dyn.MustPathFromString("artifacts")
47+
var resourcesPath = dyn.MustPathFromString("resources")
4748

4849
type resolveVariableReferences struct {
4950
prefixes []string
@@ -202,6 +203,8 @@ func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn
202203
//
203204
normalized, _ := convert.Normalize(b.Config, root, convert.IncludeMissingFields)
204205

206+
suggestFn := m.makeSuggestFn(normalized, prefixes, varPath)
207+
205208
// If the pattern is nil, we resolve references in the entire configuration.
206209
root, err := dyn.MapByPattern(root, m.pattern, func(p dyn.Path, v dyn.Value) (dyn.Value, error) {
207210
// Resolve variable references in all values.
@@ -235,8 +238,50 @@ func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn
235238
}
236239
}
237240

241+
// For references starting with "resources" that are not in
242+
// the resolution prefixes: validate the path against the
243+
// normalized tree. If invalid, emit a warning with a
244+
// suggestion. Either way, skip resolution (resources are
245+
// resolved later by terraform).
246+
if path.HasPrefix(resourcesPath) {
247+
_, lookupErr := m.lookupFn(normalized, path, b)
248+
if lookupErr != nil && dyn.IsNoSuchKeyError(lookupErr) {
249+
key := rewriteToVarShorthand(path.String())
250+
msg := fmt.Sprintf("reference does not exist: ${%s}", key)
251+
if suggestion := suggestFn(key); suggestion != "" {
252+
msg += fmt.Sprintf(". Did you mean ${%s}?", suggestion)
253+
}
254+
diags = diags.Append(diag.Diagnostic{
255+
Severity: diag.Warning,
256+
Summary: msg,
257+
})
258+
}
259+
return dyn.InvalidValue, dynvar.ErrSkipResolution
260+
}
261+
262+
// Check for prefix typos before skipping. If the first
263+
// component is close to a valid prefix, emit a warning
264+
// with a suggestion. The reference is left unresolved to
265+
// avoid breaking existing behavior.
266+
if len(path) > 0 {
267+
firstKey := path[0].Key()
268+
prefixNames := m.suggestPrefixNames(prefixes)
269+
best, dist := closestMatch(firstKey, prefixNames)
270+
if best != "" && dist > 0 {
271+
corrected := make(dyn.Path, len(path))
272+
copy(corrected, path)
273+
corrected[0] = dyn.Key(best)
274+
suggestion := rewriteToVarShorthand(corrected.String())
275+
key := rewriteToVarShorthand(path.String())
276+
diags = diags.Append(diag.Diagnostic{
277+
Severity: diag.Warning,
278+
Summary: fmt.Sprintf("reference does not exist: ${%s}. Did you mean ${%s}?", key, suggestion),
279+
})
280+
}
281+
}
282+
238283
return dyn.InvalidValue, dynvar.ErrSkipResolution
239-
})
284+
}, dynvar.WithSuggestFn(suggestFn))
240285
})
241286
if err != nil {
242287
return dyn.InvalidValue, err

0 commit comments

Comments
 (0)