Repository: vuejs/eslint-plugin-vue
Pull request #94 (2017-07-18 18:31:15)

Add `name-property-casing` rule.
This PR implements rules proposed in #93

**DONE:**
- [x] Create documtation
- [x] Add tests
- [x] Implement rule


Patch:
diff --git a/README.md b/README.md
index 3789ad0d..7a56e72a 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :wrench: | [html-no-self-closing](./docs/rules/html-no-self-closing.md) | disallow self-closing elements. |
 | :white_check_mark: | [no-confusing-v-for-v-if](./docs/rules/no-confusing-v-for-v-if.md) | disallow confusing `v-for` and `v-if` on the same element. |
 |  | [no-duplicate-attributes](./docs/rules/no-duplicate-attributes.md) | disallow duplicate attributes. |
+|  | [no-side-effects-in-computed-properties](./docs/rules/no-side-effects-in-computed-properties.md) | Don't introduce side effects in computed properties |
 | :white_check_mark: | [no-textarea-mustache](./docs/rules/no-textarea-mustache.md) | disallow mustaches in `<textarea>`. |
 |  | [order-in-components](./docs/rules/order-in-components.md) | Keep order of properties in components |
 | :white_check_mark: | [require-component-is](./docs/rules/require-component-is.md) | require `v-bind:is` of `<component>` elements. |
