export default function validateStates()

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
}