123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- /**
- * @fileoverview enforce ordering of attributes
- * @author Erin Depew
- */
- 'use strict'
- const utils = require('../utils')
- /**
- * @typedef { VDirective & { key: VDirectiveKey & { name: VIdentifier & { name: 'bind' } } } } VBindDirective
- */
- const ATTRS = {
- DEFINITION: 'DEFINITION',
- LIST_RENDERING: 'LIST_RENDERING',
- CONDITIONALS: 'CONDITIONALS',
- RENDER_MODIFIERS: 'RENDER_MODIFIERS',
- GLOBAL: 'GLOBAL',
- UNIQUE: 'UNIQUE',
- SLOT: 'SLOT',
- TWO_WAY_BINDING: 'TWO_WAY_BINDING',
- OTHER_DIRECTIVES: 'OTHER_DIRECTIVES',
- OTHER_ATTR: 'OTHER_ATTR',
- ATTR_STATIC: 'ATTR_STATIC',
- ATTR_DYNAMIC: 'ATTR_DYNAMIC',
- ATTR_SHORTHAND_BOOL: 'ATTR_SHORTHAND_BOOL',
- EVENTS: 'EVENTS',
- CONTENT: 'CONTENT'
- }
- /**
- * Check whether the given attribute is `v-bind` directive.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VBindDirective }
- */
- function isVBind(node) {
- return Boolean(node && node.directive && node.key.name.name === 'bind')
- }
- /**
- * Check whether the given attribute is `v-model` directive.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VDirective }
- */
- function isVModel(node) {
- return Boolean(node && node.directive && node.key.name.name === 'model')
- }
- /**
- * Check whether the given attribute is plain attribute.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VAttribute }
- */
- function isVAttribute(node) {
- return Boolean(node && !node.directive)
- }
- /**
- * Check whether the given attribute is plain attribute, `v-bind` directive or `v-model` directive.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VAttribute }
- */
- function isVAttributeOrVBindOrVModel(node) {
- return isVAttribute(node) || isVBind(node) || isVModel(node)
- }
- /**
- * Check whether the given attribute is `v-bind="..."` directive.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VBindDirective }
- */
- function isVBindObject(node) {
- return isVBind(node) && node.key.argument == null
- }
- /**
- * Check whether the given attribute is a shorthand boolean like `selected`.
- * @param {VAttribute | VDirective | undefined | null} node
- * @returns { node is VAttribute }
- */
- function isVShorthandBoolean(node) {
- return isVAttribute(node) && !node.value
- }
- /**
- * @param {VAttribute | VDirective} attribute
- * @param {SourceCode} sourceCode
- */
- function getAttributeName(attribute, sourceCode) {
- if (attribute.directive) {
- if (isVBind(attribute)) {
- return attribute.key.argument
- ? sourceCode.getText(attribute.key.argument)
- : ''
- } else {
- return getDirectiveKeyName(attribute.key, sourceCode)
- }
- } else {
- return attribute.key.name
- }
- }
- /**
- * @param {VDirectiveKey} directiveKey
- * @param {SourceCode} sourceCode
- */
- function getDirectiveKeyName(directiveKey, sourceCode) {
- let text = `v-${directiveKey.name.name}`
- if (directiveKey.argument) {
- text += `:${sourceCode.getText(directiveKey.argument)}`
- }
- for (const modifier of directiveKey.modifiers) {
- text += `.${modifier.name}`
- }
- return text
- }
- /**
- * @param {VAttribute | VDirective} attribute
- */
- function getAttributeType(attribute) {
- let propName
- if (attribute.directive) {
- if (!isVBind(attribute)) {
- const name = attribute.key.name.name
- switch (name) {
- case 'for': {
- return ATTRS.LIST_RENDERING
- }
- case 'if':
- case 'else-if':
- case 'else':
- case 'show':
- case 'cloak': {
- return ATTRS.CONDITIONALS
- }
- case 'pre':
- case 'once': {
- return ATTRS.RENDER_MODIFIERS
- }
- case 'model': {
- return ATTRS.TWO_WAY_BINDING
- }
- case 'on': {
- return ATTRS.EVENTS
- }
- case 'html':
- case 'text': {
- return ATTRS.CONTENT
- }
- case 'slot': {
- return ATTRS.SLOT
- }
- case 'is': {
- return ATTRS.DEFINITION
- }
- default: {
- return ATTRS.OTHER_DIRECTIVES
- }
- }
- }
- propName =
- attribute.key.argument && attribute.key.argument.type === 'VIdentifier'
- ? attribute.key.argument.rawName
- : ''
- } else {
- propName = attribute.key.name
- }
- switch (propName) {
- case 'is': {
- return ATTRS.DEFINITION
- }
- case 'id': {
- return ATTRS.GLOBAL
- }
- case 'ref':
- case 'key': {
- return ATTRS.UNIQUE
- }
- case 'slot':
- case 'slot-scope': {
- return ATTRS.SLOT
- }
- default: {
- if (isVBind(attribute)) {
- return ATTRS.ATTR_DYNAMIC
- }
- if (isVShorthandBoolean(attribute)) {
- return ATTRS.ATTR_SHORTHAND_BOOL
- }
- return ATTRS.ATTR_STATIC
- }
- }
- }
- /**
- * @param {VAttribute | VDirective} attribute
- * @param { { [key: string]: number } } attributePosition
- * @returns {number | null} If the value is null, the order is omitted. Do not force the order.
- */
- function getPosition(attribute, attributePosition) {
- const attributeType = getAttributeType(attribute)
- return attributePosition[attributeType] == null
- ? null
- : attributePosition[attributeType]
- }
- /**
- * @param {VAttribute | VDirective} prevNode
- * @param {VAttribute | VDirective} currNode
- * @param {SourceCode} sourceCode
- */
- function isAlphabetical(prevNode, currNode, sourceCode) {
- const prevName = getAttributeName(prevNode, sourceCode)
- const currName = getAttributeName(currNode, sourceCode)
- if (prevName === currName) {
- const prevIsBind = isVBind(prevNode)
- const currIsBind = isVBind(currNode)
- return prevIsBind <= currIsBind
- }
- return prevName < currName
- }
- /**
- * @param {RuleContext} context - The rule context.
- * @returns {RuleListener} AST event handlers.
- */
- function create(context) {
- const sourceCode = context.getSourceCode()
- const otherAttrs = [
- ATTRS.ATTR_DYNAMIC,
- ATTRS.ATTR_STATIC,
- ATTRS.ATTR_SHORTHAND_BOOL
- ]
- let attributeOrder = [
- ATTRS.DEFINITION,
- ATTRS.LIST_RENDERING,
- ATTRS.CONDITIONALS,
- ATTRS.RENDER_MODIFIERS,
- ATTRS.GLOBAL,
- [ATTRS.UNIQUE, ATTRS.SLOT],
- ATTRS.TWO_WAY_BINDING,
- ATTRS.OTHER_DIRECTIVES,
- otherAttrs,
- ATTRS.EVENTS,
- ATTRS.CONTENT
- ]
- if (context.options[0] && context.options[0].order) {
- attributeOrder = [...context.options[0].order]
- // check if `OTHER_ATTR` is valid
- for (const item of attributeOrder.flat()) {
- if (item === ATTRS.OTHER_ATTR) {
- for (const attribute of attributeOrder.flat()) {
- if (otherAttrs.includes(attribute)) {
- throw new Error(
- `Value "${ATTRS.OTHER_ATTR}" is not allowed with "${attribute}".`
- )
- }
- }
- }
- }
- // expand `OTHER_ATTR` alias
- for (const [index, item] of attributeOrder.entries()) {
- if (item === ATTRS.OTHER_ATTR) {
- attributeOrder[index] = otherAttrs
- } else if (Array.isArray(item) && item.includes(ATTRS.OTHER_ATTR)) {
- const attributes = item.filter((i) => i !== ATTRS.OTHER_ATTR)
- attributes.push(...otherAttrs)
- attributeOrder[index] = attributes
- }
- }
- }
- const alphabetical = Boolean(
- context.options[0] && context.options[0].alphabetical
- )
- /** @type { { [key: string]: number } } */
- const attributePosition = {}
- for (const [i, item] of attributeOrder.entries()) {
- if (Array.isArray(item)) {
- for (const attr of item) {
- attributePosition[attr] = i
- }
- } else attributePosition[item] = i
- }
- /**
- * @param {VAttribute | VDirective} node
- * @param {VAttribute | VDirective} previousNode
- */
- function reportIssue(node, previousNode) {
- const currentNode = sourceCode.getText(node.key)
- const prevNode = sourceCode.getText(previousNode.key)
- context.report({
- node,
- messageId: 'expectedOrder',
- data: {
- currentNode,
- prevNode
- },
- fix(fixer) {
- const attributes = node.parent.attributes
- /** @type { (node: VAttribute | VDirective | undefined) => boolean } */
- let isMoveUp
- if (isVBindObject(node)) {
- // prev, v-bind:foo, v-bind -> v-bind:foo, v-bind, prev
- isMoveUp = isVAttributeOrVBindOrVModel
- } else if (isVAttributeOrVBindOrVModel(node)) {
- // prev, v-bind, v-bind:foo -> v-bind, v-bind:foo, prev
- isMoveUp = isVBindObject
- } else {
- isMoveUp = () => false
- }
- const previousNodes = attributes.slice(
- attributes.indexOf(previousNode),
- attributes.indexOf(node)
- )
- const moveNodes = [node]
- for (const node of previousNodes) {
- if (isMoveUp(node)) {
- moveNodes.unshift(node)
- } else {
- moveNodes.push(node)
- }
- }
- return moveNodes.map((moveNode, index) => {
- const text = sourceCode.getText(moveNode)
- return fixer.replaceText(previousNodes[index] || node, text)
- })
- }
- })
- }
- return utils.defineTemplateBodyVisitor(context, {
- VStartTag(node) {
- const attributeAndPositions = getAttributeAndPositionList(node)
- if (attributeAndPositions.length <= 1) {
- return
- }
- let { attr: previousNode, position: previousPosition } =
- attributeAndPositions[0]
- for (let index = 1; index < attributeAndPositions.length; index++) {
- const { attr, position } = attributeAndPositions[index]
- let valid = previousPosition <= position
- if (valid && alphabetical && previousPosition === position) {
- valid = isAlphabetical(previousNode, attr, sourceCode)
- }
- if (valid) {
- previousNode = attr
- previousPosition = position
- } else {
- reportIssue(attr, previousNode)
- }
- }
- }
- })
- /**
- * @param {VStartTag} node
- * @returns { { attr: ( VAttribute | VDirective ), position: number }[] }
- */
- function getAttributeAndPositionList(node) {
- const attributes = node.attributes.filter((node, index, attributes) => {
- if (
- isVBindObject(node) &&
- (isVAttributeOrVBindOrVModel(attributes[index - 1]) ||
- isVAttributeOrVBindOrVModel(attributes[index + 1]))
- ) {
- // In Vue 3, ignore `v-bind="object"`, which is
- // a pair of `v-bind:foo="..."` and `v-bind="object"` and
- // a pair of `v-model="..."` and `v-bind="object"`,
- // because changing the order behaves differently.
- return false
- }
- return true
- })
- const results = []
- for (const [index, attr] of attributes.entries()) {
- const position = getPositionFromAttrIndex(index)
- if (position == null) {
- // The omitted order is skipped.
- continue
- }
- results.push({ attr, position })
- }
- return results
- /**
- * @param {number} index
- * @returns {number | null}
- */
- function getPositionFromAttrIndex(index) {
- const node = attributes[index]
- if (isVBindObject(node)) {
- // node is `v-bind ="object"` syntax
- // In Vue 3, if change the order of `v-bind:foo="..."`, `v-model="..."` and `v-bind="object"`,
- // the behavior will be different, so adjust so that there is no change in behavior.
- const len = attributes.length
- for (let nextIndex = index + 1; nextIndex < len; nextIndex++) {
- const next = attributes[nextIndex]
- if (isVAttributeOrVBindOrVModel(next) && !isVBindObject(next)) {
- // It is considered to be in the same order as the next bind prop node.
- return getPositionFromAttrIndex(nextIndex)
- }
- }
- }
- return getPosition(node, attributePosition)
- }
- }
- }
- module.exports = {
- meta: {
- type: 'suggestion',
- docs: {
- description: 'enforce order of attributes',
- categories: ['vue3-recommended', 'vue2-recommended'],
- url: 'https://eslint.vuejs.org/rules/attributes-order.html'
- },
- fixable: 'code',
- schema: [
- {
- type: 'object',
- properties: {
- order: {
- type: 'array',
- items: {
- anyOf: [
- { enum: Object.values(ATTRS) },
- {
- type: 'array',
- items: {
- enum: Object.values(ATTRS),
- uniqueItems: true,
- additionalItems: false
- }
- }
- ]
- },
- uniqueItems: true,
- additionalItems: false
- },
- alphabetical: { type: 'boolean' }
- },
- additionalProperties: false
- }
- ],
- messages: {
- expectedOrder: `Attribute "{{currentNode}}" should go before "{{prevNode}}".`
- }
- },
- create
- }
|