@@ -110,6 +111,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :white_check_mark: | [no-invalid-v-text](./docs/rules/no-invalid-v-text.md) | disallow invalid `v-text` directives. |
 | :white_check_mark: | [no-parsing-error](./docs/rules/no-parsing-error.md) | disallow parsing errors in `<template>`. |
 |  | [no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>`. |
+|  | [return-in-computed-property](./docs/rules/return-in-computed-property.md) | Enforces that a return statement is present in computed property. |
 
 <!--RULES_TABLE_END-->
 
diff --git a/docs/rules/name-property-casing.md b/docs/rules/name-property-casing.md
new file mode 100644
index 00000000..316cda8f
--- /dev/null
+++ b/docs/rules/name-property-casing.md
@@ -0,0 +1,37 @@
+# Requires specific casing for the name property in Vue components (name-property-casing)
+
+Define a style for the `name` property casing for consistency purposes.
+
+## :book: Rule Details
+
+:+1: Examples of **correct** code for `PascalCase`:
+
+```js
+export default {
+  name: 'MyComponent'
+}
+```
+
+:+1: Examples of **correct** code for `kebab-case`:
+
+```js
+export default {
+  name: 'my-component'
+}
+```
+
+:+1: Examples of **correct** code for `camelCase`:
+
+```js
+export default {
+  name: 'myComponent'
+}
+```
+
+## :wrench: Options
+
+Default casing is set to `PascalCase`
+
+```
+'vue/name-property-casing': [2, 'camelCase|kebab-case|PascalCase']
+```
diff --git a/docs/rules/return-in-computed-property.md b/docs/rules/return-in-computed-property.md
new file mode 100644
index 00000000..5f66d08b
--- /dev/null
+++ b/docs/rules/return-in-computed-property.md
@@ -0,0 +1,44 @@
+# Enforces that a return statement is present in computed property (return-in-computed-property)
+
+## :book: Rule Details
+
+This rule enforces that a `return` statement is present in `computed` properties.
+
+:-1: Examples of **incorrect** code for this rule:
+
+```js
+export default {
+  computed: {
+    foo () {
+    },
+    bar: function () {
+    }
+  }
+}
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```js
+export default {
+  computed: {
+    foo () {
+      return true
+    },
+    bar: function () {
+      return false
+    }
+  }
+}
+```
+
+## :wrench: Options
+
+This rule has an object option:
+- `"treatUndefinedAsUnspecified"`: `true` (default) disallows implicitly returning undefined with a `return;` statement.
+
+```
+vue/return-in-computed-property: [2, {
+  treatUndefinedAsUnspecified: true
+}]
+```
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 3d68d82a..1349dee4 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -25,11 +25,13 @@ module.exports = {
   "vue/no-invalid-v-show": "error",
   "vue/no-invalid-v-text": "error",
   "vue/no-parsing-error": "error",
+  "vue/no-side-effects-in-computed-properties": "off",
   "vue/no-template-key": "off",
   "vue/no-textarea-mustache": "error",
   "vue/order-in-components": "off",
   "vue/require-component-is": "error",
   "vue/require-v-for-key": "error",
+  "vue/return-in-computed-property": "off",
   "vue/v-bind-style": "off",
   "vue/v-on-style": "off"
 }
\ No newline at end of file
diff --git a/lib/rules/name-property-casing.js b/lib/rules/name-property-casing.js
new file mode 100644
index 00000000..31a20113
--- /dev/null
+++ b/lib/rules/name-property-casing.js
@@ -0,0 +1,99 @@
+/**
+ * @fileoverview Requires specific casing for the name property in Vue components
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+
+function kebabCase (str) {
+  return str
+    .replace(/([a-z])([A-Z])/g, match => match[0] + '-' + match[1])
+    .replace(/[^a-zA-Z:]+/g, '-')
+    .toLowerCase()
+}
+
+function camelCase (str) {
+  return str
+    .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => (
+      index === 0 ? letter.toLowerCase() : letter.toUpperCase())
+    )
+    .replace(/[^a-zA-Z:]+/g, '')
+}
+
+function pascalCase (str) {
+  return str
+    .replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => letter.toUpperCase())
+    .replace(/[^a-zA-Z:]+/g, '')
+}
+
+const allowedCaseOptions = [
+  'camelCase',
+  'kebab-case',
+  'PascalCase'
+]
+
+const convertersMap = {
+  'kebab-case': kebabCase,
+  'camelCase': camelCase,
+  'PascalCase': pascalCase
+}
+
+function getConverter (name) {
+  return convertersMap[name] || pascalCase
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+function create (context) {
+  const options = context.options[0]
+  const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'PascalCase'
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  return utils.executeOnVueComponent(context, (obj) => {
+    const node = obj.properties
+      .filter(item => (
+        item.type === 'Property' &&
+        item.key.name === 'name' &&
+        item.value.type === 'Literal'
+      ))[0]
+
+    if (!node) return
+
+    const value = getConverter(caseType)(node.value.value)
+    if (value !== node.value.value) {
+      context.report({
+        node: node.value,
+        message: 'Property name "{{value}}" is not {{caseType}}.',
+        data: {
+          value: node.value.value,
+          caseType: caseType
+        },
+        fix: fixer => fixer.replaceText(node.value, node.value.raw.replace(node.value.value, value))
+      })
+    }
+  })
+}
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Requires specific casing for the name property in Vue components',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'code',  // or "code" or "whitespace"
+    schema: [
+      {
+        enum: allowedCaseOptions
+      }
+    ]
+  },
+
+  create
+}
diff --git a/lib/rules/no-invalid-v-on.js b/lib/rules/no-invalid-v-on.js
index c2a62aeb..a271d45f 100644
--- a/lib/rules/no-invalid-v-on.js
+++ b/lib/rules/no-invalid-v-on.js
@@ -33,13 +33,6 @@ const VERB_MODIFIERS = new Set([
 function create (context) {
   utils.registerTemplateBodyVisitor(context, {
     "VAttribute[directive=true][key.name='on']" (node) {
-      if (!node.key.argument) {
-        context.report({
-          node,
-          loc: node.loc,
-          message: "'v-on' directives require event names."
-        })
-      }
       for (const modifier of node.key.modifiers) {
         if (!VALID_MODIFIERS.has(modifier) && !Number.isInteger(parseInt(modifier, 10))) {
           context.report({
diff --git a/lib/rules/return-in-computed-property.js b/lib/rules/return-in-computed-property.js
new file mode 100644
index 00000000..5a336781
--- /dev/null
+++ b/lib/rules/return-in-computed-property.js
@@ -0,0 +1,114 @@
+/**
+ * @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+
+function create (context) {
+  const options = context.options[0] || {}
+  const treatUndefinedAsUnspecified = !(options.treatUndefinedAsUnspecified === false)
+
+  let funcInfo = {
+    funcInfo: null,
+    codePath: null,
+    hasReturn: false,
+    hasReturnValue: false,
+    node: null
+  }
+  const forbiddenNodes = []
+
+  // ----------------------------------------------------------------------
+  // Helpers
+  // ----------------------------------------------------------------------
+  function isValidReturn () {
+    if (!funcInfo.hasReturn) {
+      return false
+    }
+    return !treatUndefinedAsUnspecified || funcInfo.hasReturnValue
+  }
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  return Object.assign({},
+    {
+      onCodePathStart (codePath, node) {
+        funcInfo = {
+          codePath,
+          funcInfo: funcInfo,
+          hasReturn: false,
+          hasReturnValue: false,
+          node
+        }
+      },
+      onCodePathEnd () {
+        funcInfo = funcInfo.funcInfo
+      },
+      ReturnStatement (node) {
+        funcInfo.hasReturn = true
+        funcInfo.hasReturnValue = Boolean(node.argument)
+      },
+      'FunctionExpression:exit' (node) {
+        if (!isValidReturn()) {
+          forbiddenNodes.push({
+            hasReturn: funcInfo.hasReturn,
+            node: funcInfo.node,
+            type: 'return'
+          })
+        }
+      }
+    },
+    utils.executeOnVueComponent(context, properties => {
+      const computedProperties = utils.getComputedProperties(properties)
+
+      computedProperties.forEach(cp => {
+        forbiddenNodes.forEach(el => {
+          if (
+            cp.value &&
+            el.node.loc.start.line >= cp.value.loc.start.line &&
+            el.node.loc.end.line <= cp.value.loc.end.line
+          ) {
+            context.report({
+              node: el.node,
+              message: 'Expected to return a value in "{{name}}" computed property.',
+              data: {
+                name: cp.key
+              }
+            })
+          }
+        })
+      })
+    })
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Enforces that a return statement is present in computed property.',
+      category: 'Possible Errors',
+      recommended: false
+    },
+    fixable: null,  // or "code" or "whitespace"
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          treatUndefinedAsUnspecified: {
+            type: 'boolean'
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+
+  create
+}
diff --git a/package.json b/package.json
index 7225e923..e3b596cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "3.6.0",
+  "version": "3.7.0",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {


Test patch:
diff --git a/tests/lib/rules/name-property-casing.js b/tests/lib/rules/name-property-casing.js
new file mode 100644
index 00000000..07df1508
--- /dev/null
+++ b/tests/lib/rules/name-property-casing.js
@@ -0,0 +1,170 @@
+/**
+ * @fileoverview Define a style for the name property casing for consistency purposes
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/name-property-casing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('name-property-casing', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'fooBar'
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'FooBar'
+        }
+      `,
+      options: ['PascalCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo-bar'
+        }
+      `,
+      options: ['kebab-case'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' }
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo-bar'
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo-bar" is not camelCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo  bar'
+        }
+      `,
+      options: ['PascalCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo  bar" is not PascalCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo!bar'
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo!bar" is not camelCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          name: 'foo!bar'
+        })
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Property name "foo!bar" is not camelCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo_bar'
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo_bar" is not camelCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo_bar'
+        }
+      `,
+      options: ['PascalCase'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo_bar" is not PascalCase.',
+        type: 'Literal',
+        line: 3
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'foo_bar'
+        }
+      `,
+      options: ['kebab-case'],
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Property name "foo_bar" is not kebab-case.',
+        type: 'Literal',
+        line: 3
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-invalid-v-bind.js b/tests/lib/rules/no-invalid-v-bind.js
index cf1a0a14..38592097 100644
--- a/tests/lib/rules/no-invalid-v-bind.js
+++ b/tests/lib/rules/no-invalid-v-bind.js
@@ -70,6 +70,10 @@ tester.run('no-invalid-v-bind', rule, {
     {
       filename: 'test.vue',
       code: "<template><div :aaa.camel='bbb'></div></template>"
+    },
+    {
+      filename: 'test.vue',
+      code: "<template><input v-bind='$attrs' /></template>"
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/no-invalid-v-on.js b/tests/lib/rules/no-invalid-v-on.js
index 820a9509..06ba18b4 100644
--- a/tests/lib/rules/no-invalid-v-on.js
+++ b/tests/lib/rules/no-invalid-v-on.js
@@ -54,14 +54,13 @@ tester.run('no-invalid-v-on', rule, {
     {
       filename: 'test.vue',
       code: '<template><div v-on:click.native.stop></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-on="$listeners"></div></template>'
     }
   ],
   invalid: [
-    {
-      filename: 'test.vue',
-      code: '<template><div v-on="foo"></div></template>',
-      errors: ["'v-on' directives require event names."]
-    },
     {
       filename: 'test.vue',
       code: '<template><div v-on:click.aaa="foo"></div></template>',
diff --git a/tests/lib/rules/return-in-computed-property.js b/tests/lib/rules/return-in-computed-property.js
new file mode 100644
index 00000000..4ee565e4
--- /dev/null
+++ b/tests/lib/rules/return-in-computed-property.js
@@ -0,0 +1,196 @@
+/**
+ * @fileoverview Enforces that a return statement is present in computed property (return-in-computed-property)
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/return-in-computed-property')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('return-in-computed-property', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return true
+            },
+            bar: function () {
+              return false
+            },
+            bar3: {
+              set () {
+                return true
+              },
+              get () {
+                return true
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' }
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: {
+              get () {
+                return
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: false }]
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              if (a) {
+                return
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 6, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: {
+              set () {
+              },
+              get () {
+              }
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 7
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo: function () {
+              function bar () {
+                return this.baz * 2
+              }
+              bar()
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+            },
+            bar () {
+              return
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: false }],
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return
+            }
+          }
+        }
+      `,
+      parserOptions: { ecmaVersion: 8, sourceType: 'module' },
+      options: [{ treatUndefinedAsUnspecified: true }],
+      errors: [{
+        message: 'Expected to return a value in "foo" computed property.',
+        line: 4
+      }]
+    }
+  ]
+})