in src/validation/validateStates.ts [206:422]
export default function validateStates(
rootNode: ObjectASTNode,
document: TextDocument,
rootType: RootType,
options?: ASLOptions,
): Diagnostic[] {
const statesNode = findPropChildByName(rootNode, 'States')
const startAtNode = findPropChildByName(rootNode, 'StartAt')
// Different schemas for root and root of nested state machine
let rootSchema: object = schema.Root
if (rootType === RootType.Map) {
rootSchema = schema.NestedMapRoot
} else if (rootType === RootType.Parallel) {
rootSchema = schema.NestedParallelRoot
}
let diagnostics: Diagnostic[] = []
// Check root property names against the schema
rootNode.properties.forEach((prop) => {
const key = prop.keyNode.value
if (!rootSchema[key]) {
diagnostics.push(getPropertyNodeDiagnostic(prop, document, MESSAGES.INVALID_PROPERTY_NAME))
}
})
if (statesNode) {
const stateNames = getListOfStateNamesFromStateNode(statesNode, options?.ignoreColonOffset)
const statesValueNode = statesNode.valueNode
if (startAtNode) {
const stateNameExists = (stateNames as unknown[]).includes(startAtNode.valueNode?.value)
if (startAtNode.valueNode && !stateNameExists) {
const { length, offset } = startAtNode.valueNode
const range = Range.create(document.positionAt(offset), document.positionAt(offset + length))
diagnostics.push(Diagnostic.create(range, MESSAGES.INVALID_START_AT, DiagnosticSeverity.Error))
}
}
if (statesValueNode && isObjectNode(statesValueNode)) {
// keep track of reached states and unreached states to avoid multiple loops
let reachedStates: { [ix: string]: boolean } = {}
let hasTerminalState = false
const startAtValue = startAtNode?.valueNode?.value
// mark state referred to in StartAt as reached
if (typeof startAtValue === 'string') {
reachedStates[startAtValue] = true
}
statesValueNode.properties.forEach((prop) => {
const oneStateValueNode = prop.valueNode
if (oneStateValueNode && isObjectNode(oneStateValueNode)) {
diagnostics = diagnostics.concat(validateProperties(oneStateValueNode, document))
const nextPropNode = findPropChildByName(oneStateValueNode, 'Next')
const endPropNode = findPropChildByName(oneStateValueNode, 'End')
const stateType = oneStateValueNode.properties.find((oneStateProp) => oneStateProp.keyNode.value === 'Type')
?.valueNode?.value
const nextNodeValue = nextPropNode?.valueNode?.value
if (endPropNode && endPropNode.valueNode?.value === true) {
hasTerminalState = true
}
// mark the value of Next property as reached state
if (typeof nextNodeValue === 'string') {
reachedStates[nextNodeValue] = true
}
// Validate Parameters for given state types
if (['Pass', 'Task', 'Parallel', 'Map'].includes(stateType as string)) {
const parametersPropNode = findPropChildByName(oneStateValueNode, 'Parameters')
if (parametersPropNode) {
const validateParametersDiagnostics = validateParameters(parametersPropNode, document)
diagnostics = diagnostics.concat(validateParametersDiagnostics)
}
}
// Validate Catch for given state types
if (['Task', 'Parallel', 'Map'].includes(stateType as string)) {
const validateCatchResult = validateArrayNext('Catch', oneStateValueNode, stateNames, document)
const resultSelectorPropNode = findPropChildByName(oneStateValueNode, 'ResultSelector')
diagnostics = diagnostics.concat(validateCatchResult.diagnostics)
reachedStates = { ...reachedStates, ...validateCatchResult.reachedStates }
if (resultSelectorPropNode) {
const resultSelectorDiagnostics = validateParameters(resultSelectorPropNode, document)
diagnostics = diagnostics.concat(resultSelectorDiagnostics)
}
}
switch (stateType) {
// if the type of the state is "Map" recursively run validateStates for its value node
case 'Map': {
const iteratorPropNode =
findPropChildByName(oneStateValueNode, 'Iterator') ||
findPropChildByName(oneStateValueNode, 'ItemProcessor')
if (iteratorPropNode && iteratorPropNode.valueNode && isObjectNode(iteratorPropNode.valueNode)) {
// append the result of recursive validation to the list of diagnostics
diagnostics = [
...diagnostics,
...validateStates(iteratorPropNode.valueNode, document, RootType.Map, options),
]
}
break
}
// it the type of state is "Parallel" recursively run validateStates for each child of value node (an array)
case 'Parallel': {
const branchesPropNode = findPropChildByName(oneStateValueNode, 'Branches')
if (branchesPropNode && branchesPropNode.valueNode && isArrayNode(branchesPropNode.valueNode)) {
branchesPropNode.valueNode.children.forEach((branchItem) => {
if (isObjectNode(branchItem)) {
// append the result of recursive validation to the list of diagnostics
diagnostics = [...diagnostics, ...validateStates(branchItem, document, RootType.Parallel, options)]
}
})
}
break
}
case 'Choice': {
const defaultNode = findPropChildByName(oneStateValueNode, 'Default')
if (defaultNode) {
const name = defaultNode?.valueNode?.value
const defaultStateDiagnostic = stateNameExistsInPropNode(
defaultNode,
stateNames,
document,
MESSAGES.INVALID_DEFAULT,
)
if (defaultStateDiagnostic) {
diagnostics.push(defaultStateDiagnostic)
} else if (typeof name === 'string') {
reachedStates[name] = true
}
}
const validateChoiceResult = validateArrayNext('Choices', oneStateValueNode, stateNames, document)
diagnostics = diagnostics.concat(validateChoiceResult.diagnostics)
reachedStates = { ...reachedStates, ...validateChoiceResult.reachedStates }
break
}
case 'Succeed':
case 'Fail': {
hasTerminalState = true
const validateErrorFieldDiagnostics = validateExclusivePathTypeField(oneStateValueNode, 'Error', document)
diagnostics = diagnostics.concat(validateErrorFieldDiagnostics)
const validateCauseFieldDiagnostics = validateExclusivePathTypeField(oneStateValueNode, 'Cause', document)
diagnostics = diagnostics.concat(validateCauseFieldDiagnostics)
break
}
}
if (nextPropNode) {
const nextStateDiagnostic = stateNameExistsInPropNode(
nextPropNode,
stateNames,
document,
MESSAGES.INVALID_NEXT,
)
if (nextStateDiagnostic) {
diagnostics.push(nextStateDiagnostic)
}
}
}
})
// if it doesn't have a terminal state emit diagnostic
// selecting the range of "States" property key node
if (!hasTerminalState) {
const { length, offset } = statesNode.keyNode
const range = Range.create(document.positionAt(offset), document.positionAt(offset + length))
diagnostics.push(Diagnostic.create(range, MESSAGES.NO_TERMINAL_STATE, DiagnosticSeverity.Error))
}
// Create diagnostics for states that weren't referenced by a State, Choice Rule, or Catcher's "Next" field
statesValueNode.properties
.filter((statePropNode) => {
const stateName = statePropNode.keyNode.value
return !reachedStates[stateName]
})
.forEach((unreachableStatePropNode) => {
const { length, offset } = unreachableStatePropNode.keyNode
const range = Range.create(document.positionAt(offset), document.positionAt(offset + length))
diagnostics.push(Diagnostic.create(range, MESSAGES.UNREACHABLE_STATE, DiagnosticSeverity.Error))
})
}
}
return diagnostics
}