Skip to content

fix(deployment): Fn::FindInMap DefaultValue + Fn::Sub escape + Fn::GetAtt dynamic attribute name - #852

Merged
go-to-k merged 1 commit into
mainfrom
test/intrinsics-torture-2
Jun 14, 2026
Merged

fix(deployment): Fn::FindInMap DefaultValue + Fn::Sub escape + Fn::GetAtt dynamic attribute name#852
go-to-k merged 1 commit into
mainfrom
test/intrinsics-torture-2

Conversation

@go-to-k

@go-to-k go-to-k commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes three real cdkd intrinsic-function bugs, each surfaced by the new intrinsics-torture-2 integ fixture, plus unit tests for every case. Validated green against real AWS (deploy + destroy clean).

Root causes and fixes

1. Fn::FindInMap 4th-arg DefaultValue ignored

CloudFormation supports an optional 4th argument { DefaultValue: <value> } on Fn::FindInMap: when the requested top-level OR second-level key is absent, CFn returns the DefaultValue instead 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, detect DefaultValue presence (distinguishing "no 4th arg" from a 4th arg whose DefaultValue is intentionally undefined/null), and return the resolved DefaultValue on each missing-key path (no Mappings section, missing map, missing top-level key, missing second-level key). The DefaultValue may itself be an intrinsic, so it is resolved lazily only when the fallback is taken. Without a DefaultValue the missing-key cases still throw (backward compatible).

2. Fn::Sub ${!Literal} escape not honored

Per 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 sequential String.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 phantom DependsOn / Ref edge.

3. Fn::GetAtt with an intrinsic/Ref attribute name crashed

CloudFormation allows the Fn::GetAtt attribute 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 with attributeName.includes is not a function on 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 for FindInMap DefaultValue (present + each missing-key path + intrinsic default), Fn::Sub ${!Literal} escape (literal emission, no substitution, mixed escaped/real tokens), and Fn::GetAtt dynamic 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-2 integ fixture was deployed and destroyed cleanly against real AWS (no leftover resources).

…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
go-to-k force-pushed the test/intrinsics-torture-2 branch from 213f686 to f91e437 Compare June 14, 2026 01:20
@go-to-k

go-to-k commented Jun 14, 2026

Copy link
Copy Markdown
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).

@go-to-k
go-to-k merged commit b285bab into main Jun 14, 2026
5 checks passed
@go-to-k
go-to-k deleted the test/intrinsics-torture-2 branch June 14, 2026 01:23
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))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.221.7 🎉

The release is available on:

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant