Repository: vuejs/eslint-plugin-vue
Pull request #767 (2019-01-12 08:43:56)

⭐️New: Add `vue/arrow-spacing` rule
This PR adds the wrapper rule of the `arrow-spacing` core rule to apply to the expressions in `<template>`.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index b37c250a..2e79091a 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -140,6 +140,7 @@ For example:
 | Rule ID | Description |    |
 |:--------|:------------|:---|
 | [vue/array-bracket-spacing](./array-bracket-spacing.md) | enforce consistent spacing inside array brackets | :wrench: |
+| [vue/arrow-spacing](./arrow-spacing.md) | enforce consistent spacing before and after the arrow in arrow functions | :wrench: |
 | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
 | [vue/eqeqeq](./eqeqeq.md) | require the use of `===` and `!==` | :wrench: |
 | [vue/key-spacing](./key-spacing.md) | enforce consistent spacing between keys and values in object literal properties | :wrench: |
diff --git a/docs/rules/arrow-spacing.md b/docs/rules/arrow-spacing.md
new file mode 100644
index 00000000..1caf1cb9
--- /dev/null
+++ b/docs/rules/arrow-spacing.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/arrow-spacing
+description: enforce consistent spacing before and after the arrow in arrow functions
+---
+# vue/arrow-spacing
+> enforce consistent spacing before and after the arrow in arrow functions
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+This rule is the same rule as core [arrow-spacing] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [arrow-spacing]
+
+[arrow-spacing]: https://eslint.org/docs/rules/arrow-spacing
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/arrow-spacing.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/arrow-spacing.js)
diff --git a/docs/rules/multiline-html-element-content-newline.md b/docs/rules/multiline-html-element-content-newline.md
index 9fe8d45a..1004c1ee 100644
--- a/docs/rules/multiline-html-element-content-newline.md
+++ b/docs/rules/multiline-html-element-content-newline.md
@@ -78,7 +78,8 @@ This rule enforces a line break before and after the contents of a multiline ele
 {
     "vue/multiline-html-element-content-newline": ["error", {
         "ignoreWhenEmpty": true,
-        "ignores": ["pre", "textarea"]
+        "ignores": ["pre", "textarea"],
+        "allowEmptyLines": false
     }]
 }
 ```
@@ -87,6 +88,8 @@ This rule enforces a line break before and after the contents of a multiline ele
     default `true`
 - `ignores` ... the configuration for element names to ignore line breaks style.  
     default `["pre", "textarea"]`
+- `allowEmptyLines` ... if `true`, it allows empty lines around content. If you want to disallow multiple empty lines, use [no-multiple-empty-lines] in combination.  
+    default `false`
 
 ### `"ignores": ["VueComponent", "pre", "textarea"]`
 
@@ -109,6 +112,36 @@ This rule enforces a line break before and after the contents of a multiline ele
 
 </eslint-code-block>
 
+### `"allowEmptyLines": true`
+
+<eslint-code-block fix :rules="{'vue/multiline-html-element-content-newline': ['error', { allowEmptyLines: true }]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div>
+    content
+  </div>
+  <div>
+
+    content
+
+  </div>
+
+  <!-- ✗ BAD -->
+  <div>content
+    content</div>
+</template>
+```
+
+</eslint-code-block>
+
+## :books: Further reading
+
+- [no-multiple-empty-lines]
+
+[no-multiple-empty-lines]: https://eslint.org/docs/rules/no-multiple-empty-lines
+
 ## :mag: Implementation
 
 - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multiline-html-element-content-newline.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 560bde3c..20dd92f4 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -6,6 +6,7 @@
 module.exports = {
   rules: {
     'vue/array-bracket-spacing': 'off',
+    'vue/arrow-spacing': 'off',
     'vue/html-closing-bracket-newline': 'off',
     'vue/html-closing-bracket-spacing': 'off',
     'vue/html-indent': 'off',
diff --git a/lib/index.js b/lib/index.js
index bb5b8603..c0d46541 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -8,6 +8,7 @@
 module.exports = {
   rules: {
     'array-bracket-spacing': require('./rules/array-bracket-spacing'),
+    'arrow-spacing': require('./rules/arrow-spacing'),
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
     'comment-directive': require('./rules/comment-directive'),
diff --git a/lib/rules/arrow-spacing.js b/lib/rules/arrow-spacing.js
new file mode 100644
index 00000000..6586e838
--- /dev/null
+++ b/lib/rules/arrow-spacing.js
@@ -0,0 +1,9 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { wrapCoreRule } = require('../utils')
+
+// eslint-disable-next-line
+module.exports = wrapCoreRule(require('eslint/lib/rules/arrow-spacing'))
diff --git a/lib/rules/multiline-html-element-content-newline.js b/lib/rules/multiline-html-element-content-newline.js
index b92841b8..8e900b6e 100644
--- a/lib/rules/multiline-html-element-content-newline.js
+++ b/lib/rules/multiline-html-element-content-newline.js
@@ -22,7 +22,8 @@ function isMultilineElement (element) {
 function parseOptions (options) {
   return Object.assign({
     ignores: ['pre', 'textarea'],
-    ignoreWhenEmpty: true
+    ignoreWhenEmpty: true,
+    allowEmptyLines: false
   }, options)
 }
 
@@ -69,6 +70,9 @@ module.exports = {
           items: { type: 'string' },
           uniqueItems: true,
           additionalItems: false
+        },
+        allowEmptyLines: {
+          type: 'boolean'
         }
       },
       additionalProperties: false
@@ -83,6 +87,7 @@ module.exports = {
     const options = parseOptions(context.options[0])
     const ignores = options.ignores
     const ignoreWhenEmpty = options.ignoreWhenEmpty
+    const allowEmptyLines = options.allowEmptyLines
     const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
     const sourceCode = context.getSourceCode()
 
@@ -94,6 +99,14 @@ module.exports = {
         ignores.includes(casing.kebabCase(node.rawName))
     }
 
+    function isInvalidLineBreaks (lineBreaks) {
+      if (allowEmptyLines) {
+        return lineBreaks === 0
+      } else {
+        return lineBreaks !== 1
+      }
+    }
+
     return utils.defineTemplateBodyVisitor(context, {
       'VElement' (node) {
         if (inIgnoreElement) {
@@ -127,7 +140,7 @@ module.exports = {
 
         const beforeLineBreaks = contentFirst.loc.start.line - node.startTag.loc.end.line
         const afterLineBreaks = node.endTag.loc.start.line - contentLast.loc.end.line
-        if (beforeLineBreaks !== 1) {
+        if (isInvalidLineBreaks(beforeLineBreaks)) {
           context.report({
             node: template.getLastToken(node.startTag),
             loc: {
@@ -150,7 +163,7 @@ module.exports = {
           return
         }
 
-        if (afterLineBreaks !== 1) {
+        if (isInvalidLineBreaks(afterLineBreaks)) {
           context.report({
             node: template.getFirstToken(node.endTag),
             loc: {
diff --git a/package.json b/package.json
index 964818c2..afa4e756 100644
--- a/package.json
+++ b/package.json
@@ -46,7 +46,7 @@
     "eslint": "^5.0.0"
   },
   "dependencies": {
-    "vue-eslint-parser": "^4.0.2"
+    "vue-eslint-parser": "^5.0.0"
   },
   "devDependencies": {
     "@types/node": "^4.2.16",


Test patch:
diff --git a/tests/lib/rules/arrow-spacing.js b/tests/lib/rules/arrow-spacing.js
new file mode 100644
index 00000000..04726afe
--- /dev/null
+++ b/tests/lib/rules/arrow-spacing.js
@@ -0,0 +1,125 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/arrow-spacing')
+
+const tester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('arrow-spacing', rule, {
+  valid: [
+    `<template>
+      <div :attr="() => a" />
+    </template>`,
+    `<template>
+      <div @click="() => a" />
+    </template>`,
+    `<template>
+      <div @click="
+        const fn = () => a
+        fn()
+      " />
+    </template>`,
+    {
+      code: `
+        <template>
+          <div :attr="()=>a" />
+        </template>`,
+      options: [{ before: false, after: false }]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <div :attr="()=>a" />
+        </template>`,
+      output: `
+        <template>
+          <div :attr="() => a" />
+        </template>`,
+      errors: [
+        {
+          message: 'Missing space before =>.',
+          line: 3
+        },
+        {
+          message: 'Missing space after =>.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div @click="()=>a" />
+        </template>`,
+      output: `
+        <template>
+          <div @click="() => a" />
+        </template>`,
+      errors: [
+        {
+          message: 'Missing space before =>.',
+          line: 3
+        },
+        {
+          message: 'Missing space after =>.',
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div @click="
+            const fn = ()=>a
+            fn()
+          " />
+        </template>`,
+      output: `
+        <template>
+          <div @click="
+            const fn = () => a
+            fn()
+          " />
+        </template>`,
+      errors: [
+        {
+          message: 'Missing space before =>.',
+          line: 4
+        },
+        {
+          message: 'Missing space after =>.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div :attr="() => a" />
+        </template>`,
+      options: [{ before: false, after: false }],
+      output: `
+        <template>
+          <div :attr="()=>a" />
+        </template>`,
+      errors: [
+        {
+          message: 'Unexpected space before =>.',
+          line: 3
+        },
+        {
+          message: 'Unexpected space after =>.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/multiline-html-element-content-newline.js b/tests/lib/rules/multiline-html-element-content-newline.js
index b1eaa64c..8f2f707b 100644
--- a/tests/lib/rules/multiline-html-element-content-newline.js
+++ b/tests/lib/rules/multiline-html-element-content-newline.js
@@ -72,6 +72,43 @@ tester.run('multiline-html-element-content-newline', rule, {
           class="panel">
         </div>
       </template>`,
+    // allowEmptyLines
+    {
+      code: `
+        <template>
+          <div
+            class="panel">
+
+          </div>
+        </template>`,
+      options: [{ allowEmptyLines: true, ignoreWhenEmpty: false }]
+    },
+    {
+      code: `
+        <template>
+          <div
+            class="panel">
+
+            contents
+
+          </div>
+        </template>`,
+      options: [{ allowEmptyLines: true }]
+    },
+    {
+      code: `
+        <template>
+          <div
+            class="panel">
+
+
+            contents
+
+
+          </div>
+        </template>`,
+      options: [{ allowEmptyLines: true }]
+    },
     // self closing
     `
       <template>
@@ -439,6 +476,65 @@ content
         'Expected 1 line break before closing tag (`</div>`), but 2 line breaks found.'
       ]
     },
+    // allowEmptyLines
+    {
+      code: `
+        <template>
+          <div
+            class="panel">
+
+          </div>
+          <div
+            class="panel"></div>
+        </template>`,
+      options: [{ allowEmptyLines: true, ignoreWhenEmpty: false }],
+      output: `
+        <template>
+          <div
+            class="panel">
+
+          </div>
+          <div
+            class="panel">
+</div>
+        </template>`,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.'
+      ]
+    },
+    {
+      code: `
+        <template>
+          <div>
+
+            content
+            content
+
+          </div>
+          <div>content
+            content</div>
+        </template>
+      `,
+      options: [{ allowEmptyLines: true }],
+      output: `
+        <template>
+          <div>
+
+            content
+            content
+
+          </div>
+          <div>
+content
+            content
+</div>
+        </template>
+      `,
+      errors: [
+        'Expected 1 line break after opening tag (`<div>`), but no line breaks found.',
+        'Expected 1 line break before closing tag (`</div>`), but no line breaks found.'
+      ]
+    },
     // mustache
     {
       code: `
diff --git a/tests/lib/rules/no-unused-vars.js b/tests/lib/rules/no-unused-vars.js
index e033d487..f717e7af 100644
--- a/tests/lib/rules/no-unused-vars.js
+++ b/tests/lib/rules/no-unused-vars.js
@@ -36,10 +36,10 @@ tester.run('no-unused-vars', rule, {
       code: '<template><ol v-for="i in data"><li v-for="f in i">{{ f.bar.baz }}</li></ol></template>'
     },
     {
-      code: '<template scope="props">{{props}}</template>'
+      code: '<template><template scope="props">{{props}}</template></template>'
     },
     {
-      code: '<template scope="props"><span v-if="props"></span></template>'
+      code: '<template><template scope="props"><span v-if="props"></span></template></template>'
     },
     {
       code: '<template><div v-for="(item, key) in items" :key="key">{{item.name}}</div></template>'
@@ -57,11 +57,19 @@ tester.run('no-unused-vars', rule, {
       errors: ["'i' is defined but never used."]
     },
     {
-      code: '<template scope="props"></template>',
+      code: '<template><template scope="props"></template></template>',
       errors: ["'props' is defined but never used."]
     },
     {
-      code: '<template v-for="i in 5"><comp v-for="j in 10">{{i}}{{i}}</comp></template>',
+      code: '<template><span slot-scope="props"></span></template>',
+      errors: ["'props' is defined but never used."]
+    },
+    {
+      code: '<template><span><template scope="props"></template></span></template>',
+      errors: ["'props' is defined but never used."]
+    },
+    {
+      code: '<template><div v-for="i in 5"><comp v-for="j in 10">{{i}}{{i}}</comp></div></template>',
       errors: ["'j' is defined but never used."]
     },
     {
diff --git a/tests/lib/rules/valid-v-else-if.js b/tests/lib/rules/valid-v-else-if.js
index dd0b0724..ca2b3ba5 100644
--- a/tests/lib/rules/valid-v-else-if.js
+++ b/tests/lib/rules/valid-v-else-if.js
@@ -43,7 +43,7 @@ tester.run('valid-v-else-if', rule, {
   invalid: [
     {
       filename: 'test.vue',
-      code: '<template v-else-if="foo"><div></div></template>',
+      code: '<template><template v-else-if="foo"><div></div></template></template>',
       errors: ["'v-else-if' directives require being preceded by the element which has a 'v-if' or 'v-else-if' directive."]
     },
     {
diff --git a/tests/lib/rules/valid-v-else.js b/tests/lib/rules/valid-v-else.js
index bce68152..21608b0d 100644
--- a/tests/lib/rules/valid-v-else.js
+++ b/tests/lib/rules/valid-v-else.js
@@ -43,7 +43,7 @@ tester.run('valid-v-else', rule, {
   invalid: [
     {
       filename: 'test.vue',
-      code: '<template v-else><div></div></template>',
+      code: '<template><template v-else><div></div></template></template>',
       errors: ["'v-else' directives require being preceded by the element which has a 'v-if' or 'v-else' directive."]
     },
     {
diff --git a/tests/lib/rules/valid-v-for.js b/tests/lib/rules/valid-v-for.js
index 6fe1f0f1..d36d1172 100644
--- a/tests/lib/rules/valid-v-for.js
+++ b/tests/lib/rules/valid-v-for.js
@@ -77,11 +77,11 @@ tester.run('valid-v-for', rule, {
     },
     {
       filename: 'test.vue',
-      code: '<template v-for="x of list"><slot name="item" /></template>'
+      code: '<template><template v-for="x of list"><slot name="item" /></template></template>'
     },
     {
       filename: 'test.vue',
-      code: '<template v-for="x of list">foo<div></div></template>'
+      code: '<template><template v-for="x of list">foo<div></div></template></template>'
     },
     {
       filename: 'test.vue',
@@ -90,24 +90,30 @@ tester.run('valid-v-for', rule, {
     {
       filename: 'test.vue',
       code: `
-        <template v-for="x in xs">
-          <template v-for="y in x.ys">
-            <li v-for="z in y.zs" :key="z.id">
-              123
-            </li>
+        <template>
+          <template v-for="x in xs">
+            <template v-for="y in x.ys">
+              <li v-for="z in y.zs" :key="z.id">
+                123
+              </li>
+            </template>
           </template>
-        </template>`
+        </template>
+      `
     },
     {
       filename: 'test.vue',
       code: `
-        <template v-for="x in xs">
-          <template v-for="y in ys">
-            <li v-for="z in zs" :key="x.id + y.id + z.id">
-              123
-            </li>
+        <template>
+          <template v-for="x in xs">
+            <template v-for="y in ys">
+              <li v-for="z in zs" :key="x.id + y.id + z.id">
+                123
+              </li>
+            </template>
           </template>
-        </template>`
+        </template>
+      `
     }
   ],
   invalid: [
@@ -215,38 +221,46 @@ tester.run('valid-v-for', rule, {
       filename: 'test.vue',
       errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."],
       code: `
-        <template v-for="x in xs">
-          <template v-for="y in a.ys">
-            <li v-for="z in y.zs" :key="z.id">
-              123
-            </li>
+        <template>
+          <template v-for="x in xs">
+            <template v-for="y in a.ys">
+              <li v-for="z in y.zs" :key="z.id">
+                123
+              </li>
+            </template>
           </template>
-        </template>`
+        </template>
+      `
     },
     {
       filename: 'test.vue',
       errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."],
       code: `
-        <template v-for="x in xs">
-          <template v-for="y in x.ys">
-            <li v-for="z in a.zs" :key="z.id">
-              123
-            </li>
+        <template>
+          <template v-for="x in xs">
+            <template v-for="y in x.ys">
+              <li v-for="z in a.zs" :key="z.id">
+                123
+              </li>
+            </template>
           </template>
-        </template>`
+        </template>
+      `
     },
     {
       filename: 'test.vue',
       errors: ["Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."],
       code: `
-        <template v-for="x in xs">
-          <template v-for="y in x.ys">
-            <li v-for="z in x.zs" :key="z.id">
-              123
-            </li>
+        <template>
+          <template v-for="x in xs">
+            <template v-for="y in x.ys">
+              <li v-for="z in x.zs" :key="z.id">
+                123
+              </li>
+            </template>
           </template>
-        </template>`
+        </template>
+      `
     }
-
   ]
 })