Repository: vuejs/eslint-plugin-vue
Pull request #1243 (2020-07-15 02:30:26)

Add `vue/no-sparse-arrays` rule
This PR adds the wrapper rule of the [no-sparse-arrays](https://eslint.org/docs/rules/no-sparse-arrays) core rule to apply to the expressions in `<template>`.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 3dc1c255..33285ae2 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -338,6 +338,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/no-extra-parens](./no-extra-parens.md) | disallow unnecessary parentheses | :wrench: |
 | [vue/no-irregular-whitespace](./no-irregular-whitespace.md) | disallow irregular whitespace |  |
 | [vue/no-restricted-syntax](./no-restricted-syntax.md) | disallow specified syntax |  |
+| [vue/no-sparse-arrays](./no-sparse-arrays.md) | disallow sparse arrays |  |
 | [vue/no-useless-concat](./no-useless-concat.md) | disallow unnecessary concatenation of literals or template literals |  |
 | [vue/object-curly-newline](./object-curly-newline.md) | enforce consistent line breaks inside braces | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
diff --git a/docs/rules/no-sparse-arrays.md b/docs/rules/no-sparse-arrays.md
new file mode 100644
index 00000000..ea0678c1
--- /dev/null
+++ b/docs/rules/no-sparse-arrays.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-sparse-arrays
+description: disallow sparse arrays
+---
+# vue/no-sparse-arrays
+> disallow sparse arrays
+
+This rule is the same rule as core [no-sparse-arrays] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [no-sparse-arrays]
+
+[no-sparse-arrays]: https://eslint.org/docs/rules/no-sparse-arrays
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-sparse-arrays.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-sparse-arrays.js)
+
+<sup>Taken with ❤️ [from ESLint core](https://eslint.org/docs/rules/no-sparse-arrays)</sup>
diff --git a/lib/index.js b/lib/index.js
index 282ddd39..530c05c5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -94,6 +94,7 @@ module.exports = {
     'no-shared-component-data': require('./rules/no-shared-component-data'),
     'no-side-effects-in-computed-properties': require('./rules/no-side-effects-in-computed-properties'),
     'no-spaces-around-equal-signs-in-attribute': require('./rules/no-spaces-around-equal-signs-in-attribute'),
+    'no-sparse-arrays': require('./rules/no-sparse-arrays'),
     'no-static-inline-styles': require('./rules/no-static-inline-styles'),
     'no-template-key': require('./rules/no-template-key'),
     'no-template-shadow': require('./rules/no-template-shadow'),
diff --git a/lib/rules/no-multiple-objects-in-class.js b/lib/rules/no-multiple-objects-in-class.js
index f2a17064..cf930ebe 100644
--- a/lib/rules/no-multiple-objects-in-class.js
+++ b/lib/rules/no-multiple-objects-in-class.js
@@ -21,7 +21,7 @@ const { defineTemplateBodyVisitor } = require('../utils')
  */
 function countObjectExpression(node) {
   return node.value.expression.elements.filter(
-    (element) => element.type === 'ObjectExpression'
+    (element) => element && element.type === 'ObjectExpression'
   ).length
 }
 
diff --git a/lib/rules/no-sparse-arrays.js b/lib/rules/no-sparse-arrays.js
new file mode 100644
index 00000000..89e30b36
--- /dev/null
+++ b/lib/rules/no-sparse-arrays.js
@@ -0,0 +1,9 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line no-invalid-meta, no-invalid-meta-docs-categories
+module.exports = wrapCoreRule(require('eslint/lib/rules/no-sparse-arrays'))
diff --git a/lib/rules/require-default-prop.js b/lib/rules/require-default-prop.js
index ff014ca8..ec73e369 100644
--- a/lib/rules/require-default-prop.js
+++ b/lib/rules/require-default-prop.js
@@ -10,6 +10,7 @@
  */
 
 const utils = require('../utils')
+const { isDef } = require('../utils')
 
 const NATIVE_TYPES = new Set([
   'String',
@@ -99,16 +100,21 @@ module.exports = {
     /**
      * Detects whether given value node is a Boolean type
      * @param {Expression | Pattern} value
-     * @return {Boolean}
+     * @return {boolean}
      */
     function isValueNodeOfBooleanType(value) {
-      return (
-        (value.type === 'Identifier' && value.name === 'Boolean') ||
-        (value.type === 'ArrayExpression' &&
-          value.elements.length === 1 &&
-          value.elements[0].type === 'Identifier' &&
-          value.elements[0].name === 'Boolean')
-      )
+      if (value.type === 'Identifier' && value.name === 'Boolean') {
+        return true
+      }
+      if (value.type === 'ArrayExpression') {
+        const elements = value.elements.filter(isDef)
+        return (
+          elements.length === 1 &&
+          elements[0].type === 'Identifier' &&
+          elements[0].name === 'Boolean'
+        )
+      }
+      return false
     }
 
     /**
diff --git a/lib/rules/require-prop-type-constructor.js b/lib/rules/require-prop-type-constructor.js
index 1104c7a3..0f94f0f2 100644
--- a/lib/rules/require-prop-type-constructor.js
+++ b/lib/rules/require-prop-type-constructor.js
@@ -5,6 +5,7 @@
 'use strict'
 
 const utils = require('../utils')
+const { isDef } = require('../utils')
 
 // ------------------------------------------------------------------------------
 // Rule Definition
@@ -49,10 +50,11 @@ module.exports = {
      */
     function checkPropertyNode(propName, node) {
       /** @type {ESNode[]} */
-      const nodes = node.type === 'ArrayExpression' ? node.elements : [node]
+      const nodes =
+        node.type === 'ArrayExpression' ? node.elements.filter(isDef) : [node]
 
       nodes
-        .filter((prop) => prop && isForbiddenType(prop))
+        .filter((prop) => isForbiddenType(prop))
         .forEach((prop) =>
           context.report({
             node: prop,
diff --git a/lib/rules/require-valid-default-prop.js b/lib/rules/require-valid-default-prop.js
index a6b2ca95..a0444db0 100644
--- a/lib/rules/require-valid-default-prop.js
+++ b/lib/rules/require-valid-default-prop.js
@@ -58,10 +58,10 @@ function getTypes(node) {
     return node.elements
       .filter(
         /**
-         * @param {Expression | SpreadElement} item
+         * @param {Expression | SpreadElement | null} item
          * @returns {item is Identifier}
          */
-        (item) => item.type === 'Identifier'
+        (item) => item != null && item.type === 'Identifier'
       )
       .map((item) => item.name)
   }
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 085c596a..0a6f7bf3 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -737,29 +737,27 @@ module.exports = {
         }
       })
     } else {
-      return propsNode.value.elements
-        .filter((prop) => prop)
-        .map((prop) => {
-          if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') {
-            const propName = getStringLiteralValue(prop)
-            if (propName != null) {
-              return {
-                type: 'array',
-                key: prop,
-                propName,
-                value: null,
-                node: prop
-              }
+      return propsNode.value.elements.filter(isDef).map((prop) => {
+        if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') {
+          const propName = getStringLiteralValue(prop)
+          if (propName != null) {
+            return {
+              type: 'array',
+              key: prop,
+              propName,
+              value: null,
+              node: prop
             }
           }
-          return {
-            type: 'array',
-            key: null,
-            propName: null,
-            value: null,
-            node: prop
-          }
-        })
+        }
+        return {
+          type: 'array',
+          key: null,
+          propName: null,
+          value: null,
+          node: prop
+        }
+      })
     }
   },
 
@@ -810,29 +808,27 @@ module.exports = {
         }
       })
     } else {
-      return emitsNode.value.elements
-        .filter((prop) => prop)
-        .map((prop) => {
-          if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') {
-            const emitName = getStringLiteralValue(prop)
-            if (emitName != null) {
-              return {
-                type: 'array',
-                key: prop,
-                emitName,
-                value: null,
-                node: prop
-              }
+      return emitsNode.value.elements.filter(isDef).map((prop) => {
+        if (prop.type === 'Literal' || prop.type === 'TemplateLiteral') {
+          const emitName = getStringLiteralValue(prop)
+          if (emitName != null) {
+            return {
+              type: 'array',
+              key: prop,
+              emitName,
+              value: null,
+              node: prop
             }
           }
-          return {
-            type: 'array',
-            key: null,
-            emitName: null,
-            value: null,
-            node: prop
-          }
-        })
+        }
+        return {
+          type: 'array',
+          key: null,
+          emitName: null,
+          value: null,
+          node: prop
+        }
+      })
     }
   },
 
@@ -1109,7 +1105,10 @@ module.exports = {
    */
   *iterateArrayExpression(node, groupName) {
     for (const item of node.elements) {
-      if (item.type === 'Literal' || item.type === 'TemplateLiteral') {
+      if (
+        item &&
+        (item.type === 'Literal' || item.type === 'TemplateLiteral')
+      ) {
         const name = getStringLiteralValue(item)
         if (name) {
           yield { type: 'array', name, groupName, node: item }
diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
index 701de116..035c113f 100644
--- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
@@ -298,7 +298,7 @@ export interface ThisExpression extends HasParentNode {
 }
 export interface ArrayExpression extends HasParentNode {
   type: 'ArrayExpression'
-  elements: (Expression | SpreadElement)[]
+  elements: (Expression | SpreadElement | null)[]
 }
 export interface ObjectExpression extends HasParentNode {
   type: 'ObjectExpression'


Test patch:
diff --git a/tests/lib/rules/no-multiple-objects-in-class.js b/tests/lib/rules/no-multiple-objects-in-class.js
index 45f1b721..64ce82b0 100644
--- a/tests/lib/rules/no-multiple-objects-in-class.js
+++ b/tests/lib/rules/no-multiple-objects-in-class.js
@@ -43,6 +43,17 @@ ruleTester.run('no-multiple-objects-in-class', rule, {
           type: 'VAttribute'
         }
       ]
+    },
+
+    // sparse array
+    {
+      code: `<template><div v-bind:class="[,{'foo': isFoo}, {'bar': isBar}]" /></template>`,
+      errors: [
+        {
+          message: 'Unexpected multiple objects. Merge objects.',
+          type: 'VAttribute'
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-sparse-arrays.js b/tests/lib/rules/no-sparse-arrays.js
new file mode 100644
index 00000000..7c0020fd
--- /dev/null
+++ b/tests/lib/rules/no-sparse-arrays.js
@@ -0,0 +1,55 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-sparse-arrays')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('no-sparse-arrays', rule, {
+  valid: [
+    `<template>
+      <div :class="['foo', 'bar']" />
+    </template>`,
+    `<template>
+      <div v-bind:[['foo'][0]]="bar" />
+    </template>`
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div :class="[, 'foo', 'bar']" />
+      </template>`,
+      errors: [
+        {
+          message: 'Unexpected comma in middle of array.',
+          line: 3,
+          column: 22,
+          endLine: 3,
+          endColumn: 38
+        }
+      ]
+    },
+    {
+      code: `
+      <template>
+        <div v-bind:[[,'foo'][1]]="bar" />
+      </template>`,
+      errors: [
+        {
+          message: 'Unexpected comma in middle of array.',
+          line: 3,
+          column: 22,
+          endLine: 3,
+          endColumn: 30
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unused-properties.js b/tests/lib/rules/no-unused-properties.js
index c5796e75..355fb8df 100644
--- a/tests/lib/rules/no-unused-properties.js
+++ b/tests/lib/rules/no-unused-properties.js
@@ -981,6 +981,21 @@ tester.run('no-unused-properties', rule, {
         }
       </script>`,
       options: [{ groups: ['props', 'setup'] }]
+    },
+
+    // sparse array
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>{{ count }}</div>
+        </template>
+        <script>
+          export default {
+            props: [, 'count']
+          }
+        </script>
+      `
     }
   ],
 
diff --git a/tests/lib/rules/require-default-prop.js b/tests/lib/rules/require-default-prop.js
index ce292774..cdc0f417 100644
--- a/tests/lib/rules/require-default-prop.js
+++ b/tests/lib/rules/require-default-prop.js
@@ -179,6 +179,21 @@ ruleTester.run('require-default-prop', rule, {
           }
         }
       `
+    },
+
+    // sparse array
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            a: {
+              type: [,Boolean]
+            },
+            b: [,Boolean],
+          }
+        }
+      `
     }
   ],
 
diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js
index 9f8ff727..2617e146 100644
--- a/tests/lib/rules/require-valid-default-prop.js
+++ b/tests/lib/rules/require-valid-default-prop.js
@@ -181,6 +181,20 @@ ruleTester.run('require-valid-default-prop', rule, {
         }
       }`,
       parserOptions
+    },
+
+    // sparse array
+    {
+      filename: 'test.vue',
+      code: `export default {
+        props: {
+          foo: {
+            type: [,Object, Number],
+            default: 10
+          }
+        }
+      }`,
+      parserOptions
     }
   ],