Skip to content

Commit 33ec487

Browse files
committed
State Machine: Implement check for "StartAt"
Includes UnitTest for - StartAt - Map StartAt - Parallel StartAt
1 parent 5999083 commit 33ec487

File tree

2 files changed

+139
-0
lines changed

2 files changed

+139
-0
lines changed

src/cfnlint/rules/resources/stepfunctions/StateMachineDefinition.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,56 @@ def _clean_schema(self, validator: Validator, instance: Any):
103103
if ql == "JSONata":
104104
yield self._convert_schema_to_jsonata(), ql_validator
105105

106+
def _validate_start_at(
107+
self,
108+
definition: Any,
109+
k: str,
110+
add_path_to_message: bool,
111+
path: deque | None = None
112+
) -> ValidationResult:
113+
"""
114+
Per the Amazon States Language specification, 'StartAt must' reference
115+
a valid state name that exists in the States object.
116+
117+
Reference: https://states-language.net/spec.html#toplevelfields
118+
"""
119+
120+
start_at = definition.get("StartAt")
121+
states = definition.get("States")
122+
123+
# Check if StartAt is missing
124+
if start_at is None:
125+
return # Early return to avoid further checks
126+
127+
# Check if StartAt state exists in States object
128+
if start_at not in states:
129+
if path is None: # Top level StartAt
130+
error_path = deque([k, "StartAt"])
131+
display_path = "/StartAt"
132+
else: # Nested StartAt like Parallel or Map
133+
error_path = deque([k] + list(path) + ["StartAt"])
134+
display_path = f"/{'/'.join(str(item) for item in path)}/StartAt"
135+
136+
message = f"MISSING_TRANSITION_TARGET: Missing 'Next' target '{start_at}' at {display_path}"
137+
138+
yield ValidationError(message, path=error_path,rule=self)
139+
140+
# Validate nested StartAt in Parallel and Map states
141+
for state_name, state in states.items():
142+
143+
state_type = state.get("Type")
144+
145+
if state_type == "Parallel":
146+
branches = state.get("Branches", [])
147+
for idx, branch in enumerate(branches):
148+
branch_path = deque(["States", state_name, "Branches", idx])
149+
yield from self._validate_start_at(branch, k, add_path_to_message, branch_path)
150+
151+
if state_type == "Map":
152+
processor = state.get("ItemProcessor")
153+
processor_path = deque(["States", state_name, "ItemProcessor"])
154+
yield from self._validate_start_at(processor, k, add_path_to_message, processor_path)
155+
106156
def _validate_step(
107157
self,
108158
validator: Validator,
@@ -134,6 +184,9 @@ def _validate_step(
134184

135185
yield self._clean_error(err)
136186

187+
# Validate StartAt exists
188+
yield from self._validate_start_at(value, k, add_path_to_message)
189+
137190
def validate(
138191
self, validator: Validator, keywords: Any, instance: Any, schema: dict[str, Any]
139192
) -> ValidationResult:

test/unit/rules/resources/stepfunctions/test_state_machine_definition.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,6 +1351,92 @@ def rule():
13511351
path=deque(
13521352
["Definition", "States", "Notify Failure", "Parameters"]
13531353
),
1354+
)
1355+
],
1356+
),
1357+
(
1358+
"Missing StartAt target",
1359+
{
1360+
"Definition": {
1361+
"StartAt": "FAIL",
1362+
"States": {
1363+
"Pass": {
1364+
"Type": "Pass",
1365+
"Next": "Success",
1366+
},
1367+
"Success": {
1368+
"Type": "Succeed",
1369+
},
1370+
},
1371+
}
1372+
},
1373+
[
1374+
ValidationError(
1375+
"MISSING_TRANSITION_TARGET: Missing 'Next' target 'FAIL' at /StartAt",
1376+
rule=StateMachineDefinition(),
1377+
path=deque(["Definition", "StartAt"]),
1378+
),
1379+
],
1380+
),
1381+
(
1382+
"Parallel state with missing StartAt target",
1383+
{
1384+
"Definition": {
1385+
"StartAt": "ParallelState",
1386+
"States": {
1387+
"ParallelState": {
1388+
"Type": "Parallel",
1389+
"Branches": [
1390+
{
1391+
"StartAt": "FAIL",
1392+
"States": {
1393+
"BranchState": {
1394+
"Type": "Pass",
1395+
"End": True,
1396+
},
1397+
},
1398+
}
1399+
],
1400+
"End": True,
1401+
},
1402+
},
1403+
}
1404+
},
1405+
[
1406+
ValidationError(
1407+
"MISSING_TRANSITION_TARGET: Missing 'Next' target 'FAIL' at /States/ParallelState/Branches/0/StartAt",
1408+
rule=StateMachineDefinition(),
1409+
path=deque(["Definition", "States", "ParallelState", "Branches", 0, "StartAt"]),
1410+
),
1411+
],
1412+
),
1413+
(
1414+
"Map state with missing StartAt target in ItemProcessor",
1415+
{
1416+
"Definition": {
1417+
"StartAt": "MapState",
1418+
"States": {
1419+
"MapState": {
1420+
"Type": "Map",
1421+
"ItemProcessor": {
1422+
"StartAt": "FAIL",
1423+
"States": {
1424+
"ProcessItem": {
1425+
"Type": "Pass",
1426+
"End": True,
1427+
},
1428+
},
1429+
},
1430+
"End": True,
1431+
},
1432+
},
1433+
}
1434+
},
1435+
[
1436+
ValidationError(
1437+
"MISSING_TRANSITION_TARGET: Missing 'Next' target 'FAIL' at /States/MapState/ItemProcessor/StartAt",
1438+
rule=StateMachineDefinition(),
1439+
path=deque(["Definition", "States", "MapState", "ItemProcessor", "StartAt"]),
13541440
),
13551441
],
13561442
),

0 commit comments

Comments
 (0)