Skip to content

Replace regex-based interpolation with character scanner, add "did you mean" suggestions#4715

Draft
shreyas-goenka wants to merge 7 commits intomainfrom
interpolation-parser-v2
Draft

Replace regex-based interpolation with character scanner, add "did you mean" suggestions#4715
shreyas-goenka wants to merge 7 commits intomainfrom
interpolation-parser-v2

Conversation

@shreyas-goenka
Copy link
Contributor

Summary

  • Replace the regex-based variable reference parser in dynvar/ref.go with a two-mode character scanner (libs/dyn/dynvar/interpolation/parse.go) that produces typed tokens (literal/reference), enabling proper error reporting for malformed references like ${foo.bar-} or ${foo..bar}
  • Add Levenshtein-based "did you mean" path suggestions when a valid-syntax reference fails to resolve (e.g., ${var.my_clster_id}did you mean ${var.my_cluster_id}?)
  • Add $$ escape sequence for producing literal ${ in output (following HCL2 precedent)
  • Add standalone WarnMalformedReferences mutator in the initialize phase that walks the config tree once to emit warnings for malformed variable references
  • Replace strings.Replace-based interpolation with token-based concatenation in the resolver

Design doc: design/interpolation-parser.md

Test plan

  • go test ./libs/dyn/dynvar/interpolation/... — parser unit tests
  • go test ./libs/dyn/dynvar/... — ref, resolve, suggest tests
  • go test ./bundle/config/mutator/... — mutator tests
  • go test ./acceptance -run TestAccept/bundle/variables/malformed_reference
  • go test ./acceptance -run TestAccept/bundle/variables/did_you_mean
  • Full acceptance test suite passes
  • CI

🤖 Generated with Claude Code

shreyas-goenka and others added 3 commits March 12, 2026 00:01
Two proposals:
1. Replace regex-based ${...} parsing with a proper two-mode character scanner
2. Add "did you mean?" suggestions for invalid variable references

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Captures the codebase map, key files, invariants, and instructions
needed for a new session to implement the two design proposals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…"did you mean" suggestions

- Replace regex in dynvar/ref.go with a two-mode character scanner that
  produces typed tokens (literal/ref), enabling proper error reporting
  for malformed variable references like ${foo.bar-} or ${foo..bar}
- Add Levenshtein-based "did you mean" suggestions when a valid reference
  fails to resolve (e.g., ${var.my_clster_id} -> did you mean ${var.my_cluster_id}?)
- Add $$ escape sequence for producing literal ${ in output
- Add standalone WarnMalformedReferences mutator in initialize phase
- Update token-based string interpolation in resolver

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…context

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@eng-dev-ecosystem-bot
Copy link
Collaborator

eng-dev-ecosystem-bot commented Mar 12, 2026

Commit: 12949c5

Run: 22982531466

Env 🔄​flaky 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
💚​ aws linux 8 7 268 783 6:07
🔄​ aws-ucws linux 2 7 7 364 698 7:58
🔄​ aws-ucws windows 2 6 7 367 696 5:38
🔄​ azure-ucws linux 2 1 9 369 694 8:28
🔄​ azure-ucws windows 2 1 9 371 692 6:48
💚​ gcp windows 2 9 269 782 5:03
16 interesting tests: 7 SKIP, 6 RECOVERED, 3 flaky
Test Name aws linux aws-ucws linux aws-ucws windows azure-ucws linux azure-ucws windows gcp windows
🔄​ TestAccept 💚​R 🔄​f 🔄​f 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/permissions 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 💚​R 💚​R 💚​R 🙈​S 🙈​S 🙈​S
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 💚​R 💚​R 💚​R
💚​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 💚​R 💚​R 💚​R
🙈​ TestAccept/bundle/resources/postgres_branches/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/update_protected 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/without_branch_id 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_endpoints/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/synced_database_tables/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🔄​ TestAccept/ssh/connect-serverless-gpu 🙈​s 🔄​f ✅​p 🔄​f 🔄​f 🙈​s
🔄​ TestAccept/ssh/connection 💚​R 💚​R 🔄​f 🔄​f 🔄​f 💚​R
Top 12 slowest tests (at least 2 minutes):
duration env testname
3:35 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:14 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:11 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:02 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:56 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:48 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:46 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:44 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:39 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:35 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:09 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:08 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

Split path validation into per-segment traversal with a small key regex
instead of one big regex for the entire path. This produces better error
messages pointing to the exact invalid segment, e.g.:
  "invalid key "bar-"" instead of "invalid path"

Also drops the unhelpful "at position N" from error messages since the
YAML file location is already reported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

1. **Silent failures** — `${foo.bar-}` silently treated as literal text with no warning.
2. **No suggestions** — `${bundle.nme}` produces "reference does not exist" with no hint.
3. **No escape mechanism** — no way to produce a literal `${` in output.
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. We cannot use all features we have in libs/structs/structpath such as key-value references tasks[task_key="x"].

reference resolves first. The old regex matched only `${var.tail}` (the
innermost pair). The new parser preserves this: when scanning for `}` inside
a reference, if another `${` is encountered, the outer `${` is abandoned
(treated as literal) and scanning restarts from the inner `${`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Is that what we want? I think we've discussed that we want to reject these constructs.

a reference, if another `${` is encountered, the outer `${` is abandoned
(treated as literal) and scanning restarts from the inner `${`.

### `$$` escape sequence
Copy link
Contributor

Choose a reason for hiding this comment

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

We've discussed in this in core eng meeting and there were a number of concerns with this. Notably, in bash it's current process.

I think least controversial option was: \$ -> $ \\ -> \

@@ -0,0 +1,207 @@
package interpolation
Copy link
Contributor

Choose a reason for hiding this comment

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

It should not be in dyn package, can be top level in libs. We might remove libs/dyn, but this will stay.

}

func TestParseEscapeDollar(t *testing.T) {
t.Run("double_dollar", func(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why use inner test here if there is no common setup?

Why not merge these into table test above?

// SuggestPath walks the reference path against root, correcting mistyped key
// components via fuzzy matching. Returns the corrected path string, or "" if
// the path cannot be corrected.
func SuggestPath(root dyn.Value, refPath dyn.Path) string {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make it independent of dyn?

Can we split suggestion into separate PR (both features are pretty big).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants