feat!: a variable without a default must resolve - #44
Merged
Conversation
`{{ name }}` was optional: a missing or misspelled variable substituted an empty
string and the run continued. Against the released 0.2.0 PHAR, both execution
paths:
$ctx->shell('echo rm -rf {{ deployPath }}/'); > echo rm -rf ''/ -> rm -rf /
$ctx->exec(['echo', 'rm', '-rf', '{{ deployPath }}/']); -> rm -rf /
exit 0, no warning
deployPath was never defined. A typo in a variable name produced a destructive
command that reported success, and the same typo in a template produced a config
file with a blank value that looked fine.
Now a variable must resolve unless the template says what to use instead:
{{ name }} required - throws MissingVariableException
{{ name | "" }} deliberately empty
{{ name | "x" }} default, unchanged
{{! name }} still accepted, now redundant
The rule lives in one place, Token::isRequired(): required unless it has a
default. Because the parser already distinguishes "no default" from "empty
default" by capture offset, `| ""` was a working opt-in before this change; it
just had nothing to opt out of.
The error names the escape hatch, so migrating does not mean looking up syntax:
Missing required variable: deployPath (use {{ deployPath | "" }} if an empty
value is intended)
BREAKING: templates and commands that relied on a missing variable rendering
empty must now say so with `| ""`. Five tests encoded the old behaviour and were
rewritten rather than deleted - each now pins the new rule and its explicit-empty
counterpart.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This came out of external review of 0.2.0. The reviewer's point was narrower — migrating
shellRaw()toshell()silently changes a command containing literal{{— and following it up found the general case, which is worse.What 0.2.0 does
{{ name }}was optional: a missing or misspelled variable substituted an empty string and the run continued. Against the released 0.2.0 PHAR, withdeployPathnever defined:echostands in forrmhere; the substitution is real. A typo in a variable name produces a destructive command that reports success, on both execution paths. The same typo in a template produces a config file with a blank value that looks fine.This is the same failure mode the config validation in #40/#42 removed one layer up: a typo silently becomes an empty value and the run looks successful. Leaving it in the template layer while fixing it in the config layer would have been inconsistent.
The rule
It lives in one place,
Token::isRequired(): required unless it has a default. The parser already distinguished "no default" from "empty default" by capture offset, so| ""worked before this change — it just had nothing to opt out of.Migration is in the error message
Naming the escape hatch is the difference between a five-second fix and a docs hunt. For template files the message also carries the path.
Breaking
Templates and commands that relied on a missing variable rendering empty must now say so with
| "". Consumers should expect to hit this once per genuinely-optional value — and, hopefully, once per typo they did not know they had.Five tests encoded the old behaviour. They were rewritten rather than deleted, each now pinning the new rule and its explicit-empty counterpart, so a future change cannot quietly restore the old default:
testMissingVariableWithoutADefaultThrows/testAnExplicitEmptyDefaultIsHowYouAskForEmptytestMissingVariableThrowsAndRunsNothing/testAnExplicitEmptyDefaultStillBecomesAnEmptyArgumenttestMissingVariableThrowsInsteadOfRenderingEmpty/testEmptyIsAskedForWithAnExplicitDefaultOne mock needed
has()stubbed alongsideresolve(), which is a fair reflection of the stricter contract.Verified
And against the binary, same task, before and after:
Not in this PR: the reserved-name behaviour (a project task named
initcurrently kills the whole CLI), which is the other accepted review finding and a separate change.🤖 Generated with Claude Code
https://claude.ai/code/session_018CTvnzcNYmFgm2HQcm821A