Skip to content

fix(conditions): extend Fn::Equals mutual-exclusion beyond AllowedValues parameters - #4805

Merged
kddejong merged 2 commits into
aws-cloudformation:mainfrom
Varshith-Kali:fix/e3014-conditions-exclusivity
Oct 23, 2026
Merged

kddejong merged 2 commits into
aws-cloudformation:mainfrom
Varshith-Kali:fix/e3014-conditions-exclusivity

Conversation

@Varshith-Kali

Copy link
Copy Markdown
Contributor

Fixes #4799

Bug

E3014 (requiredXor) fires a false positive when a resource property is guarded by two mutually exclusive Fn::Equals conditions, unless the compared parameter declares AllowedValues:

Conditions:
  UseIpv4: !Equals [!Ref Source, ipv4]
  UseIpv6: !Equals [!Ref Source, ipv6]

UseIpv4 and UseIpv6 can never both be true for any value of Source, yet the property-validation engine explored the both-true scenario and reported requiredXor on the ingress rule. The template-level engine (cfnlint/conditions/conditions.py) already excluded that scenario, so the two engines disagreed.

Root cause

The pairwise NAND that #4464 added in context/conditions/_conditions.py::create_from_instance sits inside a loop over template parameters that continues past any parameter without AllowedValues, and pseudo-parameters (AWS::Region, AWS::AccountId, ...) never enter the loop at all.

Fix

The NAND is now collected in a parameter-independent pass: any two Fn::Equals comparing the same single-valued reference (grouped by the right-hand side hash, which the Equal sort normalizes to the Ref dict) to different static strings are constrained to never both be true. This is sound for parameters with or without AllowedValues and for pseudo-parameters, since a Ref resolves to exactly one value. The AllowedValues exhaustion rule (Or(...) when every allowed value is covered) is preserved unchanged.

Test evidence

  • New engine-level regression test (test_mutually_exclusive_equals_without_allowed_values): both-true raises Unsatisfiable for a plain parameter and for AWS::AccountId, while each condition stays individually reachable.
  • New end-to-end fixture (test/fixtures/templates/good/conditions/e3014_exclusive_conditions.yaml, the issue's template A) plus a test_api.py case asserting zero matches.
  • Red/green on the fixture: without the fix, the template produces 2x E3014; with the fix, 0 matches.
  • Suites run: test/unit/module/context/conditions + test/unit/module/conditions (86 passed), test/unit/rules (1999 passed), test/unit/module (1012 passed; 4 failures in config/test_cli_args.py and maintenance/test_update_specs_from_pricing.py are pre-existing on unmodified main and unrelated).

@Varshith-Kali

Copy link
Copy Markdown
Contributor Author

Hi, the [CI] Test PR run on this PR is parked waiting for a maintainer approval (first-time-contributor gate). Could someone approve it when you get a chance? Thanks.

@kddejong
kddejong force-pushed the fix/e3014-conditions-exclusivity branch from 17af75b to a47b18f Compare October 21, 2026 21:20
@Varshith-Kali

Copy link
Copy Markdown
Contributor Author

Hi @kddejong, the [CI] Test PR run here is still parked waiting for a maintainer approval (first-time-contributor gate) after my follow-up push, and your earlier approval now sits on a superseded commit. Could you approve the workflow run when you get a chance? Thanks.

@kddejong
kddejong force-pushed the fix/e3014-conditions-exclusivity branch from 5deaa0c to 4354ec9 Compare October 23, 2026 18:05
…ues parameters

The pairwise NAND added in aws-cloudformation#4464 lived inside a loop that skipped parameters
without AllowedValues, and pseudo-parameters never entered it at all. Two
Fn::Equals comparing the same single-valued Ref to different static values
can never both be true, so add the constraint for every Ref, matching the
template-level engine. This stops E3014 from validating the impossible
scenario where both conditions hold and reporting a spurious requiredXor.

Fixes aws-cloudformation#4799
The windows unitlint job's pre-commit ruff-format hook failed on this file.
Formatting-only change; no test logic touched.

Signed-off-by: Varshith Puli <pulivarshit@gmail.com>
@kddejong
kddejong force-pushed the fix/e3014-conditions-exclusivity branch from 4354ec9 to 5b2f5f8 Compare October 23, 2026 19:32
@codecov

codecov Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.68%. Comparing base (2a10fb4) to head (5b2f5f8).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4805   +/-   ##
=======================================
  Coverage   94.67%   94.68%           
=======================================
  Files         439      439           
  Lines       15995    16004    +9     
  Branches     3104     3108    +4     
=======================================
+ Hits        15144    15153    +9     
  Misses        468      468           
  Partials      383      383           
Flag Coverage Δ
unittests 94.67% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@kddejong
kddejong merged commit 73ff9f9 into aws-cloudformation:main Oct 23, 2026
24 checks passed
kddejong added a commit to kddejong/cfn-python-lint that referenced this pull request Nov 3, 2026
A condition's `equals` includes the Fn::Equals of every condition it
references, so the mutual-exclusion loop added in aws-cloudformation#4805 visited the same
Fn::Equals once per referrer and re-emitted its exclusion clauses each
time. The clause count grew quadratically in the number of references
rather than in the number of distinct Fn::Equals.

Skipping an Fn::Equals whose hash has already been paired is logically a
no-op: `Equal.cnf` is `Symbol(hash)`, so a repeat visit produces a clause
sympy has already been given. On the template from aws-cloudformation#4984 (30 Fn::Equals on
AWS::Region, 30 Fn::Or groups of 10) this takes the CNF from 53,025 clauses
to 825 and the lint from 17.4s to 1.3s, with identical output.

Fixes aws-cloudformation#4984
kddejong added a commit that referenced this pull request Nov 3, 2026
#4987)

* fix(conditions): pair each Fn::Equals once when building condition CNF

A condition's `equals` includes the Fn::Equals of every condition it
references, so the mutual-exclusion loop added in #4805 visited the same
Fn::Equals once per referrer and re-emitted its exclusion clauses each
time. The clause count grew quadratically in the number of references
rather than in the number of distinct Fn::Equals.

Skipping an Fn::Equals whose hash has already been paired is logically a
no-op: `Equal.cnf` is `Symbol(hash)`, so a repeat visit produces a clause
sympy has already been given. On the template from #4984 (30 Fn::Equals on
AWS::Region, 30 Fn::Or groups of 10) this takes the CNF from 53,025 clauses
to 825 and the lint from 17.4s to 1.3s, with identical output.

Fixes #4984

* refactor(conditions): share one distinct Fn::Equals map across both loops

The AllowedValues loop walked the same conditions x equals product and
re-visited each Fn::Equals once per referrer, appending duplicate symbols
into equals_cnfs. That one was harmless -- sympy's Or is idempotent, so the
duplicates collapsed to a single clause -- but it is the same latent shape
as the clause blowup, so give it the same guard.

Hoist the dedupe into one distinct_equals map built before both loops,
mirroring the shared equal_vars dict in the template-level engine
(cfnlint/conditions/conditions.py) rather than keeping a separate guard per
loop. Also hoist the per-parameter Ref hash out of the inner loop.

Behaviour is unchanged: insertion order preserves the previous first-seen
iteration order, so the NAND pairs are emitted in the same order, and the
AllowedValues coverage check is unaffected because two distinct Fn::Equals
on one Ref always carry different values.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E3014: two Fn::Equals on the same Ref are treated as exclusive only for a Parameter with AllowedValues

2 participants