Skip to content

Commit b89eb11

Browse files
authored
Better support for rule Equals when static (#3659)
1 parent 31d4036 commit b89eb11

File tree

5 files changed

+115
-11
lines changed

5 files changed

+115
-11
lines changed

src/cfnlint/conditions/_condition.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from typing import Any, Dict, Mapping, Sequence, Union
99

1010
from sympy import And, Not, Or, Symbol
11-
from sympy.logic.boolalg import BooleanFunction
11+
from sympy.logic.boolalg import BooleanFunction, BooleanTrue
1212

1313
from cfnlint.conditions._equals import Equal
1414
from cfnlint.helpers import FUNCTION_CONDITIONS
@@ -80,8 +80,8 @@ def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction | Symbol | Non
8080
if self._condition:
8181
return self._condition.build_cnf(params)
8282
if self._fn_equals:
83-
return params.get(self._fn_equals.hash)
84-
return None
83+
return self._fn_equals.build_cnf(params)
84+
return BooleanTrue()
8585

8686
def _test(self, scenarios: Mapping[str, str]) -> bool:
8787
if self._fn_equals:

src/cfnlint/conditions/_equals.py

+18
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import logging
1010
from typing import Any, Mapping, Tuple
1111

12+
from sympy import Symbol
13+
from sympy.logic.boolalg import BooleanFalse, BooleanFunction, BooleanTrue
14+
1215
from cfnlint.conditions._utils import get_hash
1316
from cfnlint.helpers import is_function
1417

@@ -142,6 +145,21 @@ def left(self):
142145
def right(self):
143146
return self._right
144147

148+
def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction:
149+
"""Build a SymPy CNF solver based on the provided params
150+
Args:
151+
params dict[str, Symbol]: params is a dict that represents
152+
the hash of an Equal and the SymPy Symbol
153+
Returns:
154+
BooleanFunction: A Not SymPy BooleanFunction
155+
"""
156+
if self._is_static is not None:
157+
if self._is_static:
158+
return BooleanTrue()
159+
return BooleanFalse()
160+
161+
return params.get(self.hash)
162+
145163
def test(self, scenarios: Mapping[str, str]) -> bool:
146164
"""Do an equals based on the provided scenario"""
147165
if self._is_static in [True, False]:

src/cfnlint/conditions/_rule.py

+13-4
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@
55

66
from __future__ import annotations
77

8+
import logging
89
from typing import Any, Dict
910

1011
from sympy import And, Implies, Symbol
11-
from sympy.logic.boolalg import BooleanFunction
12+
from sympy.logic.boolalg import BooleanFunction, BooleanTrue
1213

1314
from cfnlint.conditions._condition import (
1415
ConditionAnd,
@@ -20,6 +21,8 @@
2021
from cfnlint.conditions._equals import Equal
2122
from cfnlint.helpers import FUNCTION_CONDITIONS
2223

24+
LOGGER = logging.getLogger(__name__)
25+
2326
# we leave the type hinting here
2427
_RULE = Dict[str, Any]
2528

@@ -53,12 +56,18 @@ def __init__(self, condition: Any, all_conditions: dict[str, dict]) -> None:
5356

5457
def build_cnf(self, params: dict[str, Symbol]) -> BooleanFunction | Symbol | None:
5558
if self._fn_equals:
56-
return self._fn_equals.hash
59+
try:
60+
return self._fn_equals.build_cnf(params)
61+
except Exception as e:
62+
LOGGER.debug(f"Error building condition: {e}")
5763

5864
if self._condition:
59-
return self._condition.build_cnf(params)
65+
try:
66+
return self._condition.build_cnf(params)
67+
except Exception as e:
68+
LOGGER.debug(f"Error building condition: {e}")
6069

61-
return None
70+
return BooleanTrue()
6271

6372
@property
6473
def equals(self) -> list[Equal]:

src/cfnlint/conditions/conditions.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,11 @@ def _init_parameters(self, cfn: Any) -> None:
7979

8080
def _init_rules(self, cfn: Any) -> None:
8181
rules = cfn.template.get("Rules")
82-
conditions = cfn.template.get("Conditions")
82+
conditions = cfn.template.get("Conditions", {})
8383
if not isinstance(rules, dict) or not isinstance(conditions, dict):
8484
return
8585
for k, v in rules.items():
86-
if not isinstance(rules, dict):
86+
if not isinstance(v, dict):
8787
continue
8888
try:
8989
self._rules.append(Rule(v, conditions))
@@ -391,7 +391,13 @@ def satisfiable(
391391
UnknownSatisfisfaction: If we don't know how to satisfy a condition
392392
"""
393393
if not conditions:
394-
return True
394+
if self._rules:
395+
satisfied = satisfiable(self._cnf, all_models=False)
396+
if satisfied is False:
397+
return satisfied
398+
return True
399+
else:
400+
return True
395401

396402
cnf = self._cnf.copy()
397403
at_least_one_param_found = False
@@ -435,7 +441,13 @@ def satisfiable(
435441
)
436442

437443
if at_least_one_param_found is False:
438-
return True
444+
if self._rules:
445+
satisfied = satisfiable(self._cnf, all_models=False)
446+
if satisfied is False:
447+
return satisfied
448+
return True
449+
else:
450+
return True
439451

440452
satisfied = satisfiable(cnf, all_models=False)
441453
if satisfied is False:

test/unit/module/conditions/test_rules.py

+65
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,71 @@ def test_conditions_with_multiple_rules(self):
237237
)
238238
)
239239

240+
def test_fn_equals_assertions_two(self):
241+
template = decode_str(
242+
"""
243+
Rules:
244+
Rule1:
245+
Assertions:
246+
- Assert: !Equals ["A", "B"]
247+
Rule2:
248+
Assertions:
249+
- Assert: !Equals ["A", "A"]
250+
"""
251+
)[0]
252+
253+
cfn = Template("", template)
254+
self.assertEqual(len(cfn.conditions._conditions), 0)
255+
self.assertEqual(len(cfn.conditions._rules), 2)
256+
257+
self.assertListEqual(
258+
[equal.hash for equal in cfn.conditions._rules[0].equals],
259+
[
260+
"e7e68477799682e53ecb09f476128abaeba0bdae",
261+
],
262+
)
263+
self.assertListEqual(
264+
[equal.hash for equal in cfn.conditions._rules[1].equals],
265+
[
266+
"da2a95009a205d5caacd42c3c11ebd4c151b3409",
267+
],
268+
)
269+
270+
self.assertFalse(
271+
cfn.conditions.satisfiable(
272+
{},
273+
{},
274+
)
275+
)
276+
277+
def test_fn_equals_assertions_one(self):
278+
template = decode_str(
279+
"""
280+
Rules:
281+
Rule1:
282+
Assertions:
283+
- Assert: !Equals ["A", "A"]
284+
"""
285+
)[0]
286+
287+
cfn = Template("", template)
288+
self.assertEqual(len(cfn.conditions._conditions), 0)
289+
self.assertEqual(len(cfn.conditions._rules), 1)
290+
291+
self.assertListEqual(
292+
[equal.hash for equal in cfn.conditions._rules[0].equals],
293+
[
294+
"da2a95009a205d5caacd42c3c11ebd4c151b3409",
295+
],
296+
)
297+
298+
self.assertTrue(
299+
cfn.conditions.satisfiable(
300+
{},
301+
{},
302+
)
303+
)
304+
240305

241306
class TestAssertion(TestCase):
242307
def test_assertion_errors(self):

0 commit comments

Comments
 (0)