Repository: vuejs/eslint-plugin-vue
Pull request #1157 (2020-05-23 02:16:59)

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


Patch:
diff --git a/docs/rules/space-in-parens.md b/docs/rules/space-in-parens.md
new file mode 100644
index 00000000..3b39305d
--- /dev/null
+++ b/docs/rules/space-in-parens.md
@@ -0,0 +1,23 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/space-in-parens
+description: enforce consistent spacing inside parentheses
+---
+# vue/space-in-parens
+> enforce consistent spacing inside parentheses
+
+- :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 [space-in-parens] rule but it applies to the expressions in `<template>`.
+
+## :books: Further reading
+
+- [space-in-parens]
+
+[space-in-parens]: https://eslint.org/docs/rules/space-in-parens
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/space-in-parens.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/space-in-parens.js)
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 3b26ae1b..5a9487ca 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -32,6 +32,7 @@ module.exports = {
     'vue/padding-line-between-blocks': 'off',
     'vue/script-indent': 'off',
     'vue/singleline-html-element-content-newline': 'off',
+    'vue/space-in-parens': 'off',
     'vue/space-infix-ops': 'off',
     'vue/space-unary-ops': 'off',
     'vue/template-curly-spacing': 'off'
diff --git a/lib/rules/space-in-parens.js b/lib/rules/space-in-parens.js
new file mode 100644
index 00000000..f3b03e22
--- /dev/null
+++ b/lib/rules/space-in-parens.js
@@ -0,0 +1,12 @@
+/**
+ * @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/space-in-parens'),
+  { skipDynamicArguments: true, skipDynamicArgumentsReport: true }
+)
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 698a9a04..0f68bf11 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -117,7 +117,9 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument (context) {
     report (descriptor, ...args) {
       let range = null
       if (descriptor.loc) {
-        range = [sourceCode.getIndexFromLoc(descriptor.loc.start), sourceCode.getIndexFromLoc(descriptor.loc.end)]
+        const startLoc = isLoc(descriptor.loc.start) ? descriptor.loc.start : descriptor.loc
+        const endLoc = descriptor.loc.end || startLoc
+        range = [sourceCode.getIndexFromLoc(startLoc), sourceCode.getIndexFromLoc(endLoc)]
       } else if (descriptor.node) {
         range = descriptor.node.range
       }
@@ -131,6 +133,10 @@ function wrapContextToOverrideReportMethodToSkipDynamicArgument (context) {
       context.report(descriptor, ...args)
     }
   }
+
+  function isLoc (loc) {
+    return loc && typeof loc === 'object' && typeof loc.line === 'number' && typeof loc.column === 'number'
+  }
 }
 
 // ------------------------------------------------------------------------------


Test patch:
diff --git a/tests/lib/rules/space-in-parens.js b/tests/lib/rules/space-in-parens.js
new file mode 100644
index 00000000..11e83591
--- /dev/null
+++ b/tests/lib/rules/space-in-parens.js
@@ -0,0 +1,205 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const { RuleTester, CLIEngine } = require('eslint')
+const semver = require('semver')
+const rule = require('../../../lib/rules/space-in-parens')
+
+const errorMessage = semver.lt(CLIEngine.version, '6.4.0')
+  ? (obj) => {
+    const messageId = obj.messageId
+    delete obj.messageId
+    obj.message = messageId.startsWith('missing') ? 'There must be a space inside this paren.' : 'There should be no spaces inside this paren.'
+    return obj
+  }
+  : obj => obj
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('space-in-parens', rule, {
+  valid: [
+    `<template>
+      <button
+        @click="foo(arg)"
+      />
+    </template>`,
+    {
+      code: `
+      <template>
+        <button
+          @click="foo( arg )"
+        />
+      </template>`,
+      options: ['always']
+    },
+    `
+    <template>
+      <button
+        :[foo(arg)]="foo(arg)"
+      />
+    </template>`,
+    {
+      code: `
+      <template>
+        <button
+          :[foo(arg)]="foo( arg )"
+        />
+      </template>`,
+      options: ['always']
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <button
+          @click="foo( arg )"
+        />
+      </template>`,
+      output: `
+      <template>
+        <button
+          @click="foo(arg)"
+        />
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'rejectedOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'rejectedClosingSpace',
+          line: 4
+        })
+      ]
+    },
+    {
+      code: `
+      <template>
+        <button
+          @click="foo(arg)"
+        />
+      </template>`,
+      options: ['always'],
+      output: `
+      <template>
+        <button
+          @click="foo( arg )"
+        />
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'missingOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'missingClosingSpace',
+          line: 4
+        })
+      ]
+    },
+    {
+      code: `
+      <template>
+        <input
+          :value="( 1 + 2 ) + 3"
+        >
+      </template>`,
+      output: `
+      <template>
+        <input
+          :value="(1 + 2) + 3"
+        >
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'rejectedOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'rejectedClosingSpace',
+          line: 4
+        })
+      ]
+    },
+    {
+      code: `
+      <template>
+        <input
+          :value="(1 + 2) + 3"
+        >
+      </template>`,
+      options: ['always'],
+      output: `
+      <template>
+        <input
+          :value="( 1 + 2 ) + 3"
+        >
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'missingOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'missingClosingSpace',
+          line: 4
+        })
+      ]
+    },
+    {
+      code: `
+      <template>
+        <input
+          :[(1+2)]="( 1 + 2 ) + 3"
+        >
+      </template>`,
+      output: `
+      <template>
+        <input
+          :[(1+2)]="(1 + 2) + 3"
+        >
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'rejectedOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'rejectedClosingSpace',
+          line: 4
+        })
+      ]
+    },
+    {
+      code: `
+      <template>
+        <input
+          :[(1+2)]="(1 + 2) + 3"
+        >
+      </template>`,
+      options: ['always'],
+      output: `
+      <template>
+        <input
+          :[(1+2)]="( 1 + 2 ) + 3"
+        >
+      </template>`,
+      errors: [
+        errorMessage({
+          messageId: 'missingOpeningSpace',
+          line: 4
+        }),
+        errorMessage({
+          messageId: 'missingClosingSpace',
+          line: 4
+        })
+      ]
+    }
+  ]
+})