Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nextchanges/cli/did-you-mean-variables.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
Error messages for failed key lookups and variable references now suggest the closest matching key if one is found. ([#6208](https://github.com/databricks/cli/pull/6208))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
bundle:
name: reference-typo-double-nested-typo-multiple

variables:
cluster:
type: complex
default:
spark_version: "13.2.x"
clusters:
type: complex
default:
spark_version: "13.2.x"

resources:
jobs:
one:
# Two typos in a single reference across nesting layers, where the outermost
# key "clustr" is within distance 2 of both "cluster" and "clusters". The
# lookup fails at the outermost key, so both are suggested and the inner typo
# ("spark_versio") is never reached.
name: ${var.clustr.spark_versio}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
bundle:
name: reference-typo-double-nested-typo

variables:
cluster:
type: complex
default:
spark_version: "13.2.x"

resources:
jobs:
one:
# Two typos in a single reference across nesting layers: "clustr" (cluster)
# and "spark_versio" (spark_version). The lookup fails at the outermost key,
# so only "cluster" is suggested; the inner typo is never reached.
name: ${var.clustr.spark_versio}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
bundle:
name: reference-typo-multiple-typos

variables:
cluster:
type: complex
default:
spark_version: "13.2.x"
node_type_id: Standard_DS3_v2

resources:
jobs:
# Two typos in references to a complex (nested) variable. References are
# resolved in sorted order of their config path, and resolution stops at the
# first unresolved one, so only job "a" is reported even though "b" is wrong too.
a:
# Deeply nested typo: ${var.cluster.spark_versio} should be spark_version.
name: ${var.cluster.spark_versio}
b:
# Typo at the variable name: ${var.clustr.node_type_id} should be cluster.
name: ${var.clustr.node_type_id}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
bundle:
name: reference-typo-multiple

variables:
host:
default: https://example.test
hosts:
default: https://example.test

resources:
jobs:
one:
# "hst" is within edit distance 2 of both "host" and "hosts"
name: ${var.hst}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
bundle:
name: reference-typo-non-var-multiple

resources:
jobs:
one:
# A non-"var" reference. "stot_path" is within edit distance 2 of both
# workspace.root_path and workspace.state_path
name: ${workspace.stot_path}
2 changes: 2 additions & 0 deletions acceptance/bundle/variables/reference-typo/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

99 changes: 99 additions & 0 deletions acceptance/bundle/variables/reference-typo/output.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@

=== Typo of a defined variable: single suggestion

>>> [CLI] bundle validate
Error: reference does not exist: ${var.hst}

did you mean:
${var.host}
Comment on lines +5 to +8

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, this is now very clear and actionable!


Name: reference-typo
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo/default

Found 1 error

=== Typo close to two defined variables: multiple suggestions

>>> [CLI] bundle validate
Error: reference does not exist: ${var.hst}

did you mean one of:
${var.host}
${var.hosts}

Name: reference-typo-multiple
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo-multiple/default

Found 1 error

=== Typo in a non-variable reference (workspace field): multiple suggestions

>>> [CLI] bundle validate
Error: reference does not exist: ${workspace.stot_path}

did you mean one of:
${workspace.root_path}
${workspace.state_path}

Name: reference-typo-non-var-multiple
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo-non-var-multiple/default

Found 1 error

=== Nested complex variable with multiple typos: only the first is reported

>>> [CLI] bundle validate
Error: reference does not exist: ${var.cluster.spark_versio}

did you mean:
${var.cluster.spark_version}

Name: reference-typo-multiple-typos
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo-multiple-typos/default

Found 1 error

=== Two typos in one reference: lookup fails at the outermost key

>>> [CLI] bundle validate
Error: reference does not exist: ${var.clustr.spark_versio}

did you mean:
${var.cluster.spark_versio}
Comment on lines +71 to +74

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this isn't great, but not much we can do since we fail fast and traversing the suggestion in search for valid references is probably overkill


Name: reference-typo-double-nested-typo
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo-double-nested-typo/default

Found 1 error

=== Two typos in one reference, outermost close to two variables: multiple suggestions

>>> [CLI] bundle validate
Error: reference does not exist: ${var.clustr.spark_versio}

did you mean one of:
${var.cluster.spark_versio}
${var.clusters.spark_versio}

Name: reference-typo-double-nested-typo-multiple
Target: default
Workspace:
User: [USERNAME]
Path: /Workspace/Users/[USERNAME]/.bundle/reference-typo-double-nested-typo-multiple/default

Found 1 error
17 changes: 17 additions & 0 deletions acceptance/bundle/variables/reference-typo/script
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
title "Typo of a defined variable: single suggestion\n"
withdir single musterr trace $CLI bundle validate

title "Typo close to two defined variables: multiple suggestions\n"
withdir multiple musterr trace $CLI bundle validate

title "Typo in a non-variable reference (workspace field): multiple suggestions\n"
withdir non-var-multiple musterr trace $CLI bundle validate

title "Nested complex variable with multiple typos: only the first is reported\n"
withdir multiple-typos musterr trace $CLI bundle validate

title "Two typos in one reference: lookup fails at the outermost key\n"
withdir double-nested-typo musterr trace $CLI bundle validate

title "Two typos in one reference, outermost close to two variables: multiple suggestions\n"
withdir double-nested-typo-multiple musterr trace $CLI bundle validate
12 changes: 12 additions & 0 deletions acceptance/bundle/variables/reference-typo/single/databricks.yml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
bundle:
name: reference-typo

variables:
host:
default: https://example.test

resources:
jobs:
one:
# "hst" is a typo of the "host" variable defined above
name: ${var.hst}
28 changes: 27 additions & 1 deletion bundle/config/mutator/resolve_variable_references.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"slices"
"strings"

"github.com/databricks/cli/libs/dyn/merge"

Expand DownExpand Up@@ -289,12 +290,37 @@ func (m *resolveVariableReferences) resolveOnce(b *bundle.Bundle, prefixes []dyn
return root, nil
})
if err != nil {
diags = diags.Extend(diag.FromErr(err))
diags = diags.Extend(resolveErrorDiags(err))
}

return hasUpdates, diags
}

