code:
/**
 * @fileoverview enforce sort-keys in a manner that is compatible with order-in-components
 * @author Loren Klingman
 * Original ESLint sort-keys by Toru Nagashima
 */
'use strict'

const naturalCompare = require('natural-compare')
const utils = require('../utils')

/**
 * Gets the property name of the given `Property` node.
 *
 * - If the property's key is an `Identifier` node, this returns the key's name
 *   whether it's a computed property or not.
 * - If the property has a static name, this returns the static name.
 * - Otherwise, this returns null.
 * @param {Property} node The `Property` node to get.
 * @returns {string|null} The property name or null.
 * @private
 */
function getPropertyName(node) {
  const staticName = utils.getStaticPropertyName(node)

  if (staticName !== null) {
    return staticName
  }

  return node.key.type === 'Identifier' ? node.key.name : null
}

/**
 * Functions which check that the given 2 names are in specific order.
 *
 * Postfix `I` is meant insensitive.
 * Postfix `N` is meant natural.
 * @private
 * @type { { [key: string]: (a:string, b:string) => boolean } }
 */
const isValidOrders = {
  asc(a, b) {
    return a <= b
  },
  ascI(a, b) {
    return a.toLowerCase() <= b.toLowerCase()
  },
  ascN(a, b) {
    return naturalCompare(a, b) <= 0
  },
  ascIN(a, b) {
    return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0
  },
  desc(a, b) {
    return isValidOrders.asc(b, a)
  },
  descI(a, b) {
    return isValidOrders.ascI(b, a)
  },
  descN(a, b) {
    return isValidOrders.ascN(b, a)
  },
  descIN(a, b) {
    return isValidOrders.ascIN(b, a)
  }
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'enforce sort-keys in a manner that is compatible with order-in-components',
      categories: null,
      recommended: false,
      url: 'https://eslint.vuejs.org/rules/sort-keys.html'
    },
    fixable: null,
    schema: [
      {
        enum: ['asc', 'desc']
      },
      {
        type: 'object',
        properties: {
          caseSensitive: {
            type: 'boolean',
            default: true
          },
          ignoreChildrenOf: {
            type: 'array'
          },
          ignoreGrandchildrenOf: {
            type: 'array'
          },
          minKeys: {
            type: 'integer',
            minimum: 2,
            default: 2
          },
          natural: {
            type: 'boolean',
            default: false
          },
          runOutsideVue: {
            type: 'boolean',
            default: true
          }
        },
        additionalProperties: false
      }
    ],
    messages: {
      sortKeys:
        "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'."
    }
  },
  /**
   * @param {RuleContext} context - The rule context.
   * @returns {RuleListener} AST event handlers.
   */
  create(context) {
    // Parse options.
    const options = context.options[1]
    const order = context.options[0] || 'asc'

    /** @type {Set<string>} */
    const ignoreGrandchildrenOf = new Set(
      (options && options.ignoreGrandchildrenOf) || [
        'computed',
        'directives',
        'inject',
        'props',
        'watch'
      ]
    )
    /** @type {Set<string>} */
    const ignoreChildrenOf = new Set(
      (options && options.ignoreChildrenOf) || ['model']
    )
    const insensitive = options && options.caseSensitive === false
    const minKeys = options && options.minKeys
    const natural = options && options.natural
    const isValidOrder =
      isValidOrders[order + (insensitive ? 'I' : '') + (natural ? 'N' : '')]

    /**
     * @typedef {object} ObjectStack
     * @property {ObjectStack | null} ObjectStack.upper
     * @property {string | null} ObjectStack.prevName
     * @property {number} ObjectStack.numKeys
     * @property {VueState} ObjectStack.vueState
     *
     * @typedef {object} VueState
     * @property {Property} [VueState.currentProperty]
     * @property {boolean} [VueState.isVueObject]
     * @property {boolean} [VueState.within]
     * @property {string} [VueState.propName]
     * @property {number} [VueState.chainLevel]
     * @property {boolean} [VueState.ignore]
     */

    /**
     * The stack to save the previous property's name for each object literals.
     * @type {ObjectStack | null}
     */
    let objectStack

    return {
      ObjectExpression(node) {
        /** @type {VueState} */
        const vueState = {}
        const upperVueState = (objectStack && objectStack.vueState) || {}
        objectStack = {
          upper: objectStack,
          prevName: null,
          numKeys: node.properties.length,
          vueState
        }

        vueState.isVueObject = utils.getVueObjectType(context, node) != null
        if (vueState.isVueObject) {
          vueState.within = vueState.isVueObject
          // Ignore Vue object properties
          vueState.ignore = true
        } else {
          if (upperVueState.within && upperVueState.currentProperty) {
            const isChain = utils.isPropertyChain(
              upperVueState.currentProperty,
              node
            )
            if (isChain) {
              let propName
              let chainLevel
              if (upperVueState.isVueObject) {
                propName =
                  utils.getStaticPropertyName(upperVueState.currentProperty) ||
                  ''
                chainLevel = 1
              } else {
                propName = upperVueState.propName || ''
                chainLevel = (upperVueState.chainLevel || 0) + 1
              }
              vueState.propName = propName
              vueState.chainLevel = chainLevel
              // chaining
              vueState.within = true

              // Judge whether to ignore the property.
              if (
                (chainLevel === 1 && ignoreChildrenOf.has(propName)) ||
                (chainLevel === 2 && ignoreGrandchildrenOf.has(propName))
              ) {
                vueState.ignore = true
              }
            } else {
              // chaining has broken.
              vueState.within = false
            }
          }
        }
      },
      'ObjectExpression:exit'() {
        objectStack = objectStack && objectStack.upper
      },
      SpreadElement(node) {
        if (!objectStack) {
          return
        }
        if (node.parent.type === 'ObjectExpression') {
          objectStack.prevName = null
        }
      },
      'ObjectExpression > Property'(node) {
        if (!objectStack) {
          return
        }
        objectStack.vueState.currentProperty = node
        if (objectStack.vueState.ignore) {
          return
        }
        const prevName = objectStack.prevName
        const numKeys = objectStack.numKeys
        const thisName = getPropertyName(node)

        if (thisName !== null) {
          objectStack.prevName = thisName
        }

        if (prevName === null || thisName === null || numKeys < minKeys) {
          return
        }

        if (!isValidOrder(prevName, thisName)) {
          context.report({
            node,
            loc: node.key.loc,
            messageId: 'sortKeys',
            data: {
              thisName,
              prevName,
              order,
              insensitive: insensitive ? 'insensitive ' : '',
              natural: natural ? 'natural ' : ''
            }
          })
        }
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 */
'use strict'

const { wrapStylisticOrCoreRule } = require('../utils')

// eslint-disable-next-line internal/no-invalid-meta
module.exports = wrapStylisticOrCoreRule('space-in-parens', {
  skipDynamicArguments: true,
  skipDynamicArgumentsReport: true,
  applyDocument: true
})

license: mit


code:
/**
 * @author Toru Nagashima
 */
'use strict'

const { wrapStylisticOrCoreRule } = require('../utils')

// eslint-disable-next-line internal/no-invalid-meta
module.exports = wrapStylisticOrCoreRule('space-infix-ops', {
  skipDynamicArguments: true,
  applyDocument: true
})


code:
/**
 * @author Toru Nagashima
 */
'use strict'

const { wrapStylisticOrCoreRule } = require('../utils')

// eslint-disable-next-line internal/no-invalid-meta
module.exports = wrapStylisticOrCoreRule('space-unary-ops', {
  skipDynamicArguments: true,
  applyDocument: true
})


code:
/**
 * @fileoverview Alphabetizes static class names.
 * @author Maciej Chmurski
 */
'use strict'

const { defineTemplateBodyVisitor } = require('../utils')

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      url: 'https://eslint.vuejs.org/rules/static-class-names-order.html',
      description: 'enforce static class names order',
      categories: undefined
    },
    fixable: 'code',
    schema: [],
    messages: {
      shouldBeOrdered: 'Classes should be ordered alphabetically.'
    }
  },
  /** @param {RuleContext} context */
  create: (context) =>
    defineTemplateBodyVisitor(context, {
      /** @param {VAttribute} node */
      "VAttribute[directive=false][key.name='class']"(node) {
        const value = node.value
        if (!value) {
          return
        }
        const classList = value.value
        const classListWithWhitespace = classList.split(/(\s+)/)

        // Detect and reuse any type of whitespace.
        let divider = ''
        if (classListWithWhitespace.length > 1) {
          divider = classListWithWhitespace[1]
        }

        const classListNoWhitespace = classListWithWhitespace.filter(
          (className) => className.trim() !== ''
        )
        const classListSorted = classListNoWhitespace.sort().join(divider)

        if (classList !== classListSorted) {
          context.report({
            node,
            loc: node.loc,
            messageId: 'shouldBeOrdered',
            fix: (fixer) => fixer.replaceText(value, `"${classListSorted}"`)
          })
        }
      }
    })
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../utils/index')

module.exports = {
  supported: '>=3.4.0',
  /** @param {RuleContext} context @returns {RuleListener} */
  createScriptVisitor(context) {
    return utils.defineScriptSetupVisitor(context, {
      onDefineModelEnter(node) {
        context.report({
          node,
          messageId: 'forbiddenDefineModel'
        })
      }
    })
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../utils/index')

module.exports = {
  supported: '>=3.3.0',
  /** @param {RuleContext} context @returns {RuleListener} */
  createScriptVisitor(context) {
    const sourceCode = context.getSourceCode()
    return utils.defineScriptSetupVisitor(context, {
      onDefineOptionsEnter(node) {
        context.report({
          node,
          messageId: 'forbiddenDefineOptions',
          fix(fixer) {
            return fix(fixer, node)
          }
        })
      }
    })

    /**
     * @param {RuleFixer} fixer
     * @param {CallExpression} node defineOptions() node
     */
    function fix(fixer, node) {
      if (node.arguments.length === 0) return null
      const scriptSetup = utils.getScriptSetupElement(context)
      if (!scriptSetup) return null
      if (
        scriptSetup.parent.children
          .filter(utils.isVElement)
          .some(
            (node) =>
              node.name === 'script' && !utils.hasAttribute(node, 'setup')
          )
      ) {
        // has `<script>`
        return null
      }

      // Find defineOptions statement
      /** @type {ASTNode} */
      let statement = node
      while (statement.parent && statement.parent.type !== 'Program') {
        statement = statement.parent
      }
      // Calc remove range
      /** @type {Range} */
      const removeRange = [...statement.range]
      if (
        sourceCode.lines[statement.loc.start.line - 1]
          .slice(0, statement.loc.start.column)
          .trim() === ''
      ) {
        removeRange[0] -= statement.loc.start.column
      }

      return [
        fixer.insertTextBefore(
          scriptSetup,
          `<script>\nexport default ${sourceCode.getText(
            node.arguments[0]
          )}\n</script>\n`
        ),
        fixer.removeRange(removeRange)
      ]
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../utils/index')

module.exports = {
  supported: '>=3.3.0',
  /** @param {RuleContext} context @returns {RuleListener} */
  createScriptVisitor(context) {
    return utils.defineScriptSetupVisitor(context, {
      onDefineSlotsEnter(node) {
        context.report({
          node,
          messageId: 'forbiddenDefineSlots'
        })
      }
    })
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'
module.exports = {
  supported: '>=2.6.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports dynamic argument node
     * @param {VExpressionContainer} dynamicArgument node of dynamic argument
     * @returns {void}
     */
    function reportDynamicArgument(dynamicArgument) {
      context.report({
        node: dynamicArgument,
        messageId: 'forbiddenDynamicDirectiveArguments'
      })
    }

    return {
      'VAttribute[directive=true] > VDirectiveKey > VExpressionContainer':
        reportDynamicArgument
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'
module.exports = {
  supported: '>=3.1.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    return {
      /** @param {VAttribute} node */
      "VAttribute[directive=false][key.name='is']"(node) {
        if (!node.value) {
          return
        }
        if (node.value.value.startsWith('vue:')) {
          context.report({
            node: node.value,
            messageId: 'forbiddenIsAttributeWithVuePrefix'
          })
        }
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'
module.exports = {
  deprecated: '2.5.0',
  supported: '<3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports `scope` node
     * @param {VDirectiveKey} scopeKey node of `scope`
     * @returns {void}
     */
    function reportScope(scopeKey) {
      context.report({
        node: scopeKey,
        messageId: 'forbiddenScopeAttribute',
        // fix to use `slot-scope`
        fix: (fixer) => fixer.replaceText(scopeKey, 'slot-scope')
      })
    }

    return {
      "VAttribute[directive=true] > VDirectiveKey[name.name='scope']":
        reportScope
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../utils')

module.exports = {
  supported: '>=2.7.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createScriptVisitor(context) {
    const scriptSetup = utils.getScriptSetupElement(context)
    if (!scriptSetup) {
      return {}
    }
    const reportNode =
      utils.getAttribute(scriptSetup, 'setup') || scriptSetup.startTag
    return {
      Program() {
        context.report({
          node: reportNode,
          messageId: 'forbiddenScriptSetup'
        })
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const canConvertToVSlot = require('./utils/can-convert-to-v-slot')
const casing = require('../../utils/casing')

module.exports = {
  deprecated: '2.6.0',
  supported: '<3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    const options = context.options[0] || {}
    /** @type {Set<string>} */
    const ignore = new Set(options.ignore)

    const sourceCode = context.getSourceCode()
    const tokenStore =
      sourceCode.parserServices.getTemplateBodyTokenStore &&
      sourceCode.parserServices.getTemplateBodyTokenStore()

    /**
     * Checks whether the given node can convert to the `v-slot`.
     * @param {VAttribute} slotAttr node of `slot`
     * @returns {boolean} `true` if the given node can convert to the `v-slot`
     */
    function canConvertFromSlotToVSlot(slotAttr) {
      if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
        return false
      }
      if (!slotAttr.value) {
        return true
      }
      const slotName = slotAttr.value.value
      // If other than alphanumeric, underscore and hyphen characters are included it can not be converted.
      return !/[^\w\-]/u.test(slotName)
    }

    /**
     * Checks whether the given node can convert to the `v-slot`.
     * @param {VDirective} slotAttr node of `v-bind:slot`
     * @returns {boolean} `true` if the given node can convert to the `v-slot`
     */
    function canConvertFromVBindSlotToVSlot(slotAttr) {
      if (!canConvertToVSlot(slotAttr.parent.parent, sourceCode, tokenStore)) {
        return false
      }

      if (!slotAttr.value) {
        return true
      }

      if (!slotAttr.value.expression) {
        // parse error or empty expression
        return false
      }

      return slotAttr.value.expression.type === 'Identifier'
    }

    /**
     * Convert to `v-slot`.
     * @param {RuleFixer} fixer fixer
     * @param {VAttribute|VDirective} slotAttr node of `slot`
     * @param {string | null} slotName name of `slot`
     * @param {boolean} vBind `true` if `slotAttr` is `v-bind:slot`
     * @returns {IterableIterator<Fix>} fix data
     */
    function* fixSlotToVSlot(fixer, slotAttr, slotName, vBind) {
      const startTag = slotAttr.parent
      const scopeAttr = startTag.attributes.find(
        (attr) =>
          attr.directive === true &&
          attr.key.name &&
          (attr.key.name.name === 'slot-scope' ||
            attr.key.name.name === 'scope')
      )
      let nameArgument = ''
      if (slotName) {
        nameArgument = vBind ? `:[${slotName}]` : `:${slotName}`
      }
      const scopeValue =
        scopeAttr && scopeAttr.value
          ? `=${sourceCode.getText(scopeAttr.value)}`
          : ''

      const replaceText = `v-slot${nameArgument}${scopeValue}`

      const element = startTag.parent
      if (element.name === 'template') {
        yield fixer.replaceText(slotAttr || scopeAttr, replaceText)
        if (slotAttr && scopeAttr) {
          yield fixer.remove(scopeAttr)
        }
      } else {
        yield fixer.remove(slotAttr || scopeAttr)
        if (slotAttr && scopeAttr) {
          yield fixer.remove(scopeAttr)
        }

        const vFor = startTag.attributes.find(
          (attr) => attr.directive && attr.key.name.name === 'for'
        )
        const vForText = vFor ? `${sourceCode.getText(vFor)} ` : ''
        if (vFor) {
          yield fixer.remove(vFor)
        }

        yield fixer.insertTextBefore(
          element,
          `<template ${vForText}${replaceText}>\n`
        )
        yield fixer.insertTextAfter(element, `\n</template>`)
      }
    }
    /**
     * Reports `slot` node
     * @param {VAttribute} slotAttr node of `slot`
     * @returns {void}
     */
    function reportSlot(slotAttr) {
      const componentName = slotAttr.parent.parent.rawName
      if (
        ignore.has(componentName) ||
        ignore.has(casing.pascalCase(componentName)) ||
        ignore.has(casing.kebabCase(componentName))
      ) {
        return
      }

      context.report({
        node: slotAttr.key,
        messageId: 'forbiddenSlotAttribute',
        // fix to use `v-slot`
        *fix(fixer) {
          if (!canConvertFromSlotToVSlot(slotAttr)) {
            return
          }
          const slotName = slotAttr.value && slotAttr.value.value
          yield* fixSlotToVSlot(fixer, slotAttr, slotName, false)
        }
      })
    }
    /**
     * Reports `v-bind:slot` node
     * @param {VDirective} slotAttr node of `v-bind:slot`
     * @returns {void}
     */
    function reportVBindSlot(slotAttr) {
      context.report({
        node: slotAttr.key,
        messageId: 'forbiddenSlotAttribute',
        // fix to use `v-slot`
        *fix(fixer) {
          if (!canConvertFromVBindSlotToVSlot(slotAttr)) {
            return
          }
          const slotName =
            slotAttr.value &&
            slotAttr.value.expression &&
            sourceCode.getText(slotAttr.value.expression).trim()
          yield* fixSlotToVSlot(fixer, slotAttr, slotName, true)
        }
      })
    }

    return {
      "VAttribute[directive=false][key.name='slot']": reportSlot,
      "VAttribute[directive=true][key.name.name='bind'][key.argument.name='slot']":
        reportVBindSlot
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const canConvertToVSlotForElement = require('./utils/can-convert-to-v-slot')

module.exports = {
  deprecated: '2.6.0',
  supported: '>=2.5.0 <3.0.0',
  /**
   * @param {RuleContext} context
   * @param {object} option
   * @param {boolean} [option.fixToUpgrade]
   * @returns {TemplateListener}
   */
  createTemplateBodyVisitor(context, { fixToUpgrade } = {}) {
    const sourceCode = context.getSourceCode()
    const tokenStore =
      sourceCode.parserServices.getTemplateBodyTokenStore &&
      sourceCode.parserServices.getTemplateBodyTokenStore()

    /**
     * Checks whether the given node can convert to the `v-slot`.
     * @param {VStartTag} startTag node of `<element v-slot ... >`
     * @returns {boolean} `true` if the given node can convert to the `v-slot`
     */
    function canConvertToVSlot(startTag) {
      if (
        !canConvertToVSlotForElement(startTag.parent, sourceCode, tokenStore)
      ) {
        return false
      }

      const slotAttr = startTag.attributes.find(
        (attr) => attr.directive === false && attr.key.name === 'slot'
      )
      if (slotAttr) {
        // if the element have `slot` it can not be converted.
        // Conversion of `slot` is done with `vue/no-deprecated-slot-attribute`.
        return false
      }

      const vBindSlotAttr = startTag.attributes.find(
        (attr) =>
          attr.directive === true &&
          attr.key.name.name === 'bind' &&
          attr.key.argument &&
          attr.key.argument.type === 'VIdentifier' &&
          attr.key.argument.name === 'slot'
      )
      if (vBindSlotAttr) {
        // if the element have `v-bind:slot` it can not be converted.
        // Conversion of `v-bind:slot` is done with `vue/no-deprecated-slot-attribute`.
        return false
      }
      return true
    }

    /**
     * Convert to `v-slot`.
     * @param {RuleFixer} fixer fixer
     * @param {VDirective} scopeAttr node of `slot-scope`
     * @returns {Fix[]} fix data
     */
    function fixSlotScopeToVSlot(fixer, scopeAttr) {
      const element = scopeAttr.parent.parent
      const scopeValue =
        scopeAttr && scopeAttr.value
          ? `=${sourceCode.getText(scopeAttr.value)}`
          : ''

      const replaceText = `v-slot${scopeValue}`
      if (element.name === 'template') {
        return [fixer.replaceText(scopeAttr, replaceText)]
      } else {
        const tokenBefore = tokenStore.getTokenBefore(scopeAttr)
        return [
          fixer.removeRange([tokenBefore.range[1], scopeAttr.range[1]]),
          fixer.insertTextBefore(element, `<template ${replaceText}>\n`),
          fixer.insertTextAfter(element, `\n</template>`)
        ]
      }
    }
    /**
     * Reports `slot-scope` node
     * @param {VDirective} scopeAttr node of `slot-scope`
     * @returns {void}
     */
    function reportSlotScope(scopeAttr) {
      context.report({
        node: scopeAttr.key,
        messageId: 'forbiddenSlotScopeAttribute',
        fix(fixer) {
          if (!fixToUpgrade) {
            return null
          }
          // fix to use `v-slot`
          const startTag = scopeAttr.parent
          if (!canConvertToVSlot(startTag)) {
            return null
          }
          return fixSlotScopeToVSlot(fixer, scopeAttr)
        }
      })
    }

    return {
      "VAttribute[directive=true][key.name.name='slot-scope']": reportSlotScope
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const { getStyleVariablesContext } = require('../../utils/style-variables')

module.exports = {
  supported: '>=3.0.3 || >=2.7.0 <3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createScriptVisitor(context) {
    const styleVars = getStyleVariablesContext(context)
    if (!styleVars) {
      return {}
    }
    return {
      Program() {
        for (const vBind of styleVars.vBinds) {
          context.report({
            node: vBind,
            messageId: 'forbiddenStyleCssVarsInjection'
          })
        }
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../../utils')
/**
 * @typedef {object} SlotVForVariables
 * @property {VForExpression} expr
 * @property {VVariable[]} variables
 */
/**
 * @typedef {object} SlotContext
 * @property {VElement} element
 * @property {VAttribute | VDirective | null} slot
 * @property {VDirective | null} vFor
 * @property {SlotVForVariables | null} slotVForVars
 * @property {string} normalizedName
 */
/**
 * Checks whether the given element can use v-slot.
 * @param {VElement} element
 * @param {SourceCode} sourceCode
 * @param {ParserServices.TokenStore} tokenStore
 */
module.exports = function canConvertToVSlot(element, sourceCode, tokenStore) {
  const ownerElement = element.parent
  if (
    ownerElement.type === 'VDocumentFragment' ||
    !utils.isCustomComponent(ownerElement) ||
    ownerElement.name === 'component'
  ) {
    return false
  }
  const slot = getSlotContext(element, sourceCode)
  if (slot.vFor && !slot.slotVForVars) {
    // E.g., <template v-for="x of xs" #one></template>
    return false
  }
  if (hasSameSlotDirective(ownerElement, slot, sourceCode, tokenStore)) {
    return false
  }
  return true
}
/**
 * @param {VElement} element
 * @param {SourceCode} sourceCode
 * @returns {SlotContext}
 */
function getSlotContext(element, sourceCode) {
  const slot =
    utils.getAttribute(element, 'slot') ||
    utils.getDirective(element, 'bind', 'slot')
  const vFor = utils.getDirective(element, 'for')
  const slotVForVars = getSlotVForVariableIfUsingIterationVars(slot, vFor)

  return {
    element,
    slot,
    vFor,
    slotVForVars,
    normalizedName: getNormalizedName(slot, sourceCode)
  }
}

/**
 * Gets the `v-for` directive and variable that provide the variables used by the given `slot` attribute.
 * @param {VAttribute | VDirective | null} slot The current `slot` attribute node.
 * @param {VDirective | null} [vFor] The current `v-for` directive node.
 * @returns { SlotVForVariables | null } The SlotVForVariables.
 */
function getSlotVForVariableIfUsingIterationVars(slot, vFor) {
  if (!slot || !slot.directive) {
    return null
  }
  const expr =
    vFor && vFor.value && /** @type {VForExpression} */ (vFor.value.expression)
  const variables =
    expr && getUsingIterationVars(slot.value, slot.parent.parent)
  return expr && variables && variables.length > 0 ? { expr, variables } : null
}

/**
 * Gets iterative variables if a given expression node is using iterative variables that the element defined.
 * @param {VExpressionContainer|null} expression The expression node to check.
 * @param {VElement} element The element node which has the expression.
 * @returns {VVariable[]} The expression node is using iteration variables.
 */
function getUsingIterationVars(expression, element) {
  const vars = []
  if (expression && expression.type === 'VExpressionContainer') {
    for (const { variable } of expression.references) {
      if (
        variable != null &&
        variable.kind === 'v-for' &&
        variable.id.range[0] > element.startTag.range[0] &&
        variable.id.range[1] < element.startTag.range[1]
      ) {
        vars.push(variable)
      }
    }
  }
  return vars
}

/**
 * Get the normalized name of a given `slot` attribute node.
 * @param {VAttribute | VDirective | null} slotAttr node of `slot`
 * @param {SourceCode} sourceCode The source code.
 * @returns {string} The normalized name.
 */
function getNormalizedName(slotAttr, sourceCode) {
  if (!slotAttr) {
    return 'default'
  }
  if (!slotAttr.directive) {
    return slotAttr.value ? slotAttr.value.value : 'default'
  }
  return slotAttr.value ? `[${sourceCode.getText(slotAttr.value)}]` : '[null]'
}

/**
 * Checks whether parent element has the same slot as the given slot.
 * @param {VElement} ownerElement The parent element.
 * @param {SlotContext} targetSlot The SlotContext with a slot to check if they are the same.
 * @param {SourceCode} sourceCode
 * @param {ParserServices.TokenStore} tokenStore
 */
function hasSameSlotDirective(
  ownerElement,
  targetSlot,
  sourceCode,
  tokenStore
) {
  for (const group of utils.iterateChildElementsChains(ownerElement)) {
    if (group.includes(targetSlot.element)) {
      continue
    }
    for (const childElement of group) {
      const slot = getSlotContext(childElement, sourceCode)
      if (!targetSlot.slotVForVars || !slot.slotVForVars) {
        if (
          !targetSlot.slotVForVars &&
          !slot.slotVForVars &&
          targetSlot.normalizedName === slot.normalizedName
        ) {
          return true
        }
        continue
      }
      if (
        equalSlotVForVariables(
          targetSlot.slotVForVars,
          slot.slotVForVars,
          tokenStore
        )
      ) {
        return true
      }
    }
  }
  return false
}

/**
 * Determines whether the two given `v-slot` variables are considered to be equal.
 * @param {SlotVForVariables} a First element.
 * @param {SlotVForVariables} b Second element.
 * @param {ParserServices.TokenStore} tokenStore The token store.
 * @returns {boolean} `true` if the elements are considered to be equal.
 */
function equalSlotVForVariables(a, b, tokenStore) {
  if (a.variables.length !== b.variables.length) {
    return false
  }
  if (!equal(a.expr.right, b.expr.right)) {
    return false
  }

  const checkedVarNames = new Set()
  const len = Math.min(a.expr.left.length, b.expr.left.length)
  for (let index = 0; index < len; index++) {
    const aPtn = a.expr.left[index]
    const bPtn = b.expr.left[index]

    const aVar = a.variables.find(
      (v) => aPtn.range[0] <= v.id.range[0] && v.id.range[1] <= aPtn.range[1]
    )
    const bVar = b.variables.find(
      (v) => bPtn.range[0] <= v.id.range[0] && v.id.range[1] <= bPtn.range[1]
    )
    if (aVar && bVar) {
      if (aVar.id.name !== bVar.id.name) {
        return false
      }
      if (!equal(aPtn, bPtn)) {
        return false
      }
      checkedVarNames.add(aVar.id.name)
    } else if (aVar || bVar) {
      return false
    }
  }
  return a.variables.every(
    (v) =>
      checkedVarNames.has(v.id.name) ||
      b.variables.some((bv) => v.id.name === bv.id.name)
  )

  /**
   * Determines whether the two given nodes are considered to be equal.
   * @param {ASTNode} a First node.
   * @param {ASTNode} b Second node.
   * @returns {boolean} `true` if the nodes are considered to be equal.
   */
  function equal(a, b) {
    if (a.type !== b.type) {
      return false
    }
    return utils.equalTokens(a, b, tokenStore)
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

module.exports = {
  supported: '>=3.2.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports `v-bind.attr` node
     * @param { VIdentifier } mod node of `v-bind.attr`
     * @returns {void}
     */
    function report(mod) {
      context.report({
        node: mod,
        messageId: 'forbiddenVBindAttrModifier'
      })
    }

    return {
      "VAttribute[directive=true][key.name.name='bind']"(node) {
        const attrMod = node.key.modifiers.find((m) => m.name === 'attr')
        if (attrMod) {
          report(attrMod)
        }
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

module.exports = {
  supported: '>=3.2.0 || >=2.6.0-beta.1 <=2.6.0-beta.3',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports `.prop` shorthand node
     * @param { VDirectiveKey & { argument: VIdentifier } } bindPropKey node of `.prop` shorthand
     * @returns {void}
     */
    function reportPropModifierShorthand(bindPropKey) {
      context.report({
        node: bindPropKey,
        messageId: 'forbiddenVBindPropModifierShorthand',
        // fix to use `:x.prop` (downgrade)
        fix: (fixer) =>
          fixer.replaceText(
            bindPropKey,
            `:${bindPropKey.argument.rawName}.prop`
          )
      })
    }

    return {
      "VAttribute[directive=true] > VDirectiveKey[name.name='bind'][name.rawName='.']":
        reportPropModifierShorthand
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../../utils')

module.exports = {
  supported: '>=3.4.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Verify the directive node
     * @param {VDirective} node The directive node to check
     * @returns {void}
     */
    function checkDirective(node) {
      if (utils.isVBindSameNameShorthand(node)) {
        context.report({
          node,
          messageId: 'forbiddenVBindSameNameShorthand',
          // fix to use `:x="x"` (downgrade)
          fix: (fixer) =>
            fixer.insertTextAfter(node, `="${node.value.expression.name}"`)
        })
      }
    }

    return {
      "VAttribute[directive=true][key.name.name='bind']": checkDirective
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'
module.exports = {
  deprecated: '3.1.0',
  supported: '>=3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports `v-is` node
     * @param {VDirective} vIsAttr node of `v-is`
     * @returns {void}
     */
    function reportVIs(vIsAttr) {
      context.report({
        node: vIsAttr.key,
        messageId: 'forbiddenVIs'
      })
    }

    return {
      "VAttribute[directive=true][key.name.name='is']": reportVIs
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'
module.exports = {
  supported: '>=3.2.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    /**
     * Reports `v-is` node
     * @param {VDirective} vMemoAttr node of `v-is`
     * @returns {void}
     */
    function reportVMemo(vMemoAttr) {
      context.report({
        node: vMemoAttr.key,
        messageId: 'forbiddenVMemo'
      })
    }

    return {
      "VAttribute[directive=true][key.name.name='memo']": reportVMemo
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

module.exports = {
  supported: '>=3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    return {
      /** @param {VDirectiveKey & { argument: VExpressionContainer | VIdentifier }} node */
      "VAttribute[directive=true] > VDirectiveKey[name.name='model'][argument!=null]"(
        node
      ) {
        context.report({
          node: node.argument,
          messageId: 'forbiddenVModelArgument'
        })
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

const BUILTIN_MODIFIERS = new Set(['lazy', 'number', 'trim'])

module.exports = {
  supported: '>=3.0.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    return {
      /** @param {VDirectiveKey} node */
      "VAttribute[directive=true] > VDirectiveKey[name.name='model'][modifiers.length>0]"(
        node
      ) {
        for (const modifier of node.modifiers) {
          if (!BUILTIN_MODIFIERS.has(modifier.name)) {
            context.report({
              node: modifier,
              messageId: 'forbiddenVModelCustomModifiers'
            })
          }
        }
      }
    }
  }
}


code:
/**
 * @author Yosuke Ota
 * See LICENSE file in root directory for full license.
 */
'use strict'

/**
 * Checks whether the given node can convert to the `slot`.
 * @param {VDirective} vSlotAttr node of `v-slot`
 * @returns {boolean} `true` if the given node can convert to the `slot`
 */
function canConvertToSlot(vSlotAttr) {
  return vSlotAttr.parent.parent.name === 'template'
}

module.exports = {
  supported: '>=2.6.0',
  /** @param {RuleContext} context @returns {TemplateListener} */
  createTemplateBodyVisitor(context) {
    const sourceCode = context.getSourceCode()

    /**
     * Convert to `slot` and `slot-scope`.
     * @param {RuleFixer} fixer fixer
     * @param {VDirective} vSlotAttr node of `v-slot`
     * @returns {null|Fix} fix data
     */
    function fixVSlotToSlot(fixer, vSlotAttr) {
      const key = vSlotAttr.key
      if (key.modifiers.length > 0) {
        // unknown modifiers
        return null
      }

      const attrs = []
      const argument = key.argument
      if (argument) {
        if (argument.type === 'VIdentifier') {
          const name = argument.rawName
          attrs.push(`slot="${name}"`)
        } else if (
          argument.type === 'VExpressionContainer' &&
          argument.expression
        ) {
          const expression = sourceCode.getText(argument.expression)
          attrs.push(`:slot="${expression}"`)
        } else {
          // unknown or syntax error
          return null
        }
      }
      const scopedValueNode = vSlotAttr.value
      if (scopedValueNode) {
        attrs.push(`slot-scope=${sourceCode.getText(scopedValueNode)}`)
      }
      if (attrs.length === 0) {
        attrs.push('slot') // useless
      }
      return fixer.replaceText(vSlotAttr, attrs.join(' '))
    }
    /**
     * Reports `v-slot` node
     * @param {VDirective} vSlotAttr node of `v-slot`
     * @returns {void}
     */
    function reportVSlot(vSlotAttr) {
      context.report({
        node: vSlotAttr.key,
        messageId: 'forbiddenVSlot',
        // fix to use `slot` (downgrade)
        fix(fixer) {
          if (!canConvertToSlot(vSlotAttr)) {
            return null
          }
          return fixVSlotToSlot(fixer, vSlotAttr)
        }
      })
    }

    return {
      "VAttribute[directive=true][key.name.name='slot']": reportVSlot
    }
  }
}


code:
/**
 * @author Yosuke Ota
 */
'use strict'

const { wrapStylisticOrCoreRule } = require('../utils')

// eslint-disable-next-line internal/no-invalid-meta
module.exports = wrapStylisticOrCoreRule('template-curly-spacing', {
  skipDynamicArguments: true,
  applyDocument: true
})


code:
/**
 * @fileoverview disallow usage of `this` in template.
 * @author Armano
 */
'use strict'

const utils = require('../utils')
const RESERVED_NAMES = new Set(require('../utils/js-reserved.json'))

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'disallow usage of `this` in template',
      categories: ['vue3-recommended', 'vue2-recommended'],
      url: 'https://eslint.vuejs.org/rules/this-in-template.html'
    },
    fixable: 'code',
    schema: [
      {
        enum: ['always', 'never']
      }
    ],
    messages: {
      unexpected: "Unexpected usage of 'this'.",
      expected: "Expected 'this'."
    }
  },

  /**
   * Creates AST event handlers for this-in-template.
   *
   * @param {RuleContext} context - The rule context.
   * @returns {Object} AST event handlers.
   */
  create(context) {
    const options = context.options[0] === 'always' ? 'always' : 'never'
    /**
     * @typedef {object} ScopeStack
     * @property {ScopeStack | null} parent
     * @property {Identifier[]} nodes
     */

    /** @type {ScopeStack | null} */
    let scopeStack = null

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VElement} node */
      VElement(node) {
        scopeStack = {
          parent: scopeStack,
          nodes: scopeStack
            ? [...scopeStack.nodes] // make copy
            : []
        }
        if (node.variables) {
          for (const variable of node.variables) {
            const varNode = variable.id
            const name = varNode.name
            if (!scopeStack.nodes.some((node) => node.name === name)) {
              // Prevent adding duplicates
              scopeStack.nodes.push(varNode)
            }
          }
        }
      },
      'VElement:exit'() {
        scopeStack = scopeStack && scopeStack.parent
      },
      ...(options === 'never'
        ? {
            /** @param { ThisExpression & { parent: MemberExpression } } node */
            'VExpressionContainer MemberExpression > ThisExpression'(node) {
              if (!scopeStack) {
                return
              }
              const propertyName = utils.getStaticPropertyName(node.parent)
              if (
                !propertyName ||
                scopeStack.nodes.some((el) => el.name === propertyName) ||
                RESERVED_NAMES.has(propertyName) || // this.class | this['class']
                /^\d.*$|[^\w$]/.test(propertyName) // this['0aaaa'] | this['foo-bar bas']
              ) {
                return
              }

              context.report({
                node,
                loc: node.loc,
                fix(fixer) {
                  // node.parent should be some code like `this.test`, `this?.['result']`
                  return fixer.replaceText(node.parent, propertyName)
                },
                messageId: 'unexpected'
              })
            }
          }
        : {
            /** @param {VExpressionContainer} node */
            VExpressionContainer(node) {
              if (!scopeStack) {
                return
              }
              if (node.parent.type === 'VDirectiveKey') {
                // We cannot use `.` in dynamic arguments because the right of the `.` becomes a modifier.
                // For example, In `:[this.prop]` case, `:[this` is an argument and `prop]` is a modifier.
                return
              }
              if (node.references) {
                for (const reference of node.references) {
                  if (
                    !scopeStack.nodes.some(
                      (el) => el.name === reference.id.name
                    )
                  ) {
                    context.report({
                      node: reference.id,
                      loc: reference.id.loc,
                      messageId: 'expected',
                      fix(fixer) {
                        return fixer.insertTextBefore(reference.id, 'this.')
                      }
                    })
                  }
                }
              }
            }
          })
    })
  }
}


code:
/**
 * @fileoverview enforce usage of `exact` modifier on `v-on`.
 * @author Armano
 */
'use strict'

/**
 * @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective
 */

const utils = require('../utils')

const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta'])
const GLOBAL_MODIFIERS = new Set([
  'stop',
  'prevent',
  'capture',
  'self',
  'once',
  'passive',
  'native'
])

/**
 * Finds and returns all keys for event directives
 *
 * @param {VStartTag} startTag Element startTag
 * @param {SourceCode} sourceCode The source code object.
 * @returns {EventDirective[]} [{ name, node, modifiers }]
 */
function getEventDirectives(startTag, sourceCode) {
  return utils.getDirectives(startTag, 'on').map((attribute) => ({
    name: attribute.key.argument
      ? sourceCode.getText(attribute.key.argument)
      : '',
    node: attribute.key,
    modifiers: attribute.key.modifiers.map((modifier) => modifier.name)
  }))
}

/**
 * Checks whether given modifier is key modifier
 *
 * @param {string} modifier
 * @returns {boolean}
 */
function isKeyModifier(modifier) {
  return !GLOBAL_MODIFIERS.has(modifier) && !SYSTEM_MODIFIERS.has(modifier)
}

/**
 * Checks whether given modifier is system one
 *
 * @param {string} modifier
 * @returns {boolean}
 */
function isSystemModifier(modifier) {
  return SYSTEM_MODIFIERS.has(modifier)
}

/**
 * Checks whether given any of provided modifiers
 * has system modifier
 *
 * @param {string[]} modifiers
 * @returns {boolean}
 */
function hasSystemModifier(modifiers) {
  return modifiers.some(isSystemModifier)
}

/**
 * Groups all events in object,
 * with keys represinting each event name
 *
 * @param {EventDirective[]} events
 * @returns { { [key: string]: EventDirective[] } } { click: [], keypress: [] }
 */
function groupEvents(events) {
  /** @type { { [key: string]: EventDirective[] } } */
  const grouped = {}
  for (const event of events) {
    if (!grouped[event.name]) {
      grouped[event.name] = []
    }
    grouped[event.name].push(event)
  }
  return grouped
}

/**
 * Creates alphabetically sorted string with system modifiers
 *
 * @param {string[]} modifiers
 * @returns {string} e.g. "alt,ctrl,del,shift"
 */
function getSystemModifiersString(modifiers) {
  return modifiers.filter(isSystemModifier).sort().join(',')
}

/**
 * Creates alphabetically sorted string with key modifiers
 *
 * @param {string[]} modifiers
 * @returns {string} e.g. "enter,tab"
 */
function getKeyModifiersString(modifiers) {
  return modifiers.filter(isKeyModifier).sort().join(',')
}

/**
 * Compares two events based on their modifiers
 * to detect possible event leakeage
 *
 * @param {EventDirective} baseEvent
 * @param {EventDirective} event
 * @returns {boolean}
 */
function hasConflictedModifiers(baseEvent, event) {
  if (event.node === baseEvent.node || event.modifiers.includes('exact'))
    return false

  const eventKeyModifiers = getKeyModifiersString(event.modifiers)
  const baseEventKeyModifiers = getKeyModifiersString(baseEvent.modifiers)

  if (
    eventKeyModifiers &&
    baseEventKeyModifiers &&
    eventKeyModifiers !== baseEventKeyModifiers
  )
    return false

  const eventSystemModifiers = getSystemModifiersString(event.modifiers)
  const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers)

  return (
    baseEvent.modifiers.length > 0 &&
    baseEventSystemModifiers !== eventSystemModifiers &&
    baseEventSystemModifiers.includes(eventSystemModifiers)
  )
}

/**
 * Searches for events that might conflict with each other
 *
 * @param {EventDirective[]} events
 * @returns {EventDirective[]} conflicted events, without duplicates
 */
function findConflictedEvents(events) {
  /** @type {EventDirective[]} */
  const conflictedEvents = []
  for (const event of events) {
    conflictedEvents.push(
      ...events
        .filter((evt) => !conflictedEvents.includes(evt)) // No duplicates
        .filter(hasConflictedModifiers.bind(null, event))
    )
  }
  return conflictedEvents
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce usage of `exact` modifier on `v-on`',
      categories: ['vue3-essential', 'vue2-essential'],
      url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html'
    },
    fixable: null,
    schema: [],
    messages: {
      considerExact: "Consider to use '.exact' modifier."
    }
  },

  /**
   * Creates AST event handlers for use-v-on-exact.
   *
   * @param {RuleContext} context - The rule context.
   * @returns {Object} AST event handlers.
   */
  create(context) {
    const sourceCode = context.getSourceCode()

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VStartTag} node */
      VStartTag(node) {
        if (node.attributes.length === 0) return

        const isCustomComponent = utils.isCustomComponent(node.parent)
        let events = getEventDirectives(node, sourceCode)

        if (isCustomComponent) {
          // For components consider only events with `native` modifier
          events = events.filter((event) => event.modifiers.includes('native'))
        }

        const grouppedEvents = groupEvents(events)

        for (const eventName of Object.keys(grouppedEvents)) {
          const eventsInGroup = grouppedEvents[eventName]
          const hasEventWithKeyModifier = eventsInGroup.some((event) =>
            hasSystemModifier(event.modifiers)
          )

          if (!hasEventWithKeyModifier) continue

          const conflictedEvents = findConflictedEvents(eventsInGroup)

          for (const e of conflictedEvents) {
            context.report({
              node: e.node,
              loc: e.node.loc,
              messageId: 'considerExact'
            })
          }
        }
      }
    })
  }
}


code:
/**
 * @author Toru Nagashima
 * @copyright 2017 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../utils')
const casing = require('../utils/casing')

/**
 * @typedef { VDirectiveKey & { name: VIdentifier & { name: 'bind' }, argument: VExpressionContainer | VIdentifier } } VBindDirectiveKey
 * @typedef { VDirective & { key: VBindDirectiveKey } } VBindDirective
 */

/**
 * @param {string} name
 * @returns {string}
 */
function kebabCaseToCamelCase(name) {
  return casing.isKebabCase(name) ? casing.camelCase(name) : name
}

/**
 * @param {VBindDirective} node
 * @returns {boolean}
 */
function isSameName(node) {
  const attrName =
    node.key.argument.type === 'VIdentifier' ? node.key.argument.rawName : null
  const valueName =
    node.value?.expression?.type === 'Identifier'
      ? node.value.expression.name
      : null

  if (!attrName || !valueName) return false

  return kebabCaseToCamelCase(attrName) === kebabCaseToCamelCase(valueName)
}

/**
 * @param {VBindDirectiveKey} key
 * @returns {number}
 */
function getCutStart(key) {
  const modifiers = key.modifiers
  return modifiers.length > 0
    ? modifiers[modifiers.length - 1].range[1]
    : key.argument.range[1]
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce `v-bind` directive style',
      categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
      url: 'https://eslint.vuejs.org/rules/v-bind-style.html'
    },
    fixable: 'code',
    schema: [
      { enum: ['shorthand', 'longform'] },
      {
        type: 'object',
        properties: {
          sameNameShorthand: { enum: ['always', 'never', 'ignore'] }
        },
        additionalProperties: false
      }
    ],
    messages: {
      expectedLonghand: "Expected 'v-bind' before ':'.",
      unexpectedLonghand: "Unexpected 'v-bind' before ':'.",
      expectedLonghandForProp: "Expected 'v-bind:' instead of '.'.",
      expectedShorthand: 'Expected same-name shorthand.',
      unexpectedShorthand: 'Unexpected same-name shorthand.'
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const preferShorthand = context.options[0] !== 'longform'
    /** @type {"always" | "never" | "ignore"} */
    const sameNameShorthand = context.options[1]?.sameNameShorthand || 'ignore'

    /** @param {VBindDirective} node */
    function checkAttributeStyle(node) {
      const shorthandProp = node.key.name.rawName === '.'
      const shorthand = node.key.name.rawName === ':' || shorthandProp
      if (shorthand === preferShorthand) {
        return
      }

      let messageId = 'expectedLonghand'
      if (preferShorthand) {
        messageId = 'unexpectedLonghand'
      } else if (shorthandProp) {
        messageId = 'expectedLonghandForProp'
      }

      context.report({
        node,
        loc: node.loc,
        messageId,
        *fix(fixer) {
          if (preferShorthand) {
            yield fixer.remove(node.key.name)
          } else {
            yield fixer.insertTextBefore(node, 'v-bind')

            if (shorthandProp) {
              // Replace `.` by `:`.
              yield fixer.replaceText(node.key.name, ':')

              // Insert `.prop` modifier if it doesn't exist.
              const modifier = node.key.modifiers[0]
              const isAutoGeneratedPropModifier =
                modifier.name === 'prop' && modifier.rawName === ''
              if (isAutoGeneratedPropModifier) {
                yield fixer.insertTextBefore(modifier, '.prop')
              }
            }
          }
        }
      })
    }

    /** @param {VBindDirective} node */
    function checkAttributeSameName(node) {
      if (sameNameShorthand === 'ignore' || !isSameName(node)) return

      const preferShorthand = sameNameShorthand === 'always'
      const isShorthand = utils.isVBindSameNameShorthand(node)
      if (isShorthand === preferShorthand) {
        return
      }

      const messageId = preferShorthand
        ? 'expectedShorthand'
        : 'unexpectedShorthand'

      context.report({
        node,
        loc: node.loc,
        messageId,
        *fix(fixer) {
          if (preferShorthand) {
            /** @type {Range} */
            const valueRange = [getCutStart(node.key), node.range[1]]

            yield fixer.removeRange(valueRange)
          } else if (node.key.argument.type === 'VIdentifier') {
            yield fixer.insertTextAfter(
              node,
              `="${kebabCaseToCamelCase(node.key.argument.rawName)}"`
            )
          }
        }
      })
    }

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VBindDirective} node */
      "VAttribute[directive=true][key.name.name='bind'][key.argument!=null]"(
        node
      ) {
        checkAttributeSameName(node)
        checkAttributeStyle(node)
      }
    })
  }
}


code:
/**
 * @fileoverview enforce `v-for` directive's delimiter style
 * @author Flo Edelmann
 * @copyright 2020 Flo Edelmann. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../utils')

module.exports = {
  meta: {
    type: 'layout',
    docs: {
      description: "enforce `v-for` directive's delimiter style",
      categories: undefined,
      recommended: false,
      url: 'https://eslint.vuejs.org/rules/v-for-delimiter-style.html'
    },
    fixable: 'code',
    schema: [{ enum: ['in', 'of'] }],
    messages: {
      expected:
        "Expected '{{preferredDelimiter}}' instead of '{{usedDelimiter}}' in 'v-for'."
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const preferredDelimiter =
      /** @type {string|undefined} */ (context.options[0]) || 'in'

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VForExpression} node */
      VForExpression(node) {
        const sourceCode = context.getSourceCode()
        const tokenStore =
          sourceCode.parserServices.getTemplateBodyTokenStore &&
          sourceCode.parserServices.getTemplateBodyTokenStore()

        const delimiterToken = /** @type {Token} */ (
          tokenStore.getTokenAfter(
            node.left.length > 0
              ? node.left[node.left.length - 1]
              : tokenStore.getFirstToken(node),
            (token) => token.type !== 'Punctuator'
          )
        )

        if (delimiterToken.value === preferredDelimiter) {
          return
        }

        context.report({
          node,
          loc: node.loc,
          messageId: 'expected',
          data: {
            preferredDelimiter,
            usedDelimiter: delimiterToken.value
          },
          *fix(fixer) {
            yield fixer.replaceText(delimiterToken, preferredDelimiter)
          }
        })
      }
    })
  }
}


code:
/**
 * @author Felipe Melendez
 * See LICENSE file in root directory for full license.
 */
'use strict'

// =============================================================================
// Requirements
// =============================================================================

const utils = require('../utils')
const casing = require('../utils/casing')

// =============================================================================
// Rule Helpers
// =============================================================================

/**
 * A conditional family is made up of a group of repeated components that are conditionally rendered
 * using v-if, v-else-if, and v-else.
 *
 * @typedef {Object} ConditionalFamily
 * @property {VElement} if - The node associated with the 'v-if' directive.
 * @property {VElement[]} elseIf - An array of nodes associated with 'v-else-if' directives.
 * @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one.
 */

/**
 * Checks if a given node has sibling nodes of the same type that are also conditionally rendered.
 * This is used to determine if multiple instances of the same component are being conditionally
 * rendered within the same parent scope.
 *
 * @param {VElement} node - The Vue component node to check for conditional rendering siblings.
 * @param {string} componentName - The name of the component to check for sibling instances.
 * @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise.
 */
const hasConditionalRenderedSiblings = (node, componentName) => {
  if (!node.parent || node.parent.type !== 'VElement') {
    return false
  }
  return node.parent.children.some(
    (sibling) =>
      sibling !== node &&
      sibling.type === 'VElement' &&
      sibling.rawName === componentName &&
      hasConditionalDirective(sibling)
  )
}

/**
 * Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing
 * and the node is part of a conditional family a report is generated.
 * The fix proposed adds a unique key based on the component's name and count,
 * following the format '${kebabCase(componentName)}-${componentCount}', e.g., 'some-component-2'.
 *
 * @param {VElement} node - The Vue component node to check for a 'key' attribute.
 * @param {RuleContext} context - The rule's context object, used for reporting.
 * @param {string} componentName - Name of the component.
 * @param {string} uniqueKey - A unique key for the repeated component, used for the fix.
 * @param {Map<VElement, ConditionalFamily>} conditionalFamilies - Map of conditionally rendered components and their respective conditional directives.
 */
const checkForKey = (
  node,
  context,
  componentName,
  uniqueKey,
  conditionalFamilies
) => {
  if (
    !node.parent ||
    node.parent.type !== 'VElement' ||
    !hasConditionalRenderedSiblings(node, componentName)
  ) {
    return
  }

  const conditionalFamily = conditionalFamilies.get(node.parent)

  if (!conditionalFamily || utils.hasAttribute(node, 'key')) {
    return
  }

  const needsKey =
    conditionalFamily.if === node ||
    conditionalFamily.else === node ||
    conditionalFamily.elseIf.includes(node)

  if (needsKey) {
    context.report({
      node: node.startTag,
      loc: node.startTag.loc,
      messageId: 'requireKey',
      data: { componentName },
      fix(fixer) {
        const afterComponentNamePosition =
          node.startTag.range[0] + componentName.length + 1
        return fixer.insertTextBeforeRange(
          [afterComponentNamePosition, afterComponentNamePosition],
          ` key="${uniqueKey}"`
        )
      }
    })
  }
}

/**
 * Checks for the presence of conditional directives in the given node.
 *
 * @param {VElement} node - The node to check for conditional directives.
 * @returns {boolean} Returns true if a conditional directive is found in the node or its parents,
 *   false otherwise.
 */
const hasConditionalDirective = (node) =>
  utils.hasDirective(node, 'if') ||
  utils.hasDirective(node, 'else-if') ||
  utils.hasDirective(node, 'else')

// =============================================================================
// Rule Definition
// =============================================================================

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: 'problem',
    docs: {
      description:
        'require key attribute for conditionally rendered repeated components',
      categories: null,
      recommended: false,
      url: 'https://eslint.vuejs.org/rules/v-if-else-key.html'
    },
    // eslint-disable-next-line eslint-plugin/require-meta-fixable -- fixer is not recognized
    fixable: 'code',
    schema: [],
    messages: {
      requireKey:
        "Conditionally rendered repeated component '{{componentName}}' expected to have a 'key' attribute."
    }
  },
  /**
   * Creates and returns a rule object which checks usage of repeated components. If a component
   * is used more than once, it checks for the presence of a key.
   *
   * @param {RuleContext} context - The context object.
   * @returns {Object} A dictionary of functions to be called on traversal of the template body by
   *   the eslint parser.
   */
  create(context) {
    /**
     * Map to store conditionally rendered components and their respective conditional directives.
     *
     * @type {Map<VElement, ConditionalFamily>}
     */
    const conditionalFamilies = new Map()

    /**
     * Array of Maps to keep track of components and their usage counts along with the first
     * node instance. Each Map represents a different scope level, and maps a component name to
     * an object containing the count and a reference to the first node.
     */
    /** @type {Map<string, { count: number; firstNode: any }>[]} */
    const componentUsageStack = [new Map()]

    /**
     * Checks if a given node represents a custom component without any conditional directives.
     *
     * @param {VElement} node - The AST node to check.
     * @returns {boolean} True if the node represents a custom component without any conditional directives, false otherwise.
     */
    const isCustomComponentWithoutCondition = (node) =>
      node.type === 'VElement' &&
      utils.isCustomComponent(node) &&
      !hasConditionalDirective(node)

    /** Set of built-in Vue components that are exempt from the rule. */
    /** @type {Set<string>} */
    const exemptTags = new Set(['component', 'slot', 'template'])

    /** Set to keep track of nodes we've pushed to the stack. */
    /** @type {Set<any>} */
    const pushedNodes = new Set()

    /**
     * Creates and returns an object representing a conditional family.
     *
     * @param {VElement} ifNode - The VElement associated with the 'v-if' directive.
     * @returns {ConditionalFamily}
     */
    const createConditionalFamily = (ifNode) => ({
      if: ifNode,
      elseIf: [],
      else: null
    })

    return utils.defineTemplateBodyVisitor(context, {
      /**
       * Callback to be executed when a Vue element is traversed. This function checks if the
       * element is a component, increments the usage count of the component in the
       * current scope, and checks for the key directive if the component is repeated.
       *
       * @param {VElement} node - The traversed Vue element.
       */
      VElement(node) {
        if (exemptTags.has(node.rawName)) {
          return
        }

        const condition =
          utils.getDirective(node, 'if') ||
          utils.getDirective(node, 'else-if') ||
          utils.getDirective(node, 'else')

        if (condition) {
          const conditionType = condition.key.name.name

          if (node.parent && node.parent.type === 'VElement') {
            let conditionalFamily = conditionalFamilies.get(node.parent)

            if (!conditionalFamily) {
              conditionalFamily = createConditionalFamily(node)
              conditionalFamilies.set(node.parent, conditionalFamily)
            }

            if (conditionalFamily) {
              switch (conditionType) {
                case 'if': {
                  conditionalFamily = createConditionalFamily(node)
                  conditionalFamilies.set(node.parent, conditionalFamily)
                  break
                }
                case 'else-if': {
                  conditionalFamily.elseIf.push(node)
                  break
                }
                case 'else': {
                  conditionalFamily.else = node
                  break
                }
              }
            }
          }
        }

        if (isCustomComponentWithoutCondition(node)) {
          componentUsageStack.push(new Map())
          return
        }

        if (!utils.isCustomComponent(node)) {
          return
        }

        const componentName = node.rawName
        const currentScope = componentUsageStack[componentUsageStack.length - 1]
        const usageInfo = currentScope.get(componentName) || {
          count: 0,
          firstNode: null
        }

        if (hasConditionalDirective(node)) {
          // Store the first node if this is the first occurrence
          if (usageInfo.count === 0) {
            usageInfo.firstNode = node
          }

          if (usageInfo.count > 0) {
            const uniqueKey = `${casing.kebabCase(componentName)}-${
              usageInfo.count + 1
            }`
            checkForKey(
              node,
              context,
              componentName,
              uniqueKey,
              conditionalFamilies
            )

            // If this is the second occurrence, also apply a fix to the first occurrence
            if (usageInfo.count === 1) {
              const uniqueKeyForFirstInstance = `${casing.kebabCase(
                componentName
              )}-1`
              checkForKey(
                usageInfo.firstNode,
                context,
                componentName,
                uniqueKeyForFirstInstance,
                conditionalFamilies
              )
            }
          }
          usageInfo.count += 1
          currentScope.set(componentName, usageInfo)
        }
        componentUsageStack.push(new Map())
        pushedNodes.add(node)
      },

      'VElement:exit'(node) {
        if (exemptTags.has(node.rawName)) {
          return
        }
        if (isCustomComponentWithoutCondition(node)) {
          componentUsageStack.pop()
          return
        }
        if (!utils.isCustomComponent(node)) {
          return
        }
        if (pushedNodes.has(node)) {
          componentUsageStack.pop()
          pushedNodes.delete(node)
        }
      }
    })
  }
}


code:
'use strict'

const utils = require('../utils')
const casing = require('../utils/casing')

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'enforce v-on event naming style on custom components in template',
      categories: ['vue3-strongly-recommended'],
      url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html',
      defaultOptions: {
        vue3: ['always', { autofix: true }]
      }
    },
    fixable: 'code',
    schema: [
      {
        enum: ['always', 'never']
      },
      {
        type: 'object',
        properties: {
          autofix: { type: 'boolean' },
          ignore: {
            type: 'array',
            items: {
              allOf: [
                { type: 'string' },
                { not: { type: 'string', pattern: ':exit$' } },
                { not: { type: 'string', pattern: String.raw`^\s*$` } }
              ]
            },
            uniqueItems: true,
            additionalItems: false
          }
        },
        additionalProperties: false
      }
    ],
    messages: {
      // eslint-disable-next-line eslint-plugin/report-message-format
      mustBeHyphenated: "v-on event '{{text}}' must be hyphenated.",
      // eslint-disable-next-line eslint-plugin/report-message-format
      cannotBeHyphenated: "v-on event '{{text}}' can't be hyphenated."
    }
  },

  /** @param {RuleContext} context */
  create(context) {
    const sourceCode = context.getSourceCode()
    const option = context.options[0]
    const optionsPayload = context.options[1]
    const useHyphenated = option !== 'never'
    /** @type {string[]} */
    const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
    const autofix = Boolean(optionsPayload && optionsPayload.autofix)

    const caseConverter = casing.getConverter(
      useHyphenated ? 'kebab-case' : 'camelCase'
    )

    /**
     * @param {VDirective} node
     * @param {VIdentifier} argument
     * @param {string} name
     */
    function reportIssue(node, argument, name) {
      const text = sourceCode.getText(node.key)

      context.report({
        node: node.key,
        loc: node.loc,
        messageId: useHyphenated ? 'mustBeHyphenated' : 'cannotBeHyphenated',
        data: {
          text
        },
        fix:
          autofix &&
          // It cannot be converted in snake_case.
          !name.includes('_')
            ? (fixer) => fixer.replaceText(argument, caseConverter(name))
            : null
      })
    }

    /**
     * @param {string} value
     */
    function isIgnoredAttribute(value) {
      const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))

      if (isIgnored) {
        return true
      }

      return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
    }

    return utils.defineTemplateBodyVisitor(context, {
      "VAttribute[directive=true][key.name.name='on']"(node) {
        if (!utils.isCustomComponent(node.parent.parent)) return
        if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
          return
        }
        const name = node.key.argument.rawName
        if (!name || isIgnoredAttribute(name)) return

        reportIssue(node, node.key.argument, name)
      }
    })
  }
}


code:
/**
 * @author Niklas Higi
 */
'use strict'

const utils = require('../utils')

/**
 * @typedef { import('../utils').ComponentPropertyData } ComponentPropertyData
 */

/**
 * Check whether the given token is a quote.
 * @param {Token} token The token to check.
 * @returns {boolean} `true` if the token is a quote.
 */
function isQuote(token) {
  return (
    token != null &&
    token.type === 'Punctuator' &&
    (token.value === '"' || token.value === "'")
  )
}

/**
 * @param {VOnExpression} node
 * @returns {CallExpression | null}
 */
function getInvalidNeverCallExpression(node) {
  /** @type {ExpressionStatement} */
  let exprStatement
  let body = node.body
  while (true) {
    const statements = body.filter((st) => st.type !== 'EmptyStatement')
    if (statements.length !== 1) {
      return null
    }
    const statement = statements[0]
    if (statement.type === 'ExpressionStatement') {
      exprStatement = statement
      break
    }
    if (statement.type === 'BlockStatement') {
      body = statement.body
      continue
    }
    return null
  }
  const expression = exprStatement.expression
  if (expression.type !== 'CallExpression' || expression.arguments.length > 0) {
    return null
  }
  if (expression.optional) {
    // Allow optional chaining
    return null
  }
  const callee = expression.callee
  if (callee.type !== 'Identifier') {
    return null
  }
  return expression
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description:
        'enforce or forbid parentheses after method calls without arguments in `v-on` directives',
      categories: undefined,
      url: 'https://eslint.vuejs.org/rules/v-on-function-call.html'
    },
    fixable: 'code',
    deprecated: true,
    replacedBy: ['v-on-handler-style'],
    schema: [
      { enum: ['always', 'never'] },
      {
        type: 'object',
        properties: {
          ignoreIncludesComment: {
            type: 'boolean'
          }
        },
        additionalProperties: false
      }
    ],
    messages: {
      always: "Method calls inside of 'v-on' directives must have parentheses.",
      never:
        "Method calls without arguments inside of 'v-on' directives must not have parentheses."
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const always = context.options[0] === 'always'

    if (always) {
      return utils.defineTemplateBodyVisitor(context, {
        /** @param {Identifier} node */
        "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer > Identifier"(
          node
        ) {
          context.report({
            node,
            messageId: 'always'
          })
        }
      })
    }

    const option = context.options[1] || {}
    const ignoreIncludesComment = !!option.ignoreIncludesComment
    /** @type {Set<string>} */
    const useArgsMethods = new Set()

    return utils.defineTemplateBodyVisitor(
      context,
      {
        /** @param {VOnExpression} node */
        "VAttribute[directive=true][key.name.name='on'][key.argument!=null] VOnExpression"(
          node
        ) {
          const expression = getInvalidNeverCallExpression(node)
          if (!expression) {
            return
          }

          const sourceCode = context.getSourceCode()
          const tokenStore =
            sourceCode.parserServices.getTemplateBodyTokenStore()
          const tokens = tokenStore.getTokens(node.parent, {
            includeComments: true
          })
          /** @type {Token | undefined} */
          let leftQuote
          /** @type {Token | undefined} */
          let rightQuote
          if (isQuote(tokens[0])) {
            leftQuote = tokens.shift()
            rightQuote = tokens.pop()
          }

          const hasComment = tokens.some(
            (token) => token.type === 'Block' || token.type === 'Line'
          )

          if (ignoreIncludesComment && hasComment) {
            return
          }

          if (
            expression.callee.type === 'Identifier' &&
            useArgsMethods.has(expression.callee.name)
          ) {
            // The behavior of target method can change given the arguments.
            return
          }

          context.report({
            node: expression,
            messageId: 'never',
            fix: hasComment
              ? null /* The comment is included and cannot be fixed. */
              : (fixer) => {
                  /** @type {Range} */
                  const range =
                    leftQuote && rightQuote
                      ? [leftQuote.range[1], rightQuote.range[0]]
                      : [tokens[0].range[0], tokens[tokens.length - 1].range[1]]

                  return fixer.replaceTextRange(
                    range,
                    context.getSourceCode().getText(expression.callee)
                  )
                }
          })
        }
      },
      utils.defineVueVisitor(context, {
        onVueObjectEnter(node) {
          for (const method of utils.iterateProperties(
            node,
            new Set(['methods'])
          )) {
            if (useArgsMethods.has(method.name)) {
              continue
            }
            if (method.type !== 'object') {
              continue
            }
            const value = method.property.value
            if (
              (value.type === 'FunctionExpression' ||
                value.type === 'ArrowFunctionExpression') &&
              value.params.length > 0
            ) {
              useArgsMethods.add(method.name)
            }
          }
        }
      })
    )
  }
}


code:
/**
 * @author Yosuke Ota <https://github.com/ota-meshi>
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../utils')

/**
 * @typedef {import('eslint').ReportDescriptorFix} ReportDescriptorFix
 * @typedef {'method' | 'inline' | 'inline-function'} HandlerKind
 * @typedef {object} ObjectOption
 * @property {boolean} [ignoreIncludesComment]
 */

/**
 * @param {RuleContext} context
 */
function parseOptions(context) {
  /** @type {[HandlerKind | HandlerKind[] | undefined, ObjectOption | undefined]} */
  const options = /** @type {any} */ (context.options)
  /** @type {HandlerKind[]} */
  const allows = []
  if (options[0]) {
    if (Array.isArray(options[0])) {
      allows.push(...options[0])
    } else {
      allows.push(options[0])
    }
  } else {
    allows.push('method', 'inline-function')
  }

  const option = options[1] || {}
  const ignoreIncludesComment = !!option.ignoreIncludesComment

  return { allows, ignoreIncludesComment }
}

/**
 * Check whether the given token is a quote.
 * @param {Token} token The token to check.
 * @returns {boolean} `true` if the token is a quote.
 */
function isQuote(token) {
  return (
    token != null &&
    token.type === 'Punctuator' &&
    (token.value === '"' || token.value === "'")
  )
}
/**
 * Check whether the given node is an identifier call expression. e.g. `foo()`
 * @param {Expression} node The node to check.
 * @returns {node is CallExpression & {callee: Identifier}}
 */
function isIdentifierCallExpression(node) {
  if (node.type !== 'CallExpression') {
    return false
  }
  if (node.optional) {
    // optional chaining
    return false
  }
  const callee = node.callee
  return callee.type === 'Identifier'
}

/**
 * Returns a call expression node if the given VOnExpression or BlockStatement consists
 * of only a single identifier call expression.
 * e.g.
 *   @click="foo()"
 *   @click="{ foo() }"
 *   @click="foo();;"
 * @param {VOnExpression | BlockStatement} node
 * @returns {CallExpression & {callee: Identifier} | null}
 */
function getIdentifierCallExpression(node) {
  /** @type {ExpressionStatement} */
  let exprStatement
  let body = node.body
  while (true) {
    const statements = body.filter((st) => st.type !== 'EmptyStatement')
    if (statements.length !== 1) {
      return null
    }
    const statement = statements[0]
    if (statement.type === 'ExpressionStatement') {
      exprStatement = statement
      break
    }
    if (statement.type === 'BlockStatement') {
      body = statement.body
      continue
    }
    return null
  }
  const expression = exprStatement.expression
  if (!isIdentifierCallExpression(expression)) {
    return null
  }
  return expression
}

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce writing style for handlers in `v-on` directives',
      categories: undefined,
      url: 'https://eslint.vuejs.org/rules/v-on-handler-style.html'
    },
    fixable: 'code',
    schema: [
      {
        oneOf: [
          { enum: ['inline', 'inline-function'] },
          {
            type: 'array',
            items: [
              { const: 'method' },
              { enum: ['inline', 'inline-function'] }
            ],
            uniqueItems: true,
            additionalItems: false,
            minItems: 2,
            maxItems: 2
          }
        ]
      },
      {
        type: 'object',
        properties: {
          ignoreIncludesComment: {
            type: 'boolean'
          }
        },
        additionalProperties: false
      }
    ],
    messages: {
      preferMethodOverInline:
        'Prefer method handler over inline handler in v-on.',
      preferMethodOverInlineWithoutIdCall:
        'Prefer method handler over inline handler in v-on. Note that you may need to create a new method.',
      preferMethodOverInlineFunction:
        'Prefer method handler over inline function in v-on.',
      preferMethodOverInlineFunctionWithoutIdCall:
        'Prefer method handler over inline function in v-on. Note that you may need to create a new method.',
      preferInlineOverMethod:
        'Prefer inline handler over method handler in v-on.',
      preferInlineOverInlineFunction:
        'Prefer inline handler over inline function in v-on.',
      preferInlineOverInlineFunctionWithMultipleParams:
        'Prefer inline handler over inline function in v-on. Note that the custom event must be changed to a single payload.',
      preferInlineFunctionOverMethod:
        'Prefer inline function over method handler in v-on.',
      preferInlineFunctionOverInline:
        'Prefer inline function over inline handler in v-on.'
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const { allows, ignoreIncludesComment } = parseOptions(context)

    /** @type {Set<VElement>} */
    const upperElements = new Set()
    /** @type {Map<string, number>} */
    const methodParamCountMap = new Map()
    /** @type {Identifier[]} */
    const $eventIdentifiers = []

    /**
     * Verify for inline handler.
     * @param {VOnExpression} node
     * @param {HandlerKind} kind
     * @returns {boolean} Returns `true` if reported.
     */
    function verifyForInlineHandler(node, kind) {
      switch (kind) {
        case 'method': {
          return verifyCanUseMethodHandlerForInlineHandler(node)
        }
        case 'inline-function': {
          reportCanUseInlineFunctionForInlineHandler(node)
          return true
        }
      }
      return false
    }
    /**
     * Report for method handler.
     * @param {Identifier} node
     * @param {HandlerKind} kind
     * @returns {boolean} Returns `true` if reported.
     */
    function reportForMethodHandler(node, kind) {
      switch (kind) {
        case 'inline':
        case 'inline-function': {
          context.report({
            node,
            messageId:
              kind === 'inline'
                ? 'preferInlineOverMethod'
                : 'preferInlineFunctionOverMethod'
          })
          return true
        }
      }
      // This path is currently not taken.
      return false
    }
    /**
     * Verify for inline function handler.
     * @param {ArrowFunctionExpression | FunctionExpression} node
     * @param {HandlerKind} kind
     * @returns {boolean} Returns `true` if reported.
     */
    function verifyForInlineFunction(node, kind) {
      switch (kind) {
        case 'method': {
          return verifyCanUseMethodHandlerForInlineFunction(node)
        }
        case 'inline': {
          reportCanUseInlineHandlerForInlineFunction(node)
          return true
        }
      }
      return false
    }

    /**
     * Get token information for the given VExpressionContainer node.
     * @param {VExpressionContainer} node
     */
    function getVExpressionContainerTokenInfo(node) {
      const sourceCode = context.getSourceCode()
      const tokenStore = sourceCode.parserServices.getTemplateBodyTokenStore()
      const tokens = tokenStore.getTokens(node, {
        includeComments: true
      })
      const firstToken = tokens[0]
      const lastToken = tokens[tokens.length - 1]

      const hasQuote = isQuote(firstToken)
      /** @type {Range} */
      const rangeWithoutQuotes = hasQuote
        ? [firstToken.range[1], lastToken.range[0]]
        : [firstToken.range[0], lastToken.range[1]]

      return {
        rangeWithoutQuotes,
        get hasComment() {
          return tokens.some(
            (token) => token.type === 'Block' || token.type === 'Line'
          )
        },
        hasQuote
      }
    }

    /**
     * Checks whether the given node refers to a variable of the element.
     * @param {Expression | VOnExpression} node
     */
    function hasReferenceUpperElementVariable(node) {
      for (const element of upperElements) {
        for (const vv of element.variables) {
          for (const reference of vv.references) {
            const { range } = reference.id
            if (node.range[0] <= range[0] && range[1] <= node.range[1]) {
              return true
            }
          }
        }
      }
      return false
    }
    /**
     * Check if `v-on:click="foo()"` can be converted to `v-on:click="foo"` and report if it can.
     * @param {VOnExpression} node
     * @returns {boolean} Returns `true` if reported.
     */
    function verifyCanUseMethodHandlerForInlineHandler(node) {
      const { rangeWithoutQuotes, hasComment } =
        getVExpressionContainerTokenInfo(node.parent)
      if (ignoreIncludesComment && hasComment) {
        return false
      }

      const idCallExpr = getIdentifierCallExpression(node)
      if (
        (!idCallExpr || idCallExpr.arguments.length > 0) &&
        hasReferenceUpperElementVariable(node)
      ) {
        // It cannot be converted to method because it refers to the variable of the element.
        // e.g. <template v-for="e in list"><button @click="foo(e)" /></template>
        return false
      }

      context.report({
        node,
        messageId: idCallExpr
          ? 'preferMethodOverInline'
          : 'preferMethodOverInlineWithoutIdCall',
        fix: (fixer) => {
          if (
            hasComment /* The statement contains comment and cannot be fixed. */ ||
            !idCallExpr /* The statement is not a simple identifier call and cannot be fixed. */ ||
            idCallExpr.arguments.length > 0
          ) {
            return null
          }
          const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
          if (paramCount != null && paramCount > 0) {
            // The behavior of target method can change given the arguments.
            return null
          }
          return fixer.replaceTextRange(
            rangeWithoutQuotes,
            context.getSourceCode().getText(idCallExpr.callee)
          )
        }
      })
      return true
    }
    /**
     * Check if `v-on:click="() => foo()"` can be converted to `v-on:click="foo"` and report if it can.
     * @param {ArrowFunctionExpression | FunctionExpression} node
     * @returns {boolean} Returns `true` if reported.
     */
    function verifyCanUseMethodHandlerForInlineFunction(node) {
      const { rangeWithoutQuotes, hasComment } =
        getVExpressionContainerTokenInfo(
          /** @type {VExpressionContainer} */ (node.parent)
        )
      if (ignoreIncludesComment && hasComment) {
        return false
      }

      /** @type {CallExpression & {callee: Identifier} | null} */
      let idCallExpr = null
      if (node.body.type === 'BlockStatement') {
        idCallExpr = getIdentifierCallExpression(node.body)
      } else if (isIdentifierCallExpression(node.body)) {
        idCallExpr = node.body
      }
      if (
        (!idCallExpr || !isSameParamsAndArgs(idCallExpr)) &&
        hasReferenceUpperElementVariable(node)
      ) {
        // It cannot be converted to method because it refers to the variable of the element.
        // e.g. <template v-for="e in list"><button @click="() => foo(e)" /></template>
        return false
      }

      context.report({
        node,
        messageId: idCallExpr
          ? 'preferMethodOverInlineFunction'
          : 'preferMethodOverInlineFunctionWithoutIdCall',
        fix: (fixer) => {
          if (
            hasComment /* The function contains comment and cannot be fixed. */ ||
            !idCallExpr /* The function is not a simple identifier call and cannot be fixed. */
          ) {
            return null
          }
          if (!isSameParamsAndArgs(idCallExpr)) {
            // It is not a call with the arguments given as is.
            return null
          }
          const paramCount = methodParamCountMap.get(idCallExpr.callee.name)
          if (
            paramCount != null &&
            paramCount !== idCallExpr.arguments.length
          ) {
            // The behavior of target method can change given the arguments.
            return null
          }
          return fixer.replaceTextRange(
            rangeWithoutQuotes,
            context.getSourceCode().getText(idCallExpr.callee)
          )
        }
      })
      return true

      /**
       * Checks whether parameters are passed as arguments as-is.
       * @param {CallExpression} expression
       */
      function isSameParamsAndArgs(expression) {
        return (
          node.params.length === expression.arguments.length &&
          node.params.every((param, index) => {
            if (param.type !== 'Identifier') {
              return false
            }
            const arg = expression.arguments[index]
            if (!arg || arg.type !== 'Identifier') {
              return false
            }
            return param.name === arg.name
          })
        )
      }
    }
    /**
     * Report `v-on:click="foo()"` can be converted to `v-on:click="()=>foo()"`.
     * @param {VOnExpression} node
     * @returns {void}
     */
    function reportCanUseInlineFunctionForInlineHandler(node) {
      context.report({
        node,
        messageId: 'preferInlineFunctionOverInline',
        *fix(fixer) {
          const has$Event = $eventIdentifiers.some(
            ({ range }) =>
              node.range[0] <= range[0] && range[1] <= node.range[1]
          )
          if (has$Event) {
            /* The statements contains $event and cannot be fixed. */
            return
          }
          const { rangeWithoutQuotes, hasQuote } =
            getVExpressionContainerTokenInfo(node.parent)
          if (!hasQuote) {
            /* The statements is not enclosed in quotes and cannot be fixed. */
            return
          }
          yield fixer.insertTextBeforeRange(rangeWithoutQuotes, '() => ')
          const sourceCode = context.getSourceCode()
          const tokenStore =
            sourceCode.parserServices.getTemplateBodyTokenStore()
          const firstToken = tokenStore.getFirstToken(node)
          const lastToken = tokenStore.getLastToken(node)
          if (firstToken.value === '{' && lastToken.value === '}') return
          if (
            lastToken.value !== ';' &&
            node.body.length === 1 &&
            node.body[0].type === 'ExpressionStatement'
          ) {
            // it is a single expression
            return
          }
          yield fixer.insertTextBefore(firstToken, '{')
          yield fixer.insertTextAfter(lastToken, '}')
        }
      })
    }
    /**
     * Report `v-on:click="() => foo()"` can be converted to `v-on:click="foo()"`.
     * @param {ArrowFunctionExpression | FunctionExpression} node
     * @returns {void}
     */
    function reportCanUseInlineHandlerForInlineFunction(node) {
      // If a function has one parameter, you can turn it into an inline handler using $event.
      // If a function has two or more parameters, it cannot be easily converted to an inline handler.
      // However, users can use inline handlers by changing the payload of the component's custom event.
      // So we report it regardless of the number of parameters.

      context.report({
        node,
        messageId:
          node.params.length > 1
            ? 'preferInlineOverInlineFunctionWithMultipleParams'
            : 'preferInlineOverInlineFunction',
        fix:
          node.params.length > 0
            ? null /* The function has parameters and cannot be fixed. */
            : (fixer) => {
                let text = context.getSourceCode().getText(node.body)
                if (node.body.type === 'BlockStatement') {
                  text = text.slice(1, -1) // strip braces
                }
                return fixer.replaceText(node, text)
              }
      })
    }

    return utils.defineTemplateBodyVisitor(
      context,
      {
        VElement(node) {
          upperElements.add(node)
        },
        'VElement:exit'(node) {
          upperElements.delete(node)
        },
        /** @param {VExpressionContainer} node */
        "VAttribute[directive=true][key.name.name='on'][key.argument!=null] > VExpressionContainer.value:exit"(
          node
        ) {
          const expression = node.expression
          if (!expression) {
            return
          }
          switch (expression.type) {
            case 'VOnExpression': {
              // e.g. v-on:click="foo()"
              if (allows[0] === 'inline') {
                return
              }
              for (const allow of allows) {
                if (verifyForInlineHandler(expression, allow)) {
                  return
                }
              }
              break
            }
            case 'Identifier': {
              // e.g. v-on:click="foo"
              if (allows[0] === 'method') {
                return
              }
              for (const allow of allows) {
                if (reportForMethodHandler(expression, allow)) {
                  return
                }
              }
              break
            }
            case 'ArrowFunctionExpression':
            case 'FunctionExpression': {
              // e.g. v-on:click="()=>foo()"
              if (allows[0] === 'inline-function') {
                return
              }
              for (const allow of allows) {
                if (verifyForInlineFunction(expression, allow)) {
                  return
                }
              }
              break
            }
            default: {
              return
            }
          }
        },
        ...(allows.includes('inline-function')
          ? // Collect $event identifiers to check for side effects
            // when converting from `v-on:click="foo($event)"` to `v-on:click="()=>foo($event)"` .
            {
              'Identifier[name="$event"]'(node) {
                $eventIdentifiers.push(node)
              }
            }
          : {})
      },
      allows.includes('method')
        ? // Collect method definition with params information to check for side effects.
          // when converting from `v-on:click="foo()"` to `v-on:click="foo"`, or
          // converting from `v-on:click="() => foo()"` to `v-on:click="foo"`.
          utils.defineVueVisitor(context, {
            onVueObjectEnter(node) {
              for (const method of utils.iterateProperties(
                node,
                new Set(['methods'])
              )) {
                if (method.type !== 'object') {
                  // This branch is usually not passed.
                  continue
                }
                const value = method.property.value
                if (
                  value.type === 'FunctionExpression' ||
                  value.type === 'ArrowFunctionExpression'
                ) {
                  methodParamCountMap.set(
                    method.name,
                    value.params.some((p) => p.type === 'RestElement')
                      ? Number.POSITIVE_INFINITY
                      : value.params.length
                  )
                }
              }
            }
          })
        : {}
    )
  }
}


code:
/**
 * @author Toru Nagashima
 * @copyright 2017 Toru Nagashima. All rights reserved.
 * See LICENSE file in root directory for full license.
 */
'use strict'

const utils = require('../utils')

module.exports = {
  meta: {
    type: 'suggestion',
    docs: {
      description: 'enforce `v-on` directive style',
      categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
      url: 'https://eslint.vuejs.org/rules/v-on-style.html'
    },
    fixable: 'code',
    schema: [{ enum: ['shorthand', 'longform'] }],
    messages: {
      expectedShorthand: "Expected '@' instead of 'v-on:'.",
      expectedLonghand: "Expected 'v-on:' instead of '@'."
    }
  },
  /** @param {RuleContext} context */
  create(context) {
    const preferShorthand = context.options[0] !== 'longform'

    return utils.defineTemplateBodyVisitor(context, {
      /** @param {VDirective} node */
      "VAttribute[directive=true][key.name.name='on'][key.argument!=null]"(
        node
      ) {
        const shorthand = node.key.name.rawName === '@'
        if (shorthand === preferShorthand) {
          return
        }

        const pos = node.range[0]
        context.report({
          node,
          loc: node.loc,
          messageId: preferShorthand ? 'expectedShorthand' : 'expectedLonghand',
          fix: (fixer) =>
            preferShorthand
              ? fixer.replaceTextRange([pos, pos + 5], '@')
              : fixer.replaceTextRange([pos, pos + 1], 'v-on:')
        })
      }
    })
  }
}