Repository: vuejs/eslint-plugin-vue
Pull request #1159 (2020-05-23 09:55:05)

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


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 8916464e..57890dec 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -278,6 +278,7 @@ For example:
 | [vue/camelcase](./camelcase.md) | enforce camelcase naming convention |  |
 | [vue/comma-dangle](./comma-dangle.md) | require or disallow trailing commas | :wrench: |
 | [vue/comma-spacing](./comma-spacing.md) | enforce consistent spacing before and after commas | :wrench: |
+| [vue/comma-style](./comma-style.md) | enforce consistent comma style | :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/dot-location](./dot-location.md) | enforce consistent newlines before and after dots | :wrench: |
 | [vue/eqeqeq](./eqeqeq.md) | require the use of `===` and `!==` | :wrench: |
@@ -308,6 +309,7 @@ For example:
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
 | [vue/sort-keys](./sort-keys.md) | enforce sort-keys in a manner that is compatible with order-in-components |  |
+| [vue/space-in-parens](./space-in-parens.md) | enforce consistent spacing inside parentheses | :wrench: |
 | [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators | :wrench: |
 | [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators | :wrench: |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: |
diff --git a/docs/rules/comma-style.md b/docs/rules/comma-style.md
new file mode 100644
index 00000000..6e4ea742
--- /dev/null
+++ b/docs/rules/comma-style.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/comma-style
+description: enforce consistent comma style
+---
+# vue/comma-style
+> enforce consistent comma style
+
+- :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 [comma-style] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [comma-style]
+
+[comma-style]: https://eslint.org/docs/rules/comma-style
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/comma-style.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/comma-style.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 5a9487ca..57ea8002 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -11,6 +11,7 @@ module.exports = {
     'vue/brace-style': 'off',
     'vue/comma-dangle': 'off',
     'vue/comma-spacing': 'off',
+    'vue/comma-style': 'off',
     'vue/dot-location': 'off',
     'vue/html-closing-bracket-newline': 'off',
     'vue/html-closing-bracket-spacing': 'off',
diff --git a/lib/index.js b/lib/index.js
index 66c381b3..5e52f514 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -16,6 +16,7 @@ module.exports = {
     'camelcase': require('./rules/camelcase'),
     'comma-dangle': require('./rules/comma-dangle'),
     'comma-spacing': require('./rules/comma-spacing'),
+    'comma-style': require('./rules/comma-style'),
     'comment-directive': require('./rules/comment-directive'),
     'component-definition-name-casing': require('./rules/component-definition-name-casing'),
     'component-name-in-template-casing': require('./rules/component-name-in-template-casing'),
@@ -114,6 +115,7 @@ module.exports = {
     'script-indent': require('./rules/script-indent'),
     'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
     'sort-keys': require('./rules/sort-keys'),
+    'space-in-parens': require('./rules/space-in-parens'),
     'space-infix-ops': require('./rules/space-infix-ops'),
     'space-unary-ops': require('./rules/space-unary-ops'),
     'static-class-names-order': require('./rules/static-class-names-order'),
diff --git a/lib/rules/comma-style.js b/lib/rules/comma-style.js
new file mode 100644
index 00000000..87ffaabc
--- /dev/null
+++ b/lib/rules/comma-style.js
@@ -0,0 +1,20 @@
+/**
+ * @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/comma-style'),
+  {
+    create (_context, { coreHandlers }) {
+      return {
+        VSlotScopeExpression (node) {
+          coreHandlers.FunctionExpression(node)
+        }
+      }
+    }
+  }
+)
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 0f68bf11..735d182d 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -22,6 +22,7 @@
 
 /**
  * @typedef {import('eslint').Rule.RuleContext} RuleContext
+ * @typedef {import('eslint').Rule.RuleModule} RuleModule
  * @typedef {import('vue-eslint-parser').AST.Token} Token
  */
 
@@ -154,28 +155,20 @@ module.exports = {
    * @param {Object} [scriptVisitor] The visitor to traverse the script.
    * @returns {Object} The merged visitor.
    */
-  defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
-    if (context.parserServices.defineTemplateBodyVisitor == null) {
-      context.report({
-        loc: { line: 1, column: 0 },
-        message: 'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
-      })
-      return {}
-    }
-    return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
-  },
+  defineTemplateBodyVisitor,
 
   /**
    * Wrap a given core rule to apply it to Vue.js template.
-   * @param {Rule} coreRule The core rule implementation to wrap.
+   * @param {RuleModule} coreRule The core rule implementation to wrap.
    * @param {Object} [options] The option of this rule.
-   * @param {string|undefined} options.category The category of this rule.
-   * @param {boolean|undefined} options.skipDynamicArguments If `true`, skip validation within dynamic arguments.
-   * @param {boolean|undefined} options.skipDynamicArgumentsReport If `true`, skip report within dynamic arguments.
-   * @returns {Rule} The wrapped rule implementation.
+   * @param {string} [options.category] The category of this rule.
+   * @param {boolean} [options.skipDynamicArguments] If `true`, skip validation within dynamic arguments.
+   * @param {boolean} [options.skipDynamicArgumentsReport] If `true`, skip report within dynamic arguments.
+   * @param {RuleModule["create"]} [options.create] If define, extend core rule.
+   * @returns {RuleModule} The wrapped rule implementation.
    */
   wrapCoreRule (coreRule, options) {
-    const { category, skipDynamicArguments, skipDynamicArgumentsReport } = options || {}
+    const { category, skipDynamicArguments, skipDynamicArgumentsReport, create } = options || {}
     return {
       create (context) {
         const tokenStore =
@@ -193,7 +186,8 @@ module.exports = {
         }
 
         // Move `Program` handlers to `VElement[parent.type!='VElement']`
-        const handlers = coreRule.create(context)
+        const coreHandlers = coreRule.create(context)
+        const handlers = Object.assign({}, coreHandlers)
         if (handlers.Program) {
           handlers["VElement[parent.type!='VElement']"] = handlers.Program
           delete handlers.Program
@@ -216,8 +210,12 @@ module.exports = {
           handlers['VDirectiveKey > VExpressionContainer:exit'] = () => { withinDynamicArguments = false }
         }
 
+        if (create) {
+          compositingVisitors(handlers, create(context, { coreHandlers }))
+        }
+
         // Apply the handlers to templates.
-        return module.exports.defineTemplateBodyVisitor(context, handlers)
+        return defineTemplateBodyVisitor(context, handlers)
       },
 
       meta: Object.assign({}, coreRule.meta, {
@@ -1089,6 +1087,27 @@ module.exports = {
   }
 }
 
+/**
+ * Register the given visitor to parser services.
+ * If the parser service of `vue-eslint-parser` was not found,
+ * this generates a warning.
+ *
+ * @param {RuleContext} context The rule context to use parser services.
+ * @param {Object} templateBodyVisitor The visitor to traverse the template body.
+ * @param {Object} [scriptVisitor] The visitor to traverse the script.
+ * @returns {Object} The merged visitor.
+ */
+function defineTemplateBodyVisitor (context, templateBodyVisitor, scriptVisitor) {
+  if (context.parserServices.defineTemplateBodyVisitor == null) {
+    context.report({
+      loc: { line: 1, column: 0 },
+      message: 'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
+    })
+    return {}
+  }
+  return context.parserServices.defineTemplateBodyVisitor(templateBodyVisitor, scriptVisitor)
+}
+
 /**
  * Unwrap typescript types like "X as F"
  * @template T
@@ -1294,8 +1313,7 @@ function getComponentComments (context) {
   return tokens
 }
 
-function compositingVisitors (...visitors) {
-  const visitor = {}
+function compositingVisitors (visitor, ...visitors) {
   for (const v of visitors) {
     for (const key in v) {
       if (visitor[key]) {


Test patch:
diff --git a/tests/lib/rules/comma-style.js b/tests/lib/rules/comma-style.js
new file mode 100644
index 00000000..bd469d7a
--- /dev/null
+++ b/tests/lib/rules/comma-style.js
@@ -0,0 +1,124 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { RuleTester } = require('eslint')
+const rule = require('../../../lib/rules/comma-style')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2018 }
+})
+
+tester.run('comma-style', rule, {
+  valid: [
+    `<template>
+      <CustomButton @click="() => fn({
+        a,
+        b
+      })" />
+    </template>`,
+    {
+      code: `
+        <template>
+          <CustomButton @click="($event,
+            data) => fn()" />
+        </template>`,
+      options: ['last', { exceptions: { ArrowFunctionExpression: false }}]
+    },
+    {
+      code: `
+        <template>
+          <CustomButton @click="($event
+            , data) => fn()" />
+        </template>`,
+      options: ['first', { exceptions: { ArrowFunctionExpression: false }}]
+    }
+  ],
+  invalid: [
+    {
+      code: `
+        <template>
+          <CustomButton @click="() => fn({
+            a
+            , b
+          })" />
+        </template>`,
+      output: `
+        <template>
+          <CustomButton @click="() => fn({
+            a,
+             b
+          })" />
+        </template>`,
+      errors: [
+        {
+          message: "',' should be placed last.",
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomButton @click="($event
+            , data) => fn()" />
+        </template>`,
+      options: ['last', { exceptions: { ArrowFunctionExpression: false }}],
+      output: `
+        <template>
+          <CustomButton @click="($event,
+             data) => fn()" />
+        </template>`,
+      errors: [
+        {
+          message: "',' should be placed last.",
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomButton @click="($event,
+            data) => fn()" />
+        </template>`,
+      options: ['first', { exceptions: { ArrowFunctionExpression: false }}],
+      output: `
+        <template>
+          <CustomButton @click="($event
+            ,data) => fn()" />
+        </template>`,
+      errors: [
+        {
+          message: "',' should be placed first."
+          // line: 3 // eslint v7.0
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+          <CustomButton v-slot="foo,
+            bar" >
+            <div/>
+          </CustomButton>
+        </template>`,
+      options: ['first', { exceptions: { FunctionExpression: false }}],
+      output: `
+        <template>
+          <CustomButton v-slot="foo
+            ,bar" >
+            <div/>
+          </CustomButton>
+        </template>`,
+      errors: [
+        {
+          message: "',' should be placed first."
+          // line: 3 // eslint v7.0
+        }
+      ]
+    }
+  ]
+})