fix(deployment): Fn::FindInMap DefaultValue + Fn::Sub escape + Fn::GetAtt dynamic attribute name - #852
Merged
Merged
Conversation
…tValue, Sub ${!} escape, dynamic GetAtt attr)
Resolve three intrinsic-function edge cases surfaced by harder argument shapes:
- Fn::FindInMap: support the optional 4th argument DefaultValue, returned when
the map/top-level/second-level key is not found instead of erroring.
- Fn::Sub: handle the ${!Literal} escape sequence by emitting a literal
${Literal} with no substitution and no phantom dependency edge.
- Fn::GetAtt: resolve a dynamic (intrinsic) attribute name before performing
string operations on it.
Add the intrinsics-torture-2 integration fixture exercising these harder
intrinsic arg-shapes, plus unit coverage in the template parser and intrinsic
resolver. Regenerate the integ-coverage / scenario-coverage matrices.
go-to-k
force-pushed
the
test/intrinsics-torture-2
branch
from
June 14, 2026 01:20
213f686 to
f91e437
Compare
Owner
Author
|
Independent code review complete (pr-code-reviewer): all 3 intrinsic fixes verified clean, no blockers — FindInMap 4th-arg DefaultValue guard robust, Fn::Sub ${!} escape single-pass-correct + dependency-exclusion, Fn::GetAtt dynamic attribute-name resolved-before-string-ops with clear throw. No regressions. Plus unit tests + real-AWS integ PASS. Setting pr-review bound to f91e437 (3-axis tier; per CLAUDE.md bias-down for tightly-scoped bug fixes with reproducing fixtures, 1 strong code review + full unit/integ validation). |
github-actions Bot
pushed a commit
that referenced
this pull request
Jun 14, 2026
## [0.221.7](v0.221.6...v0.221.7) (2026-06-14) ### Bug Fixes * **deployment:** Fn::FindInMap DefaultValue + Fn::Sub escape + Fn::GetAtt dynamic attribute name ([#852](#852)) ([b285bab](b285bab))
|
🎉 This PR is included in version 0.221.7 🎉 The release is available on: Your semantic-release bot 📦🚀 |
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.
Summary
Fixes three real cdkd intrinsic-function bugs, each surfaced by the new
intrinsics-torture-2integ fixture, plus unit tests for every case. Validated green against real AWS (deploy + destroy clean).Root causes and fixes
1.
Fn::FindInMap4th-argDefaultValueignoredCloudFormation supports an optional 4th argument
{ DefaultValue: <value> }onFn::FindInMap: when the requested top-level OR second-level key is absent, CFn returns theDefaultValueinstead of failing. cdkd's resolver only accepted the 3-arg form and threw on any missing key, breaking templates that rely on the default-fallback.Fix (
src/deployment/intrinsic-function-resolver.ts): accept the optional 4th argument, detectDefaultValuepresence (distinguishing "no 4th arg" from a 4th arg whoseDefaultValueis intentionallyundefined/null), and return the resolvedDefaultValueon each missing-key path (no Mappings section, missing map, missing top-level key, missing second-level key). TheDefaultValuemay itself be an intrinsic, so it is resolved lazily only when the fallback is taken. Without aDefaultValuethe missing-key cases still throw (backward compatible).2.
Fn::Sub${!Literal}escape not honoredPer the CloudFormation spec, a
${immediately followed by!is an escape:${!X}renders as the literal text${X}with no variable substitution, and is NOT a reference. cdkd substituted it anyway and additionally created a phantom dependency / Ref edge from the escaped token.Fix:
src/deployment/intrinsic-function-resolver.ts: the substitution regex now captures the optional leading!; escaped tokens emit${X}verbatim and never reach variable / Ref / GetAtt resolution. Replacements are applied in a single left-to-right pass over the same regex (consuming the pre-collected replacements positionally) to avoid the first-occurrence clobber hazard of a sequentialString.replace(match, ...)loop — e.g. an escaped${!X}producing literal${X}that a later real${X}would otherwise overwrite.src/analyzer/template-parser.ts: the dependency-extraction regex likewise skips the escaped form so an escaped token contributes no phantomDependsOn/Refedge.3.
Fn::GetAttwith an intrinsic/Refattribute name crashedCloudFormation allows the
Fn::GetAttattribute name (2nd array element) to be any string-valued expression, including an intrinsic such as{Ref: AttrNameParam}or{Fn::Sub: ...}. cdkd assumed it was always a literal string and ran string operations (.includes,.split, the nested-path walk, the per-type switch) directly on it, crashing withattributeName.includes is not a functionon a non-string.Fix (
src/deployment/intrinsic-function-resolver.ts): resolve the attribute name first (before any string ops); if it does not resolve to a string, throw a clear error naming the logical id and the actual type. The logical id (1st element) must be a static string per CFn, so it is not resolved.Tests
tests/unit/deployment/intrinsic-functions.test.ts: unit coverage forFindInMapDefaultValue (present + each missing-key path + intrinsic default),Fn::Sub${!Literal}escape (literal emission, no substitution, mixed escaped/real tokens), andFn::GetAttdynamic attribute name (intrinsic name resolved + non-string error).tests/unit/analyzer/template-parser.test.ts:${!X}contributes no dependency edge.tests/integration/intrinsics-torture-2/: new integ fixture exercising all three behaviors end to end (deploy + destroy). Validated green against real AWS.Validation
Unit tests, typecheck, lint, build all pass. The
intrinsics-torture-2integ fixture was deployed and destroyed cleanly against real AWS (no leftover resources).