// resolveErrorDiags renders "did you mean" suggestions as a diagnostic Detail so
// libs/diag owns the multi-line formatting.
func resolveErrorDiags(err error) diag.Diagnostics {
refErr, ok := errors.AsType[*dynvar.ReferenceError](err)
if !ok || len(refErr.Suggestions) == 0 {
return diag.FromErr(err)
}

header := "did you mean:"
if len(refErr.Suggestions) > 1 {
header = "did you mean one of:"
}
var detail strings.Builder
detail.WriteString(header)
for _, ref := range refErr.Suggestions {
detail.WriteString("\n ${" + ref + "}")
}

return diag.Diagnostics{{
Severity: diag.Error,
Summary: refErr.Error(),
Detail: detail.String(),
}}
}

// selectivelyMutate applies a function to a subset of the configuration
func (m *resolveVariableReferences) selectivelyMutate(b *bundle.Bundle, fn func(value dyn.Value) (dyn.Value, error)) error {
return b.Config.Mutate(func(root dyn.Value) (dyn.Value, error) {
Expand Down
14 changes: 13 additions & 1 deletion libs/dyn/dynvar/resolve.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -37,6 +37,17 @@ func Resolve(in dyn.Value, fn Lookup) (out dyn.Value, err error) {
return resolver{in: in, fn: fn}.run()
}

// ReferenceError is returned for an unresolved variable reference. Suggestions
// are carried as data so callers (which can import libs/diag) format them.
type ReferenceError struct {
Reference string // original reference text, e.g. "var.hst"
Suggestions []string // corrected references, e.g. ["var.host", "var.hosts"]
}

func (e *ReferenceError) Error() string {
return fmt.Sprintf("reference does not exist: ${%s}", e.Reference)
}

type lookupResult struct {
v dyn.Value
err error
Expand DownExpand Up@@ -215,7 +226,8 @@ func (r *resolver) resolveKey(key string, seen []string) (dyn.Value, error) {
v, err := r.fn(p)
if err != nil {
if dyn.IsNoSuchKeyError(err) {
err = fmt.Errorf("reference does not exist: ${%s}", key)
// Carry suggestions as data; the caller formats them via libs/diag.
err = &ReferenceError{Reference: key, Suggestions: dyn.SuggestedReferences(err, key)}
}

// Cache the return value and return to the caller.
Expand Down
14 changes: 14 additions & 0 deletions libs/dyn/dynvar/resolve_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -39,6 +39,20 @@ func TestResolveNotFound(t *testing.T) {
require.ErrorContains(t, err, `reference does not exist: ${a}`)
}

func TestResolveNotFoundSuggestsCloseKey(t *testing.T) {
in := dyn.V(map[string]dyn.Value{
"host": dyn.V("example.com"),
"b": dyn.V("${hst}"),
})

_, err := dynvar.Resolve(in, dynvar.DefaultLookup(in))
require.ErrorContains(t, err, "reference does not exist: ${hst}")

var refErr *dynvar.ReferenceError
require.ErrorAs(t, err, &refErr)
assert.Equal(t, []string{"host"}, refErr.Suggestions)
}

func TestResolveWithNesting(t *testing.T) {
in := dyn.V(map[string]dyn.Value{
"a": dyn.V("${f.a}"),
Expand Down
Loading
Loading