diff --git a/docs/rules/attributes-order.md b/docs/rules/attributes-order.md index 8bcb8b8ea..9caad0b67 100644 --- a/docs/rules/attributes-order.md +++ b/docs/rules/attributes-order.md @@ -27,7 +27,9 @@ This rule aims to enforce ordering of component attributes. The default order is - `GLOBAL` e.g. 'id' - `UNIQUE` - e.g. 'ref', 'key', 'v-slot', 'slot' + e.g. 'ref', 'key' +- `SLOT` + e.g. 'v-slot', 'slot'. - `TWO_WAY_BINDING` e.g. 'v-model' - `OTHER_DIRECTIVES` @@ -127,7 +129,7 @@ Note that `v-bind="object"` syntax is considered to be the same as the next or p "CONDITIONALS", "RENDER_MODIFIERS", "GLOBAL", - "UNIQUE", + ["UNIQUE", "SLOT"], "TWO_WAY_BINDING", "OTHER_DIRECTIVES", "OTHER_ATTR", diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js index 96fb1ac1d..7c985e5e4 100644 --- a/lib/rules/attributes-order.js +++ b/lib/rules/attributes-order.js @@ -20,6 +20,7 @@ const ATTRS = { RENDER_MODIFIERS: 'RENDER_MODIFIERS', GLOBAL: 'GLOBAL', UNIQUE: 'UNIQUE', + SLOT: 'SLOT', TWO_WAY_BINDING: 'TWO_WAY_BINDING', OTHER_DIRECTIVES: 'OTHER_DIRECTIVES', OTHER_ATTR: 'OTHER_ATTR', @@ -121,7 +122,7 @@ function getAttributeType(attribute) { } else if (name === 'html' || name === 'text') { return ATTRS.CONTENT } else if (name === 'slot') { - return ATTRS.UNIQUE + return ATTRS.SLOT } else if (name === 'is') { return ATTRS.DEFINITION } else { @@ -139,13 +140,10 @@ function getAttributeType(attribute) { return ATTRS.DEFINITION } else if (propName === 'id') { return ATTRS.GLOBAL - } else if ( - propName === 'ref' || - propName === 'key' || - propName === 'slot' || - propName === 'slot-scope' - ) { + } else if (propName === 'ref' || propName === 'key') { return ATTRS.UNIQUE + } else if (propName === 'slot' || propName === 'slot-scope') { + return ATTRS.SLOT } else { return ATTRS.OTHER_ATTR } @@ -154,12 +152,13 @@ function getAttributeType(attribute) { /** * @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 ? attributePosition[attributeType] - : -1 + : null } /** @@ -190,7 +189,7 @@ function create(context) { ATTRS.CONDITIONALS, ATTRS.RENDER_MODIFIERS, ATTRS.GLOBAL, - ATTRS.UNIQUE, + [ATTRS.UNIQUE, ATTRS.SLOT], ATTRS.TWO_WAY_BINDING, ATTRS.OTHER_DIRECTIVES, ATTRS.OTHER_ATTR, @@ -267,74 +266,96 @@ function create(context) { return utils.defineTemplateBodyVisitor(context, { VStartTag(node) { - const attributes = node.attributes.filter((node, index, attributes) => { - if ( - isVBindObject(node) && - (isVAttributeOrVBind(attributes[index - 1]) || - isVAttributeOrVBind(attributes[index + 1])) - ) { - // In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax - // as they behave differently if you change the order. - return false - } - return true - }) - if (attributes.length <= 1) { + const attributeAndPositions = getAttributeAndPositionList(node) + if (attributeAndPositions.length <= 1) { return } - let previousNode = attributes[0] - let previousPosition = getPositionFromAttrIndex(0) - for (let index = 1; index < attributes.length; index++) { - const node = attributes[index] - const position = getPositionFromAttrIndex(index) + 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, node, sourceCode) + valid = isAlphabetical(previousNode, attr, sourceCode) } if (valid) { - previousNode = node + previousNode = attr previousPosition = position } else { - reportIssue(node, previousNode) + reportIssue(attr, previousNode) } } + } + }) - /** - * @param {number} index - * @returns {number} - */ - function getPositionFromAttrIndex(index) { - const node = attributes[index] - if (isVBindObject(node)) { - // node is `v-bind ="object"` syntax + /** + * @param {VStartTag} node + * @returns { { attr: ( VAttribute | VDirective ), position: number }[] } + */ + function getAttributeAndPositionList(node) { + const attributes = node.attributes.filter((node, index, attributes) => { + if ( + isVBindObject(node) && + (isVAttributeOrVBind(attributes[index - 1]) || + isVAttributeOrVBind(attributes[index + 1])) + ) { + // In Vue 3, ignore the `v-bind:foo=" ... "` and `v-bind ="object"` syntax + // as they behave differently if you change the order. + return false + } + return true + }) - // In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`, - // the behavior will be different, so adjust so that there is no change in behavior. + const results = [] + for (let index = 0; index < attributes.length; index++) { + const attr = attributes[index] + 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 - const len = attributes.length - for (let nextIndex = index + 1; nextIndex < len; nextIndex++) { - const next = attributes[nextIndex] + // In Vue 3, if change the order of `v-bind:foo=" ... "` and `v-bind ="object"`, + // the behavior will be different, so adjust so that there is no change in behavior. - if (isVAttributeOrVBind(next) && !isVBindObject(next)) { - // It is considered to be in the same order as the next bind prop node. - return getPositionFromAttrIndex(nextIndex) - } + const len = attributes.length + for (let nextIndex = index + 1; nextIndex < len; nextIndex++) { + const next = attributes[nextIndex] + + if (isVAttributeOrVBind(next) && !isVBindObject(next)) { + // It is considered to be in the same order as the next bind prop node. + return getPositionFromAttrIndex(nextIndex) } - for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) { - const prev = attributes[prevIndex] + } + for (let prevIndex = index - 1; prevIndex >= 0; prevIndex--) { + const prev = attributes[prevIndex] - if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) { - // It is considered to be in the same order as the prev bind prop node. - return getPositionFromAttrIndex(prevIndex) - } + if (isVAttributeOrVBind(prev) && !isVBindObject(prev)) { + // It is considered to be in the same order as the prev bind prop node. + return getPositionFromAttrIndex(prevIndex) } } - return getPosition(node, attributePosition) } + return getPosition(node, attributePosition) } - }) + } } module.exports = { diff --git a/tests/lib/rules/attributes-order.js b/tests/lib/rules/attributes-order.js index 97c3b6e73..6933a01d1 100644 --- a/tests/lib/rules/attributes-order.js +++ b/tests/lib/rules/attributes-order.js @@ -421,6 +421,20 @@ tester.run('attributes-order', rule, { `, options: [{ alphabetical: true }] + }, + + // omit order + { + filename: 'test.vue', + code: ` + `, + options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }] } ], @@ -1213,6 +1227,113 @@ tester.run('attributes-order', rule, { 'Attribute "v-bind" should go before "v-on:click".', 'Attribute "v-if" should go before "v-on:click".' ] + }, + + // omit order + { + filename: 'test.vue', + code: ` + `, + options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }], + output: ` + `, + errors: ['Attribute "v-for" should go before "v-if".'] + }, + { + filename: 'test.vue', + code: ` + `, + options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }], + output: ` + `, + errors: ['Attribute "v-for" should go before "v-if".'] + }, + + // slot + { + filename: 'test.vue', + options: [ + { + order: [ + 'UNIQUE', + 'LIST_RENDERING', + 'CONDITIONALS', + 'RENDER_MODIFIERS', + 'GLOBAL', + 'TWO_WAY_BINDING', + 'OTHER_DIRECTIVES', + 'OTHER_ATTR', + 'EVENTS', + 'CONTENT', + 'DEFINITION', + 'SLOT' + ] + } + ], + code: + '', + output: + '', + errors: [ + { + message: 'Attribute "bar" should go before "v-slot".' + } + ] + }, + + { + filename: 'test.vue', + options: [ + { + order: [ + 'UNIQUE', + 'LIST_RENDERING', + 'CONDITIONALS', + 'RENDER_MODIFIERS', + 'GLOBAL', + 'TWO_WAY_BINDING', + 'OTHER_DIRECTIVES', + 'OTHER_ATTR', + 'EVENTS', + 'CONTENT', + 'DEFINITION', + 'SLOT' + ] + } + ], + code: + '', + output: + '', + errors: [ + { + message: 'Attribute "ref" should go before "bar".' + } + ] } ] })