Repository: vuejs/eslint-plugin-vue
Pull request #1242 (2020-07-15 02:17:14)

Fixed `vue/no-unused-properties`, `vue/require-valid-default-prop`, `vue/require-default-prop` and `vue/no-multiple-objects-in-class` rules crash on sparse arrays.


Patch:
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/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-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
     }
   ],