Skip to content

Commit b860bff

Browse files
authored
Convert more rules to v1 (aws-cloudformation#3250)
* Convert more rules to v1 * Move cfn_path to context * Switch to Path for tracking all things path
1 parent 31a7144 commit b860bff

File tree

88 files changed

+840
-848
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+840
-848
lines changed

src/cfnlint/context/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
__all__ = ["Context", "create_context_for_template"]
22

3-
from cfnlint.context.context import Context, create_context_for_template
3+
from cfnlint.context.context import Context, Path, create_context_for_template

src/cfnlint/context/context.py

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,62 @@ def has_language_extensions_transform(self):
3838
return bool(lang_extensions_transform in self._transforms)
3939

4040

41+
@dataclass(frozen=True)
42+
class Path:
43+
"""
44+
A `Path` keeps track of the different Path values
45+
"""
46+
47+
# path keeps track of the path as we move down the template
48+
# Example: Resources, MyResource, Properties, Name, ...
49+
path: Deque[str | int] = field(init=True, default_factory=deque)
50+
51+
# value_path is an override of the value if we got it from another place
52+
# like a Parameter default value
53+
# Example: Parameters, MyParameter, Default, ...
54+
value_path: Deque[str | int] = field(init=True, default_factory=deque)
55+
56+
# cfn_path is a generic path used by cfn-lint to help make
57+
# writing rules easier. The resource name is replaced by the type
58+
# lists are replaced with a *
59+
# Example: Resources, AWS::S3::Bucket, Properties, Name, ...
60+
# Example: Resources, *, Type
61+
cfn_path: Deque[str] = field(init=True, default_factory=deque)
62+
63+
def descend(self, **kwargs):
64+
"""
65+
Create a new Path by appending values
66+
"""
67+
cls = self.__class__
68+
69+
for f in fields(Path):
70+
if kwargs.get(f.name) is not None:
71+
kwargs[f.name] = getattr(self, f.name) + deque([kwargs[f.name]])
72+
else:
73+
kwargs[f.name] = getattr(self, f.name)
74+
75+
return cls(**kwargs)
76+
77+
def evolve(self, **kwargs):
78+
"""
79+
Create a new path without appending values
80+
"""
81+
cls = self.__class__
82+
83+
for f in fields(Path):
84+
kwargs.setdefault(f.name, getattr(self, f.name))
85+
86+
return cls(**kwargs)
87+
88+
@property
89+
def path_string(self):
90+
return "/".join(str(p) for p in self.path)
91+
92+
@property
93+
def cfn_path_string(self):
94+
return "/".join(self.cfn_path)
95+
96+
4197
@dataclass(frozen=True)
4298
class Context:
4399
"""
@@ -61,12 +117,7 @@ class Context:
61117
# supported functions at this point in the template
62118
functions: Sequence[str] = field(init=True, default_factory=list)
63119

64-
# path keeps track of the path as we move down the template
65-
# Example: Resources, MyResource, Properties, Name, ...
66-
path: Deque[str] = field(init=True, default_factory=deque)
67-
# value_path is an override of the value if we got it from another place
68-
# like a Parameter default value
69-
value_path: Deque[str] = field(init=True, default_factory=deque)
120+
path: Path = field(init=True, default_factory=Path)
70121

71122
# cfn-lint Template class
72123
parameters: Dict[str, "Parameter"] = field(init=True, default_factory=dict)
@@ -95,21 +146,14 @@ def __post_init__(self) -> None:
95146

96147
def evolve(self, **kwargs) -> "Context":
97148
"""
98-
Create a new context merging together attributes
149+
Create a new context without merging together attributes
99150
"""
100151
cls = self.__class__
101152

102-
if "path" in kwargs and kwargs["path"] is not None:
103-
path = self.path.copy()
104-
path.append(kwargs["path"])
105-
kwargs["path"] = path
106-
else:
107-
kwargs["path"] = self.path.copy()
108-
109-
if "ref_values" in kwargs and kwargs["ref_values"] is not None:
110-
ref_values = self.ref_values.copy()
111-
ref_values.update(kwargs["ref_values"])
112-
kwargs["ref_values"] = ref_values
153+
if "ref_values" in kwargs:
154+
new_ref_values = self.ref_values.copy()
155+
new_ref_values.update(kwargs["ref_values"])
156+
kwargs["ref_values"] = new_ref_values
113157

114158
for f in fields(Context):
115159
if f.init:
@@ -134,7 +178,9 @@ def ref_value(self, instance: str) -> Iterator[Tuple[str | List[str], "Context"]
134178
if instance in self.parameters:
135179
for v, path in self.parameters[instance].ref(self):
136180
yield v, self.evolve(
137-
value_path=deque(["Parameters", instance]) + path,
181+
path=self.path.evolve(
182+
value_path=deque(["Parameters", instance]) + path
183+
),
138184
ref_values={instance: v},
139185
)
140186
return
@@ -450,6 +496,6 @@ def create_context_for_template(cfn):
450496
transforms=transforms,
451497
mappings=mappings,
452498
regions=cfn.regions,
453-
path=deque([]),
499+
path=Path(),
454500
functions=["Fn::Transform"],
455501
)

src/cfnlint/graph.py

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
SPDX-License-Identifier: MIT-0
66
"""
77

8+
from __future__ import annotations
9+
810
import logging
911
import warnings
1012
from typing import Any, List
@@ -43,6 +45,14 @@ class GraphSettings:
4345
parameter: NodeSetting
4446
output: NodeSetting
4547

48+
def _pydot_string_convert(self, value: str | int) -> str | int:
49+
if not isinstance(value, str):
50+
return value
51+
if ":" in value:
52+
return f'"{value}"'
53+
54+
return value
55+
4656
def subgraph_view(self, graph) -> networkx.MultiDiGraph:
4757
view = networkx.MultiDiGraph(name="template")
4858
resources: List[str] = [
@@ -52,17 +62,22 @@ def subgraph_view(self, graph) -> networkx.MultiDiGraph:
5262
# have to add quotes when outputing to dot
5363
for resource in resources:
5464
node = graph.nodes[resource]
55-
node["type"] = f'"{node["type"]}"'
65+
node["label"] = self._pydot_string_convert(node["label"])
66+
node["type"] = self._pydot_string_convert(node["type"])
67+
del node["resource_type"]
5668
view.add_node(resource, **node)
5769

58-
view.add_edges_from(
59-
(n, nbr, key, d)
60-
for n, nbrs in graph.adj.items()
61-
if n in resources
62-
for nbr, keydict in nbrs.items()
63-
if nbr in resources
64-
for key, d in keydict.items()
65-
)
70+
for edge_1, edge_2, edge_data in graph.edges(data=True):
71+
if edge_1 in resources and edge_2 in resources:
72+
edge_data["source_paths"] = [
73+
self._pydot_string_convert(p) for p in edge_data["source_paths"]
74+
]
75+
view.add_edge(
76+
edge_1,
77+
edge_2,
78+
**edge_data,
79+
)
80+
6681
view.graph.update(graph.graph)
6782
return view
6883

@@ -131,7 +146,7 @@ def _add_outputs(self, cfn: Any) -> None:
131146
# add all outputs in the template as nodes
132147
for output_id in cfn.template.get("Outputs", {}).keys():
133148
graph_label = str.format(f'"{output_id}"')
134-
self._add_node(output_id, label=graph_label, settings=self.settings.output)
149+
self._add_node(output_id, settings=self.settings.output, label=graph_label)
135150

136151
def _add_resources(self, cfn: Any):
137152
# add all resources in the template as nodes
@@ -141,9 +156,12 @@ def _add_resources(self, cfn: Any):
141156
type_val = resourceVals.get("Type", "")
142157
if not isinstance(type_val, str):
143158
continue
144-
graph_label = str.format(f'"{resourceId}\\n<{type_val}>"')
159+
graph_label = str.format(f"{resourceId}\\n<{type_val}>")
145160
self._add_node(
146-
resourceId, label=graph_label, settings=self.settings.resource
161+
resourceId,
162+
settings=self.settings.resource,
163+
label=graph_label,
164+
resource_type=type_val,
147165
)
148166
target_ids = resourceVals.get("DependsOn", [])
149167
if isinstance(target_ids, (list, str)):
@@ -248,17 +266,14 @@ def _add_subs(self, cfn: Any) -> None:
248266
source_id, sub_parameter, source_path, self.settings.ref
249267
)
250268

251-
def _add_node(self, node_id, label, settings):
269+
def _add_node(self, node_id, settings, **attr):
252270
if settings.node_type in ["Parameter", "Output"]:
253271
node_id = f"{settings.node_type}-{node_id}"
254272

255-
self.graph.add_node(
256-
node_id,
257-
label=label,
258-
color=settings.color,
259-
shape=settings.shape,
260-
type=settings.node_type,
261-
)
273+
attr.setdefault("color", settings.color)
274+
attr.setdefault("shape", settings.shape)
275+
attr.setdefault("type", settings.node_type)
276+
self.graph.add_node(node_id, **attr)
262277

263278
def _add_edge(self, source_id, target_id, source_path, settings):
264279
self.graph.add_edge(

src/cfnlint/jsonschema/_filter.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,18 @@
33
SPDX-License-Identifier: MIT-0
44
"""
55

6+
from __future__ import annotations
7+
68
from dataclasses import dataclass, field, fields
7-
from typing import Any, Sequence, Tuple
9+
from typing import TYPE_CHECKING, Any, Sequence, Tuple
810

911
from cfnlint.helpers import REGEX_DYN_REF, ToPy
1012
from cfnlint.jsonschema._utils import ensure_list
1113

14+
if TYPE_CHECKING:
15+
from cfnlint.jsonschema.protocols import Validator
16+
17+
1218
_all_types = ["array", "boolean", "integer", "number", "object", "string"]
1319

1420

@@ -53,7 +59,7 @@ class FunctionFilter:
5359
default=True,
5460
)
5561

56-
def _filter_schemas(self, schema, validator: Any) -> Tuple[Any, Any]:
62+
def _filter_schemas(self, schema, validator: Validator) -> Tuple[Any, Any]:
5763
"""
5864
Filter the schemas to only include the ones that are required
5965
"""
@@ -64,8 +70,8 @@ def _filter_schemas(self, schema, validator: Any) -> Tuple[Any, Any]:
6470
# Example: Typically we want to remove (!Ref AWS::NoValue)
6571
# to count minItems, maxItems, required properties but if we
6672
# are in an If we need to be more strict
67-
if len(validator.context.path) > 0:
68-
if validator.context.path[-1] in ["Fn::If"]:
73+
if len(validator.context.path.path) > 0:
74+
if validator.context.path.path[-1] in ["Fn::If"]:
6975
return schema, None
7076

7177
standard_schema = {}
@@ -78,7 +84,7 @@ def _filter_schemas(self, schema, validator: Any) -> Tuple[Any, Any]:
7884

7985
if self.add_cfn_lint_keyword and "$ref" not in standard_schema:
8086
standard_schema["cfnLint"] = ensure_list(standard_schema.get("cfnLint", []))
81-
standard_schema["cfnLint"].append("/".join(validator.cfn_path))
87+
standard_schema["cfnLint"].append("/".join(validator.context.path.cfn_path))
8288

8389
# some times CloudFormation dumps to standard nested "json".
8490
# it will do by using {"type": "object"} with no properties

src/cfnlint/jsonschema/_keywords.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def additionalProperties(
4646
if validator.is_type(aP, "object"):
4747
for extra in extras:
4848
yield from validator.descend(
49-
instance[extra], aP, path=extra, property_path="*"
49+
instance[extra],
50+
aP,
51+
path=extra,
52+
property_path="*",
5053
)
5154
elif not aP and extras:
5255
if "patternProperties" in schema:
@@ -543,7 +546,7 @@ def type(
543546
yield ValidationError(f"{instance!r} is not of type {reprs}")
544547

545548

546-
def prefixItems(validator, prefixItems, instance, schema):
549+
def prefixItems(validator: Validator, prefixItems: Any, instance: Any, schema: Any):
547550
if not validator.is_type(instance, "array"):
548551
return
549552

src/cfnlint/jsonschema/_resolvers_cfn.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
4747
if "DefaultValue" in options:
4848
for value, v, _ in validator.resolve_value(options["DefaultValue"]):
4949
yield value, v.evolve(
50-
context=v.context.evolve(value_path=deque([4, "DefaultValue"]))
50+
context=v.context.evolve(
51+
path=v.context.path.evolve(
52+
value_path=deque([4, "DefaultValue"])
53+
)
54+
),
5155
), None
5256
default_value = value
5357

@@ -66,7 +70,9 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
6670
not (equal(map_name, each)) for each in mappings
6771
):
6872
yield None, map_v.evolve(
69-
context=map_v.context.evolve(value_path=[0])
73+
context=map_v.context.evolve(
74+
path=map_v.context.path.evolve(value_path=deque([0])),
75+
),
7076
), ValidationError(
7177
f"{map_name!r} is not one of {mappings!r}", path=[0]
7278
)
@@ -79,7 +85,9 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
7985
not (equal(top_level_key, each)) for each in top_level_keys
8086
):
8187
yield None, top_v.evolve(
82-
context=top_v.context.evolve(value_path=[1])
88+
context=top_v.context.evolve(
89+
path=top_v.context.path.evolve(value_path=deque([1])),
90+
),
8391
), ValidationError(
8492
f"{top_level_key!r} is not one of {top_level_keys!r}",
8593
path=[0],
@@ -96,7 +104,11 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
96104
for each in second_level_keys
97105
):
98106
yield None, second_v.evolve(
99-
context=second_v.context.evolve(value_path=[2])
107+
context=second_v.context.evolve(
108+
path=second_v.context.path.evolve(
109+
value_path=deque([2])
110+
),
111+
),
100112
), ValidationError(
101113
f"{second_level_key!r} is not one of {second_level_keys!r}",
102114
path=[0],
@@ -111,12 +123,16 @@ def find_in_map(validator: Validator, instance: Any) -> ResolutionResult:
111123
value,
112124
validator.evolve(
113125
context=validator.context.evolve(
114-
value_path=[
115-
"Mappings",
116-
map_name,
117-
top_level_key,
118-
second_level_key,
119-
]
126+
path=validator.context.path.evolve(
127+
value_path=deque(
128+
[
129+
"Mappings",
130+
map_name,
131+
top_level_key,
132+
second_level_key,
133+
]
134+
)
135+
)
120136
)
121137
),
122138
None,
@@ -298,7 +314,9 @@ def if_(validator: Validator, instance: Any) -> ResolutionResult:
298314
yield (
299315
value,
300316
v.evolve(
301-
context=v.context.evolve(value_path=deque([i])),
317+
context=v.context.evolve(
318+
path=v.context.path.evolve(value_path=deque([i])),
319+
),
302320
),
303321
err,
304322
)

src/cfnlint/jsonschema/protocols.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class Validator(Protocol):
6262
cfn: Template | None
6363
context: Context
6464
function_filter: FunctionFilter
65-
cfn_path: deque[str | int]
65+
cfn_path: deque[str]
6666

6767
def __init__(
6868
self,

0 commit comments

Comments
 (0)