Repository: vuejs/eslint-plugin-vue
Pull request #1429 (2021-02-02 02:28:09)

Add "SLOT" option to vue/attributes-order rule to specify v-slot order.
This PR adds the "SLOT" option to `vue/attributes-order` rule.

This PR has taken over and changed #1311.
The difference from #1311 is that if the order setting is omitted, the order of the omitted attributes will be excluded from the check.

close #1311


Patch:
diff --git a/docs/rules/attributes-order.md b/docs/rules/attributes-order.md
index 8bcb8b8e..9caad0b6 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 96fb1ac1..7c985e5e 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 = {


Test patch:
diff --git a/tests/lib/rules/attributes-order.js b/tests/lib/rules/attributes-order.js
index 97c3b6e7..6933a01d 100644
--- a/tests/lib/rules/attributes-order.js
+++ b/tests/lib/rules/attributes-order.js
@@ -421,6 +421,20 @@ tester.run('attributes-order', rule, {
         </div>
       </template>`,
       options: [{ alphabetical: true }]
+    },
+
+    // omit order
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          v-for="a in items"
+          v-if="a"
+          attr="a">
+        </div>
+      </template>`,
+      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: `
+      <template>
+        <div
+          v-if="a"
+          attr="a"
+          v-for="a in items">
+        </div>
+      </template>`,
+      options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
+      output: `
+      <template>
+        <div
+          v-for="a in items"
+          v-if="a"
+          attr="a">
+        </div>
+      </template>`,
+      errors: ['Attribute "v-for" should go before "v-if".']
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div
+          attr="a"
+          v-if="a"
+          v-for="a in items">
+        </div>
+      </template>`,
+      options: [{ order: ['LIST_RENDERING', 'CONDITIONALS'] }],
+      output: `
+      <template>
+        <div
+          attr="a"
+          v-for="a in items"
+          v-if="a">
+        </div>
+      </template>`,
+      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:
+        '<template><div ref="foo" v-slot="{ qux }" bar="baz"></div></template>',
+      output:
+        '<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
+      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:
+        '<template><div bar="baz" ref="foo" v-slot="{ qux }"></div></template>',
+      output:
+        '<template><div ref="foo" bar="baz" v-slot="{ qux }"></div></template>',
+      errors: [
+        {
+          message: 'Attribute "ref" should go before "bar".'
+        }
+      ]
     }
   ]
 })