export default function validateStates()

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
}