feat(varlock)!: stop reading strings as regexes - #1091
theoephraim wants to merge 1 commit into
Conversation
The breaking half of #1087. A pattern must be a regex("pattern", "flags") call; a string is no longer read as a regex anywhere. - matches (string, url, domain, nested types): any string is a schema error whose tip shows the exact regex() call. A referenced pattern (matches=$PATTERN) errors at plan time - a reference can only be a string, and an empty optional item never resolves its type parts. Any other resolver-valued option that resolves to a string errors at the final build. - remap() match values are compared exactly, like any other string. - @auditExtraPatterns() takes regex() calls only. - VSCode diagnostics extract a pattern from a regex() call only. - parseRegexLikeString and the deprecation machinery are deleted. Plugin types with their own matches setting are untouched. regex() itself is unchanged, including the guard against a /.../-wrapped argument, which is most useful right at this boundary.
|
The changes in this PR will be included in the next version bump.
|
📦 Bundle size✅ shrinks the bundle by 11.1 KB (−0.2%)
dist/ only; native binaries are versioned separately and not counted here. |
There was a problem hiding this comment.
Important
Deferred validation, migration guidance, and the VS Code test suite need fixes before this can merge.
Reviewed changes in c090f982, covering the breaking removal of implicit string-to-regex behavior across the env graph, CLI audit patterns, editor diagnostics, tests, docs, and release metadata.
- Built-in
matchesoptions: Static strings and direct references now produce schema errors, while explicitregex()calls and resolver-selected regexes remain supported. remap()semantics: Slash-wrapped strings now compare exactly, whileRegExpvalues continue to test the source value.- Audit patterns:
@auditExtraPatterns()now accepts onlyregex()results and removes its deprecation warning path. - Editor and docs: VS Code pattern extraction and public documentation now describe
regex()as the only pattern syntax. - Release metadata: Adds a major
varlockbump and minorenv-spec-languagebump for the breaking behavior change.
⚠️ The deprecation release has not shipped yet
The PR correctly states that this removal must follow the release containing #1087, but .bumpy/regex-fn-flags.md is still present at this head. Merging before that release would publish the warning and removal together, eliminating the promised migration window.
Technical details
# Preserve the staged rollout
## Affected sites
- `.bumpy/regex-fn-flags.md` - the deprecation change is still pending release
- `.bumpy/regex-strings-removed.md` - this PR schedules the breaking removal
## Required outcome
- Ship the #1087 deprecation release before merging or releasing this removal.azure/gpt-5.6-sol | 𝕏
| if (readsOptionAsRegex(name, key) && typeof val === 'string') { | ||
| pushWarningOnce(ctx.warnings, deprecatedRegexStringWarning(val, `${context} - option "${key}"`)); | ||
| if (settings[key] instanceof DeferredValue && readsOptionAsRegex(name, key) && typeof val === 'string') { | ||
| throw stringPatternError(val, `${context} - option "${key}"`); |
There was a problem hiding this comment.
This check is reached only during the final type build, but ConfigItem returns for an empty optional value before resolving deferred type parts. Consequently, matches=if(true, "^[A-Z]+$", "x") leaves an empty item valid with no errors and makes the same schema invalid only when the item gains a value; reject unsupported string-producing patterns independently of item presence.
Technical details
# Deferred string patterns bypass validation on empty items
## Affected sites
- `packages/varlock/src/env-graph/lib/type-decorator.ts:509-510` - string results are rejected only during `build(resolved)`
- `packages/varlock/src/env-graph/lib/config-item.ts:1030-1045` - empty values return before deferred type parts are resolved
## Required outcome
- An unsupported resolver-valued string pattern must invalidate the schema consistently even when the item is currently empty.
- Preserve support for resolver expressions that resolve to explicit `regex()` values.| if (sink.some((w) => w.message === warning.message && w.tip === warning.tip)) return; | ||
| sink.push(warning); | ||
| /** quote a pattern for an env-spec example - only `"` needs escaping inside a double-quoted value */ | ||
| const quoteForExample = (str: string) => `"${str.replaceAll('"', '\\"')}"`; |
There was a problem hiding this comment.
Always emitting double quotes does not preserve single-quoted patterns containing $NAME, because env-spec expands references inside double quotes. For example, matches='[$FOO]' suggests regex("[$FOO]"); pasting that suggestion produces Invalid function args instead of the original static pattern, so the replacement must use parser-safe quoting that preserves literal text.
Technical details
# Migration tip changes literal pattern semantics
## Affected sites
- `packages/varlock/src/env-graph/lib/type-decorator.ts:56-68` - `quoteForExample` always selects double quotes
- `packages/env-spec-parser/src/expand.ts:95-101` and `144-150` - double-quoted `$NAME` text is expanded, while single-quoted text is not
## Required outcome
- Every emitted `regex()` suggestion must parse as a static pattern with the same source and flags as the rejected string.| const regexLiteral = value.match(/^\/(.*)\/([dgimsuvy]*)$/s); | ||
| if (regexLiteral) return { pattern: regexLiteral[1].replaceAll('\\/', '/'), flags: regexLiteral[2] }; | ||
| return { pattern: value, flags: '' }; | ||
| return undefined; |
There was a problem hiding this comment.
This behavior change leaves four existing diagnostics tests failing because they still expect plain and slash-wrapped strings to be interpreted as patterns. Update those expectations for the new contract so bun run --filter env-spec-language test:ci passes again.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
varlock-website | c090f98 | Commit Preview URL Branch Preview URL |
Sep 15 2026, 08:45 PM |




Closes #1089. The breaking half of #1087.
Caution
Do not merge until the release containing #1087 has shipped. That release carries the deprecation warning. Merged before it, the warning and the removal land in the same version and nobody gets the migration window.
.bumpy/regex-fn-flags.mdis still in the tree, so that release has not happened yet.What changes
A pattern must be a
regex("pattern", "flags")call. A string is no longer read as a regex anywhere:matches(string, url, domain, and nested element/key types): any string --/^abc$/i,"/^abc$/i", or a plain"^abc$"-- is a schema error whose tip shows the exactregex()call to write. A pattern referenced from another variable (matches=$PATTERN) is an error at plan time: a reference can only be a string, and an empty optional item never resolves its type parts, so waiting for a value would miss it. Any other resolver-valued option that resolves to a string errors at the final build. One that resolves to aregex()(if(cond, regex(a), regex(b))) is unaffected.remap()match values are compared exactly, like any other string.remap($DIR, /usr/lib/, x)finally means what it looks like.regex()match values test as before.@auditExtraPatterns()takesregex()calls only; the quoted-string carve-out is gone.regex()call only.parseRegexLikeStringand everything that fed the deprecation warning are deleted.regex()itself is unchanged: literal string argument, optional flags, and it still rejects a/.../-wrapped argument. That guard is most useful right at this boundary -- someone migrating off/^abc$/is exactly the person who writesregex("/^abc$/")-- and can be relaxed in a later minor once the habit has died out.Plugin-registered types with their own
matchessetting are untouched; the rule is scoped to the built-in types that readmatchesas a pattern.Why
Inference from shape, at runtime, after quotes were stripped, was the source of every confusion in #1087: quoting never made a value literal,
/usr/lib/was a regex in one position and a path in another, two spellings had different slash rules, and a comma quietly broke one of them. The mechanism is removed rather than documented.Dynamic patterns (a regex built from another variable) are deliberately not supported: a validation rule supplied by the environment being validated inverts what the rule is for. A dynamic choice between static
regex()calls still works.Migration
/^abc$/ibecomesregex("^abc$", "i");"^abc$"becomesregex("^abc$"). Every error carries the exact replacement, and the warning shipped in #1087 says the same thing at every site that changes.