Skip to content

refactor(cfn-lang-ext): extract shared Mapping-name primitives for Fn::ForEach#9018

Open
bnusunny wants to merge 1 commit into
developfrom
fix/9005-followup-shared-mapping-primitives
Open

refactor(cfn-lang-ext): extract shared Mapping-name primitives for Fn::ForEach#9018
bnusunny wants to merge 1 commit into
developfrom
fix/9005-followup-shared-mapping-primitives

Conversation

@bnusunny
Copy link
Copy Markdown
Contributor

Summary

Follow-up to #9009 (issue #9005). Closes the second class of structural divergence the bot reviews on that PR repeatedly caught: the Mapping name, Fn::FindInMap lookup key, and compound Mapping table key for nested Fn::ForEach were each constructed inline in two places — once in the build-time pipeline (`samcli/commands/build/build_context.py`) and once in the package-time pipeline (`samcli/lib/package/language_extensions_packaging.py`). Bot review #2 caught a missing prefix list; bot review #3 caught a collision counter keyed on the dotted path. Both bugs were duplicate string-construction logic drifting between sites.

This PR extracts the three computations into a single module so neither pipeline can drift from the other again.

Changes

  • New module `samcli/lib/cfn_language_extensions/foreach_mapping_helpers.py` with three pure functions:
    • `compute_mapping_name(leaf, nesting_path, *, has_collision, resource_template_key)` — builds `SAM<nesting_path>[]` with collision-aware suffix.
    • `compute_lookup_key(loop_variable, referenced_outer_vars)` — builds the second arg of Fn::FindInMap (`{Ref}` or `{Fn::Join}`).
    • `compute_compound_mapping_key(outer_values, inner_value)` — dash-joined Mapping table key for nested Fn::ForEach.
  • New test file `tests/unit/lib/cfn_language_extensions/test_foreach_mapping_helpers.py` with 19 tests, including:
    • Parameterized construction cases for each primitive.
    • Cross-pipeline equivalence test — asserts that the build-side collision-counting input and the package-side collision-counting input produce byte-identical Mapping names for the same logical inputs. This is the structural guarantee that future drift can't recur.
  • Replace package-side inline constructions in `samcli/lib/package/language_extensions_packaging.py`:
    • `_compute_mapping_name` body becomes a 3-line delegation to `compute_mapping_name`.
    • `_replace_dynamic_artifact_with_findmap` default `mapping_name` and inline `lookup_key` construction become helper calls.
    • `_generate_artifact_mappings` inline compound-key construction becomes a helper call.
  • Replace build-side inline constructions in `samcli/commands/build/build_context.py`:
    • `_update_foreach_artifact_paths` inline `mapping_name` and `lookup_key` constructions become helper calls; collapses two if/else branches in the process.
    • `_collect_nested_mapping_entry` inline compound-key construction becomes a helper call.
    • Removes the now-unused `sanitize_resource_key_for_mapping` import.

What this PR does NOT do

  • Does not unify orchestration. The build walker stays a recursive walker; the package detect-then-emit pipeline stays a detect-then-emit pipeline. Both still exist; they just compute Mapping identifiers through one shared source of truth.
  • Does not touch auto-dependency-layer Mapping emission (`_collect_foreach_layer_mappings` in `build_context.py:670`). That stays in `build_context.py` interleaved with the artifact loop. Its Mapping name (`SAMLayers<nesting_path>`) is constructed inline at `build_context.py:666` and is intentionally not affected.
  • Does not change `DynamicArtifactProperty` or `PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES`. No detection changes.
  • Does not consolidate the value extractors (`_get_artifact_value` vs `_find_artifact_uri_for_resource`). Their normalization shapes are genuinely different (raw string vs URI dict→string).

