function create(context) {
  const sourceCode = context.getSourceCode()
  const otherAttrs = [
    ATTRS.ATTR_DYNAMIC,
    ATTRS.ATTR_STATIC,
    ATTRS.ATTR_SHORTHAND_BOOL
  ]
  let attributeOrder = [
    ATTRS.DEFINITION,
    ATTRS.LIST_RENDERING,
    ATTRS.CONDITIONALS,
    ATTRS.RENDER_MODIFIERS,
    ATTRS.GLOBAL,
    [ATTRS.UNIQUE, ATTRS.SLOT],
    ATTRS.TWO_WAY_BINDING,
    ATTRS.OTHER_DIRECTIVES,
    otherAttrs,
    ATTRS.EVENTS,
    ATTRS.CONTENT
  ]
  if (context.options[0] && context.options[0].order) {
    attributeOrder = [...context.options[0].order]

    // check if `OTHER_ATTR` is valid
    for (const item of attributeOrder.flat()) {
      if (item === ATTRS.OTHER_ATTR) {
        for (const attribute of attributeOrder.flat()) {
          if (otherAttrs.includes(attribute)) {
            throw new Error(
              `Value "${ATTRS.OTHER_ATTR}" is not allowed with "${attribute}".`
            )
          }
        }
      }
    }

    // expand `OTHER_ATTR` alias
    for (const [index, item] of attributeOrder.entries()) {
      if (item === ATTRS.OTHER_ATTR) {
        attributeOrder[index] = otherAttrs
      } else if (Array.isArray(item) && item.includes(ATTRS.OTHER_ATTR)) {
        const attributes = item.filter((i) => i !== ATTRS.OTHER_ATTR)
        attributes.push(...otherAttrs)
        attributeOrder[index] = attributes
      }
    }
  }
  const alphabetical = Boolean(
    context.options[0] && context.options[0].alphabetical
  )

  /** @type { { [key: string]: number } } */
  const attributePosition = {}
  for (const [i, item] of attributeOrder.entries()) {
    if (Array.isArray(item)) {
      for (const attr of item) {
        attributePosition[attr] = i
      }
    } else attributePosition[item] = i
  }

  /**
   * @param {VAttribute | VDirective} node
   * @param {VAttribute | VDirective} previousNode
   */
  function reportIssue(node, previousNode) {
    const currentNode = sourceCode.getText(node.key)
    const prevNode = sourceCode.getText(previousNode.key)
    context.report({
      node,
      messageId: 'expectedOrder',
      data: {
        currentNode,
        prevNode
      },

      fix(fixer) {
        const attributes = node.parent.attributes

        /** @type { (node: VAttribute | VDirective | undefined) => boolean } */
        let isMoveUp

        if (isVBindObject(node)) {
          // prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
          isMoveUp = isVAttributeOrVBindOrVModel
        } else if (isVAttributeOrVBindOrVModel(node)) {
          // prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
          isMoveUp = isVBindObject
        } else {
          isMoveUp = () => false
        }

        const previousNodes = attributes.slice(
          attributes.indexOf(previousNode),
          attributes.indexOf(node)
        )
        const moveNodes = [node]
        for (const node of previousNodes) {
          if (isMoveUp(node)) {
            moveNodes.unshift(node)
          } else {
            moveNodes.push(node)
          }
        }

        return moveNodes.map((moveNode, index) => {
          const text = sourceCode.getText(moveNode)
          return fixer.replaceText(previousNodes[index] || node, text)
        })
      }
    })
  }

  return utils.defineTemplateBodyVisitor(context, {
    VStartTag(node) {
      const attributeAndPositions = getAttributeAndPositionList(node)
      if (attributeAndPositions.length <= 1) {
        return
      }

      let { attr: previousNode, position: previousPosition } =
        attributeAndPositions[0]
      for (let index = 1; index < attributeAndPositions.length; index++) {
        const { attr, position } = attributeAndPositions[index]

        let valid = previousPosition <= position
        if (valid && alphabetical && previousPosition === position) {
          valid = isAlphabetical(previousNode, attr, sourceCode)
        }
        if (valid) {
          previousNode = attr
          previousPosition = position
        } else {
          reportIssue(attr, previousNode)
        }
      }
    }
  })

  /**
   * @param {VStartTag} node
   * @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
   */
  function getAttributeAndPositionList(node) {
    const attributes = node.attributes.filter((node, index, attributes) => {
      if (
        isVBindObject(node) &&
        (isVAttributeOrVBindOrVModel(attributes[index - 1]) ||
          isVAttributeOrVBindOrVModel(attributes[index + 1]))
      ) {
        // In Vue 3, ignore `v-bind="object"`, which is
        // a pair of `v-bind:foo="..."` and `v-bind="object"` and
        // a pair of `v-model="..."` and `v-bind="object"`,
        // because changing the order behaves differently.
        return false
      }
      return true
    })

    const results = []
    for (const [index, attr] of attributes.entries()) {
      const position = getPositionFromAttrIndex(index)
      if (position == null) {
        // The omitted order is skipped.
        continue
      }
      results.push({ attr, position })
    }

    return results

    /**
     * @param {number} index
     * @returns {number | null}
     */
    function getPositionFromAttrIndex(index) {
      const node = attributes[index]
      if (isVBindObject(node)) {
        // node is `v-bind ="object"` syntax

        // In Vue 3, if change the order of `v-bind:foo="..."`, `v-model="..."` and `v-bind="object"`,
        // the behavior will be different, so adjust so that there is no change in behavior.

        const len = attributes.length
        for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
          const next = attributes[nextIndex]

          if (isVAttributeOrVBindOrVModel(next) && !isVBindObject(next)) {
            // It is considered to be in the same order as the next bind prop node.
            return getPositionFromAttrIndex(nextIndex)
          }
        }
      }
      return getPosition(node, attributePosition)
    }
  }
}
@param {RuleContext} context - The rule context.
@returns {RuleListener} AST event handlers.