Repository: vuejs/eslint-plugin-vue
Pull request #1430 (2021-02-02 02:55:10)

Change options schema to strictly.
This PR modifies the option schema of some rules to be strict.
This change causes an error to be thrown if the user specifies an incorrect option.

Targets:

- vue/attributes-order
- vue/component-tags-order
- vue/max-attributes-per-line
- vue/new-line-between-multi-line-property
- vue/no-bare-strings-in-template
- vue/no-duplicate-attributes
- vue/no-potential-component-option-typo
- vue/no-reserved-component-names
- vue/no-use-v-if-with-v-for
- vue/no-useless-mustaches
- vue/no-useless-v-bind
- vue/valid-v-slot




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..8e618eba 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 = {
@@ -346,18 +367,33 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/attributes-order.html'
     },
     fixable: 'code',
-    schema: {
-      type: 'array',
-      properties: {
-        order: {
-          items: {
-            type: 'string'
+    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
           },
-          maxItems: 10,
-          minItems: 10
-        }
+          alphabetical: { type: 'boolean' }
+        },
+        additionalProperties: false
       }
-    }
+    ]
   },
   create
 }
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index b2a2692a..dc7da972 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -40,7 +40,8 @@ module.exports = {
             uniqueItems: true,
             additionalItems: false
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     messages: {
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index 8cec6361..e379df47 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -61,7 +61,8 @@ module.exports = {
               }
             ]
           }
-        }
+        },
+        additionalProperties: false
       }
     ]
   },
diff --git a/lib/rules/new-line-between-multi-line-property.js b/lib/rules/new-line-between-multi-line-property.js
index b48d7837..c1be3618 100644
--- a/lib/rules/new-line-between-multi-line-property.js
+++ b/lib/rules/new-line-between-multi-line-property.js
@@ -68,7 +68,8 @@ module.exports = {
             type: 'number',
             minimum: 2
           }
-        }
+        },
+        additionalProperties: false
       }
     ]
   },
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
index e8dc5bfc..89970003 100644
--- a/lib/rules/no-bare-strings-in-template.js
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -149,7 +149,8 @@ module.exports = {
             items: { type: 'string', pattern: '^v-' },
             uniqueItems: true
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     messages: {
diff --git a/lib/rules/no-duplicate-attributes.js b/lib/rules/no-duplicate-attributes.js
index 83cc52d8..4a3309b0 100644
--- a/lib/rules/no-duplicate-attributes.js
+++ b/lib/rules/no-duplicate-attributes.js
@@ -59,7 +59,8 @@ module.exports = {
           allowCoexistStyle: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ]
   },
diff --git a/lib/rules/no-potential-component-option-typo.js b/lib/rules/no-potential-component-option-typo.js
index f3ac4736..cf3b33cd 100644
--- a/lib/rules/no-potential-component-option-typo.js
+++ b/lib/rules/no-potential-component-option-typo.js
@@ -44,7 +44,8 @@ module.exports = {
             type: 'number',
             minimum: 1
           }
-        }
+        },
+        additionalProperties: false
       }
     ]
   },
diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js
index aecbe602..890f1a30 100644
--- a/lib/rules/no-reserved-component-names.js
+++ b/lib/rules/no-reserved-component-names.js
@@ -84,7 +84,8 @@ module.exports = {
           disallowVue3BuiltInComponents: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     messages: {
diff --git a/lib/rules/no-use-v-if-with-v-for.js b/lib/rules/no-use-v-if-with-v-for.js
index 18733926..47904629 100644
--- a/lib/rules/no-use-v-if-with-v-for.js
+++ b/lib/rules/no-use-v-if-with-v-for.js
@@ -62,7 +62,8 @@ module.exports = {
           allowUsingIterationVar: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ]
   },
diff --git a/lib/rules/no-useless-mustaches.js b/lib/rules/no-useless-mustaches.js
index b8a2057d..845c42f9 100644
--- a/lib/rules/no-useless-mustaches.js
+++ b/lib/rules/no-useless-mustaches.js
@@ -50,7 +50,8 @@ module.exports = {
           ignoreStringEscape: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     type: 'suggestion'
diff --git a/lib/rules/no-useless-v-bind.js b/lib/rules/no-useless-v-bind.js
index 01549ca6..5d4dc006 100644
--- a/lib/rules/no-useless-v-bind.js
+++ b/lib/rules/no-useless-v-bind.js
@@ -30,7 +30,8 @@ module.exports = {
           ignoreStringEscape: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     type: 'suggestion'
diff --git a/lib/rules/valid-v-slot.js b/lib/rules/valid-v-slot.js
index f5ff3a0e..4914edef 100644
--- a/lib/rules/valid-v-slot.js
+++ b/lib/rules/valid-v-slot.js
@@ -287,7 +287,8 @@ module.exports = {
           allowModifiers: {
             type: 'boolean'
           }
-        }
+        },
+        additionalProperties: false
       }
     ],
     messages: {


Test patch:
diff --git a/tests/lib/rules/attributes-order.js b/tests/lib/rules/attributes-order.js
index 97c3b6e7..15ccccae 100644
--- a/tests/lib/rules/attributes-order.js
+++ b/tests/lib/rules/attributes-order.js
@@ -259,7 +259,7 @@ tester.run('attributes-order', rule, {
           <div
             v-if="!visible"
             class="content"
-            :class="className"
+            v-model="foo"
             v-text="textContent"
             >
           </div>
@@ -273,7 +273,7 @@ tester.run('attributes-order', rule, {
             'DEFINITION',
             'EVENTS',
             'UNIQUE',
-            ['BINDING', 'OTHER_ATTR'],
+            ['TWO_WAY_BINDING', 'OTHER_ATTR'],
             'CONTENT',
             'GLOBAL'
           ]
@@ -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'] }]
     }
   ],
 
@@ -705,7 +719,7 @@ tester.run('attributes-order', rule, {
             <div
               class="content"
               v-if="!visible"
-              :class="className"
+              v-model="foo"
               v-text="textContent"
               >
             </div>
@@ -719,7 +733,7 @@ tester.run('attributes-order', rule, {
             'DEFINITION',
             'EVENTS',
             'UNIQUE',
-            ['BINDING', 'OTHER_ATTR'],
+            ['TWO_WAY_BINDING', 'OTHER_ATTR'],
             'CONTENT',
             'GLOBAL'
           ]
@@ -729,7 +743,7 @@ tester.run('attributes-order', rule, {
             <div
               v-if="!visible"
               class="content"
-              :class="className"
+              v-model="foo"
               v-text="textContent"
               >
             </div>
@@ -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".'
+        }
+      ]
     }
   ]
 })