Test plan

  • Baseline (clean develop): `9191 passed, 25 skipped, 28 subtests passed`
  • After this PR: `9211 passed, 25 skipped, 28 subtests passed` — net +20 tests, 0 regressions
  • Targeted suite (`cfn_language_extensions`, `package`, `buildcmd`): 2052 passed
  • `ruff check samcli schema` clean
  • `mypy --no-incremental setup.py samcli tests schema` clean
  • `black --check` clean on all touched files
  • Leaf-collision regression tests from fix(cfn-lang-ext): unify packageable resource properties (#9005) #9009 commit `5e818202f` still pass:
    • `test_leaf_collision_across_resource_types_disambiguates_mapping_name` (build path)
    • `test_generate_artifact_mappings_disambiguates_leaf_collision_across_types` (package path)
  • End-to-end Sam deploy fails on missing /tmp folder files #9005 repro: `samdev package --resolve-s3` against `language-extensions/tc-026-nested-application`. Output template still rewrites `Properties.Location` to an `https://s3.us-west-2.amazonaws.com/.../...template\` URL. Behavior unchanged.

Relationship to PR #9017

Independent. PR #9017 consolidates the copy helpers and relocates the prop-name helpers into a public module. PR B (this one) extracts the Mapping-name primitives. They touch different functions in the same files; the merge order doesn't matter.

…::ForEach

Follow-up to #9009 (issue #9005). Closes the second class of structural
divergence the bot reviews on that PR repeatedly caught: the Mapping name,
Fn::FindInMap lookup key, and compound Mapping table key for nested
Fn::ForEach were each constructed inline in two places — once in the build-
time pipeline (samcli/commands/build/build_context.py) and once in the
package-time pipeline (samcli/lib/package/language_extensions_packaging.py).
Bot review #2 caught the missing prefix list; bot review #3 caught the
collision counter keyed on the dotted path. Both bugs were duplicate
string-construction logic drifting between sites.

This PR extracts the three computations into a single module:
samcli/lib/cfn_language_extensions/foreach_mapping_helpers.py with three
pure functions:

- compute_mapping_name(leaf, nesting_path, *, has_collision, resource_template_key)
- compute_lookup_key(loop_variable, referenced_outer_vars)
- compute_compound_mapping_key(outer_values, inner_value)

Both pipelines now call these helpers; neither constructs the strings
inline. The cross-pipeline equivalence test in
tests/unit/lib/cfn_language_extensions/test_foreach_mapping_helpers.py
asserts that build's collision-counting input and package's collision-
counting input produce byte-identical Mapping names for the same logical
inputs — locking in the structural guarantee.

Mechanical changes:
- samcli/lib/cfn_language_extensions/foreach_mapping_helpers.py (new) — three
  pure helpers + module docstring + __all__.
- tests/unit/lib/cfn_language_extensions/test_foreach_mapping_helpers.py
  (new) — 19 tests including parameterized construction cases and three
  cross-pipeline equivalence cases.
- samcli/lib/package/language_extensions_packaging.py — _compute_mapping_name
  body becomes a 3-line delegation; _replace_dynamic_artifact_with_findmap's
  default mapping_name and inline lookup_key construction become helper
  calls; _generate_artifact_mappings's inline compound-key construction
  becomes a helper call.
- samcli/commands/build/build_context.py — _update_foreach_artifact_paths's
  inline mapping_name and lookup_key constructions become helper calls;
  _collect_nested_mapping_entry's inline compound-key construction becomes
  a helper call. Collapses two if/else branches in the process. Removes
  the now-unused sanitize_resource_key_for_mapping import.

What this PR does NOT do:
- It does not unify orchestration. The build walker stays a recursive
  walker; the package detect-then-emit pipeline stays a detect-then-emit
  pipeline. They just compute Mapping identifiers through one shared
  source of truth.
- It does not touch auto-dependency-layer Mapping emission
  (_collect_foreach_layer_mappings). That stays in build_context.py.
- It does not change DynamicArtifactProperty or
  PACKAGEABLE_RESOURCE_ARTIFACT_PROPERTIES. No detection changes.

Verification:
- baseline 9191 passed, 25 skipped → after 9211 passed, 25 skipped (+20)
- targeted: 2052 passed across cfn_language_extensions, package, build
- ruff check, mypy, black --check all clean
- end-to-end #9005 repro (sam package against
  language-extensions/tc-026-nested-application) still rewrites
  Properties.Location to an https://s3... URL
- leaf-collision regression tests from #9009 commit 5e81820 still pass
@bnusunny bnusunny requested a review from a team as a code owner May 15, 2026 17:31
@github-actions github-actions Bot added area/build sam build command pr/internal labels May 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/build sam build command pr/internal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant