in src/validation/validateStates.ts [115:303]
export default function validateStates(rootNode: ObjectASTNode, document: TextDocument, isRoot?: Boolean, options?: ASLOptions): Diagnostic[] {
const statesNode = findPropChildByName(rootNode, 'States')
const startAtNode = findPropChildByName(rootNode, 'StartAt')
// Different schemas for root and root of nested state machine
const rootSchema = isRoot ? schema.Root : schema.NestedRoot
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
const stateName = prop.keyNode.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')
if (iteratorPropNode && iteratorPropNode.valueNode && isObjectNode(iteratorPropNode.valueNode)) {
// append the result of recursive validation to the list of diagnostics
diagnostics = [...diagnostics, ...validateStates(iteratorPropNode.valueNode, document, undefined, 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, undefined, 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
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
}