Repository: vuejs/eslint-plugin-vue
Pull request #146 (2017-08-10 14:31:26)

Improve support for spread operator
Fix issues with trying to access undefined properties
- `Cannot read property 'type' of null` when `return` has no argument

Improvements:
- `iterateProperties` - utils
- `checkOrder` - order-in-components
- `getComputedProperties` - utils
- `iterateFunctionExpression` - utils
- add more tests

This changes provide spread operator support support for all rules.


Patch:
diff --git a/docs/rules/no-multi-spaces.md b/docs/rules/no-multi-spaces.md
new file mode 100644
index 00000000..b93254da
--- /dev/null
+++ b/docs/rules/no-multi-spaces.md
@@ -0,0 +1,31 @@
+# This rule warns about the usage of extra whitespaces between attributes (no-multi-spaces)
+
+The `--fix` option on the command line can automatically fix some of the problems reported by this rule.
+
+This rule aims to remove multiple spaces in a row between attributes witch are not used for indentation.
+
+## Rule Details
+
+Examples of **incorrect** code for this rule:
+
+```html
+<template>
+  <div class="foo"      :style="foo"
+    :foo="bar"         >
+  </div>
+</template>
+```
+
+Examples of **correct** code for this rule:
+
+```html
+<template>
+  <div class="foo"
+    :style="foo">
+  </div>
+</template>
+```
+
+### Options
+
+Nothing
diff --git a/lib/rules/no-multi-spaces.js b/lib/rules/no-multi-spaces.js
new file mode 100644
index 00000000..c9d6369f
--- /dev/null
+++ b/lib/rules/no-multi-spaces.js
@@ -0,0 +1,66 @@
+/**
+ * @fileoverview This rule warns about the usage of extra whitespaces between attributes
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'This rule warns about the usage of extra whitespaces between attributes',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'whitespace',  // or "code" or "whitespace"
+    schema: []
+  },
+
+  /**
+   * @param {RuleContext} context - The rule context.
+   * @returns {Object} AST event handlers.
+   */
+  create (context) {
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    return {
+      Program (node) {
+        if (context.parserServices.getTemplateBodyTokenStore == null) {
+          context.report({
+            loc: { line: 1, column: 0 },
+            message: 'Use the latest vue-eslint-parser. See also https://github.com/vuejs/eslint-plugin-vue#what-is-the-use-the-latest-vue-eslint-parser-error.'
+          })
+          return
+        }
+        const sourceCode = context.getSourceCode()
+        const tokenStore = context.parserServices.getTemplateBodyTokenStore()
+        const tokens = tokenStore.getTokens(node.templateBody, { includeComments: true })
+
+        let prevToken = tokens.shift()
+        for (const token of tokens) {
+          const spaces = token.range[0] - prevToken.range[1]
+          if (spaces > 1 && token.loc.start.line === prevToken.loc.start.line) {
+            context.report({
+              node: token,
+              loc: {
+                start: prevToken.loc.end,
+                end: token.loc.start
+              },
+              message: "Multiple spaces found before '{{displayValue}}'.",
+              fix: (fixer) => fixer.replaceTextRange([prevToken.range[1], token.range[0]], ' '),
+              data: {
+                displayValue: sourceCode.getText(token)
+              }
+            })
+          }
+          prevToken = token
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index d35d6458..e6a131f7 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -53,7 +53,9 @@ function getOrderMap (order) {
 }
 
 function checkOrder (propertiesNodes, orderMap, context) {
-  const properties = propertiesNodes.map(property => property.key)
+  const properties = propertiesNodes
+    .filter(property => property.type === 'Property')
+    .map(property => property.key)
 
   properties.forEach((property, i) => {
     const propertiesAbove = properties.slice(0, i)
diff --git a/lib/utils/index.js b/lib/utils/index.js
index b2a5bacb..e40c0935 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -323,6 +323,7 @@ module.exports = {
   getComputedProperties (componentObject) {
     const computedPropertiesNode = componentObject.properties
       .find(p =>
+        p.type === 'Property' &&
         p.key.type === 'Identifier' &&
         p.key.name === 'computed' &&
         p.value.type === 'ObjectExpression'
@@ -480,6 +481,8 @@ module.exports = {
     const nodes = node.properties.filter(p => p.type === 'Property' && groups.has(this.getStaticPropertyName(p.key)))
     for (const item of nodes) {
       const name = this.getStaticPropertyName(item.key)
+      if (!name) continue
+
       if (item.value.type === 'ArrayExpression') {
         yield * this.iterateArrayExpression(item.value, name)
       } else if (item.value.type === 'ObjectExpression') {
@@ -531,7 +534,7 @@ module.exports = {
     assert(node.type === 'FunctionExpression')
     if (node.body.type === 'BlockStatement') {
       for (const item of node.body.body) {
-        if (item.type === 'ReturnStatement' && item.argument.type === 'ObjectExpression') {
+        if (item.type === 'ReturnStatement' && item.argument && item.argument.type === 'ObjectExpression') {
           yield * this.iterateObjectExpression(item.argument, groupName)
         }
       }
diff --git a/package.json b/package.json
index 643b95ff..12917f31 100644
--- a/package.json
+++ b/package.json
@@ -45,7 +45,7 @@
   },
   "dependencies": {
     "requireindex": "^1.1.0",
-    "vue-eslint-parser": "2.0.0-beta.6"
+    "vue-eslint-parser": "2.0.0-beta.7"
   },
   "devDependencies": {
     "@types/node": "^4.2.16",


Test patch:
diff --git a/tests/lib/rules/html-end-tags.js b/tests/lib/rules/html-end-tags.js
index e470e62b..b4d648d5 100644
--- a/tests/lib/rules/html-end-tags.js
+++ b/tests/lib/rules/html-end-tags.js
@@ -49,21 +49,24 @@ tester.run('html-end-tags', rule, {
     }
   ],
   invalid: [
-        // {
-        //     filename: "test.vue",
-        //     code: "<template><div><hr></hr></div></template>",
-        //     errors: ["'<hr>' should not have end tag."],
-        // },
-        // {
-        //     filename: "test.vue",
-        //     code: "<template><div><img></img></div></template>",
-        //     errors: ["'<img>' should not have end tag."],
-        // },
-        // {
-        //     filename: "test.vue",
-        //     code: "<template><div><input></input></div></template>",
-        //     errors: ["'<input>' should not have end tag."],
-        // },
+    // {
+    //   filename: 'test.vue',
+    //   code: '<template><div><hr></hr></div></template>',
+    //   output: '<template><div><hr></template>',
+    //   errors: ["'<hr>' should not have end tag."]
+    // },
+    // {
+    //   filename: 'test.vue',
+    //   code: '<template><div><img></img></div></template>',
+    //   output: '<template><div><img></template>',
+    //   errors: ["'<img>' should not have end tag."]
+    // },
+    // {
+    //   filename: 'test.vue',
+    //   code: '<template><div><input></input></div></template>',
+    //   output: '<template><div><input></template>',
+    //   errors: ["'<input>' should not have end tag."]
+    // },
     {
       filename: 'test.vue',
       code: '<template><div><div></div></template>',
diff --git a/tests/lib/rules/name-property-casing.js b/tests/lib/rules/name-property-casing.js
index 88ede6d6..3c09f23f 100644
--- a/tests/lib/rules/name-property-casing.js
+++ b/tests/lib/rules/name-property-casing.js
@@ -17,7 +17,8 @@ const RuleTester = require('eslint').RuleTester
 
 const parserOptions = {
   ecmaVersion: 6,
-  sourceType: 'module'
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
 }
 
 const ruleTester = new RuleTester()
@@ -33,6 +34,16 @@ ruleTester.run('name-property-casing', rule, {
       options: ['camelCase'],
       parserOptions
     },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          ...name
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
index 34c57e75..e7cd0269 100644
--- a/tests/lib/rules/no-async-in-computed-properties.js
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -36,6 +36,7 @@ ruleTester.run('no-async-in-computed-properties', rule, {
       filename: 'test.vue',
       code: `
         export default {
+          ...foo,
           computed: {
             ...mapGetters({
               test: 'getTest'
diff --git a/tests/lib/rules/no-dupe-keys.js b/tests/lib/rules/no-dupe-keys.js
index fbb314c9..fb23ff8a 100644
--- a/tests/lib/rules/no-dupe-keys.js
+++ b/tests/lib/rules/no-dupe-keys.js
@@ -32,6 +32,9 @@ ruleTester.run('no-dupe-keys', rule, {
               dat: null
             }
           },
+          data () {
+            return
+          },
           methods: {
             _foo () {},
             test () {
@@ -68,7 +71,12 @@ ruleTester.run('no-dupe-keys', rule, {
             ...foo(),
             test () {
             }
-          }
+          },
+          data () {
+            return {
+              ...dat
+            }
+          },
         }
       `,
       parserOptions: { ecmaVersion: 8, sourceType: 'module', ecmaFeatures: { experimentalObjectRestSpread: true }}
diff --git a/tests/lib/rules/no-multi-spaces.js b/tests/lib/rules/no-multi-spaces.js
new file mode 100644
index 00000000..afb93cf0
--- /dev/null
+++ b/tests/lib/rules/no-multi-spaces.js
@@ -0,0 +1,177 @@
+/**
+ * @fileoverview This rule warns about the usage of extra whitespaces between attributes
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-multi-spaces')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('no-multi-spaces', rule, {
+  valid: [
+    '',
+    '<template></template>',
+    '<template><div /></template>',
+    '<template><div class="foo"></div></template>',
+    '<template><div class="     foo   " style="     foo   "></div></template>',
+    '<template><div class="foo" @click="bar"></div></template>',
+    '<template><div class="foo"\n          :style="foo"></div></template>',
+    '<template><div class="foo"\n\t\t\t:style="foo"></div></template>',
+    '<template><div class="foo"\n      :style="foo"\n      ></div></template>',
+    '<template><div class="foo"\n                       :style="foo" /></template>',
+    '<template><div class="foo"\n                       :style="foo"\n                            /></template>',
+    '<template><div>{{ test }}</div></template>',
+    '<template><div>{{test}}</div></template>',
+    '<template><div>{{test}}<!-- fooo --></div></template>',
+    '<template><div>{{test}} <!--        fooo           --></div></template>',
+    '<template><div v-for="i in b">{{ i }}</div></template>',
+    '<template><div v-for=" i in b ">{{ i }}</div></template>',
+    '<template><div :test="`           `"> {{ a }} </div></template>',
+    '<template><div :test="`           `">          \n        {{ a }} </div></template>'
+  ],
+  invalid: [
+    {
+      code: '<template><div     /></template>',
+      output: '<template><div /></template>',
+      errors: [{
+        message: "Multiple spaces found before '/>'.",
+        type: 'HTMLSelfClosingTagClose'
+      }]
+    },
+    {
+      code: '<template><div   class="foo"  /></template>',
+      output: '<template><div class="foo" /></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before 'class'.",
+          type: 'HTMLIdentifier'
+        },
+        {
+          message: "Multiple spaces found before '/>'.",
+          type: 'HTMLSelfClosingTagClose'
+        }
+      ]
+    },
+    {
+      code: '<template><div\t\tclass="foo"\t\t/></template>',
+      output: '<template><div class="foo" /></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before 'class'.",
+          type: 'HTMLIdentifier'
+        },
+        {
+          message: "Multiple spaces found before '/>'.",
+          type: 'HTMLSelfClosingTagClose'
+        }
+      ]
+    },
+    {
+      code: '<template><div   :class="foo"  /></template>',
+      output: '<template><div :class="foo" /></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before ':class'.",
+          type: 'HTMLIdentifier'
+        },
+        {
+          message: "Multiple spaces found before '/>'.",
+          type: 'HTMLSelfClosingTagClose'
+        }
+      ]
+    },
+    {
+      code: '<template><div :foo="" class="foo"  /></template>',
+      output: '<template><div :foo="" class="foo" /></template>',
+      errors: [{
+        message: "Multiple spaces found before '/>'.",
+        type: 'HTMLSelfClosingTagClose'
+      }]
+    },
+    {
+      code: '<template><div foo="" class="foo"  /></template>',
+      output: '<template><div foo="" class="foo" /></template>',
+      errors: [{
+        message: "Multiple spaces found before '/>'.",
+        type: 'HTMLSelfClosingTagClose'
+      }]
+    },
+    {
+      code: '<template><foo v-foo="" class="foo"  /></template>',
+      output: '<template><foo v-foo="" class="foo" /></template>',
+      errors: [{
+        message: "Multiple spaces found before '/>'.",
+        type: 'HTMLSelfClosingTagClose'
+      }]
+    },
+    {
+      code: '<template><foo v-foo="" \n         class="foo"    /></template>',
+      output: '<template><foo v-foo="" \n         class="foo" /></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before '/>'.",
+          type: 'HTMLSelfClosingTagClose'
+        }
+      ]
+    },
+    {
+      code: '<template><div>{{  test  }}</div></template>',
+      output: '<template><div>{{ test }}</div></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before 'test'.",
+          type: 'Identifier'
+        },
+        {
+          message: "Multiple spaces found before '}}'.",
+          type: 'VExpressionEnd'
+        }
+      ]
+    },
+    {
+      code: '<template><div               ></div></template>',
+      output: '<template><div ></div></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before '>'.",
+          type: 'HTMLTagClose'
+        }
+      ]
+    },
+    {
+      code: '<template><div v-for="      i    in    b       ">{{ test }}</div></template>',
+      output: '<template><div v-for=" i in b ">{{ test }}</div></template>',
+      errors: [
+        {
+          message: "Multiple spaces found before 'i'.",
+          type: 'Identifier'
+        },
+        {
+          message: "Multiple spaces found before 'in'.",
+          type: 'Keyword'
+        },
+        {
+          message: "Multiple spaces found before 'b'.",
+          type: 'Identifier'
+        },
+        {
+          message: "Multiple spaces found before '\"'.",
+          type: 'Punctuator'
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-reservered-keys.js b/tests/lib/rules/no-reservered-keys.js
index 378a840c..46744a26 100644
--- a/tests/lib/rules/no-reservered-keys.js
+++ b/tests/lib/rules/no-reservered-keys.js
@@ -11,6 +11,12 @@
 const rule = require('../../../lib/rules/no-reservered-keys')
 const RuleTester = require('eslint').RuleTester
 
+const parserOptions = {
+  ecmaVersion: 7,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
 // ------------------------------------------------------------------------------
 // Tests
 // ------------------------------------------------------------------------------
@@ -39,7 +45,7 @@ ruleTester.run('no-reservered-keys', rule, {
           }
         }
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+      parserOptions
     }
   ],
 
diff --git a/tests/lib/rules/no-shared-component-data.js b/tests/lib/rules/no-shared-component-data.js
index aad6b304..2a54ad07 100644
--- a/tests/lib/rules/no-shared-component-data.js
+++ b/tests/lib/rules/no-shared-component-data.js
@@ -32,6 +32,24 @@ ruleTester.run('no-shared-component-data', rule, {
         })
       `
     },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          ...data,
+          data () {
+            return {
+              foo: 'bar'
+            }
+          }
+        })
+      `,
+      parserOptions: {
+        ecmaVersion: 7,
+        sourceType: 'module',
+        ecmaFeatures: { experimentalObjectRestSpread: true }
+      }
+    },
     {
       filename: 'test.js',
       code: `
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index c6f95f15..a84e6fae 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -26,6 +26,7 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
   valid: [
     {
       code: `Vue.component('test', {
+        ...foo,
         computed: {
           ...test0({}),
           test1() {
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 4470e895..4886e88b 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -9,6 +9,12 @@ const RuleTester = require('eslint').RuleTester
 
 const ruleTester = new RuleTester()
 
+const parserOptions = {
+  ecmaVersion: 6,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
 ruleTester.run('order-in-components', rule, {
 
   valid: [
@@ -20,6 +26,7 @@ ruleTester.run('order-in-components', rule, {
           props: {
             propA: Number,
           },
+          ...a,
           data () {
             return {
               msg: 'Welcome to Your Vue.js App'
@@ -27,21 +34,21 @@ ruleTester.run('order-in-components', rule, {
           },
         }
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+      parserOptions
     },
     {
       filename: 'test.vue',
       code: `
         export default {}
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+      parserOptions
     },
     {
       filename: 'test.vue',
       code: `
         export default 'example-text'
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+      parserOptions
     },
     {
       filename: 'test.jsx',
@@ -55,7 +62,7 @@ ruleTester.run('order-in-components', rule, {
           },
         }
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+      parserOptions
     },
     {
       filename: 'test.js',
@@ -136,7 +143,7 @@ ruleTester.run('order-in-components', rule, {
           },
         }
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parserOptions,
       errors: [{
         message: 'The "props" property should be above the "data" property on line 4.',
         line: 9
@@ -268,7 +275,7 @@ ruleTester.run('order-in-components', rule, {
           name: 'burger',
         };
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parserOptions,
       errors: [{
         message: 'The "name" property should be above the "data" property on line 3.',
         line: 16
@@ -284,7 +291,7 @@ ruleTester.run('order-in-components', rule, {
           test: 'ok'
         };
       `,
-      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      parserOptions,
       options: [{ order: ['data', 'test', 'name'] }],
       errors: [{
         message: 'The "test" property should be above the "name" property on line 5.',
diff --git a/tests/lib/rules/require-prop-types.js b/tests/lib/rules/require-prop-types.js
index 835d2552..f630354c 100644
--- a/tests/lib/rules/require-prop-types.js
+++ b/tests/lib/rules/require-prop-types.js
@@ -24,6 +24,7 @@ ruleTester.run('require-prop-types', rule, {
       filename: 'test.vue',
       code: `
         export default {
+          ...foo,
           props: {
             ...test(),
             foo: String
diff --git a/tests/lib/rules/require-render-return.js b/tests/lib/rules/require-render-return.js
index 5aebda6e..c12910ff 100644
--- a/tests/lib/rules/require-render-return.js
+++ b/tests/lib/rules/require-render-return.js
@@ -26,6 +26,7 @@ ruleTester.run('require-render-return', rule, {
   valid: [
     {
       code: `Vue.component('test', {
+        ...foo,
         render() {
           return {}
         }
diff --git a/tests/lib/rules/require-valid-default-prop.js b/tests/lib/rules/require-valid-default-prop.js
index b95fa71a..b8794fbf 100644
--- a/tests/lib/rules/require-valid-default-prop.js
+++ b/tests/lib/rules/require-valid-default-prop.js
@@ -13,7 +13,8 @@ const RuleTester = require('eslint').RuleTester
 
 const parserOptions = {
   ecmaVersion: 6,
-  sourceType: 'module'
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true, jsx: true }
 }
 
 function errorMessage (type) {
@@ -32,6 +33,14 @@ const ruleTester = new RuleTester()
 ruleTester.run('require-valid-default-prop', rule, {
 
   valid: [
+    {
+      filename: 'test.vue',
+      code: `export default {
+        ...foo,
+        props: { ...foo }
+      }`,
+      parserOptions
+    },
     {
       filename: 'test.vue',
       code: `export default {
@@ -65,8 +74,13 @@ ruleTester.run('require-valid-default-prop', rule, {
           foo: null,
           foo: Number,
           foo: [String, Number],
+          foo: { },
+          foo: { type: String },
           foo: { type: Number, default: VAR_BAR },
           foo: { type: Number, default: 100 },
+          foo: { type: Number, default: Number.MAX_VALUE },
+          foo: { type: Number, default: Foo.BAR },
+          foo: { type: {}, default: '' },
           foo: { type: [String, Number], default: '' },
           foo: { type: [String, Number], default: 0 },
           foo: { type: String, default: '' },
@@ -80,6 +94,8 @@ ruleTester.run('require-valid-default-prop', rule, {
           foo: { type: Symbol, default () { } },
           foo: { type: Array, default () { } },
           foo: { type: Symbol, default: Symbol('a') },
+          foo: { type: String, default: \`Foo\` },
+          foo: { type: Foo, default: Foo('a') },
           foo: { type: String, default: \`Foo\` }
         }
       })`,
diff --git a/tests/lib/rules/valid-v-for.js b/tests/lib/rules/valid-v-for.js
index 80da1818..e3da7c5e 100644
--- a/tests/lib/rules/valid-v-for.js
+++ b/tests/lib/rules/valid-v-for.js
@@ -78,6 +78,10 @@ tester.run('valid-v-for', rule, {
     {
       filename: 'test.vue',
       code: '<template v-for="x of list"><slot name="item" /></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template v-for="x of list">foo<div></div></template>'
     }
   ],
   invalid: [
@@ -170,6 +174,11 @@ tester.run('valid-v-for', rule, {
       filename: 'test.vue',
       code: '<template><div><template v-for="x in list" :key="x"><custom-component></custom-component></template></div></template>',
       errors: ["Custom elements in iteration require 'v-bind:key' directives."]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><template v-for="xin list"><div></div></template></div></template>',
+      errors: ["'v-for' directives require that attribute value."]
     }
   ]
 })