Repository: vuejs/eslint-plugin-vue
Pull request #153 (2017-08-13 21:37:05)

Add `mustache-curly-spacing` rule.
This PR implements rule proposed in #150.

armano2: @mysticatea suggestion requested in code review applied

Patch:
diff --git a/README.md b/README.md
index a6d61292..8fda2f0d 100644
--- a/README.md
+++ b/README.md
@@ -94,7 +94,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 |:---|:--------|:------------|
 |  | [no-dupe-keys](./docs/rules/no-dupe-keys.md) | Prevents duplication of field names. |
 | :white_check_mark: | [no-parsing-error](./docs/rules/no-parsing-error.md) | disallow parsing errors in `<template>`. |
-|  | [no-reservered-keys](./docs/rules/no-reservered-keys.md) | Prevent overwrite reserved keys. |
+|  | [no-reserved-keys](./docs/rules/no-reserved-keys.md) | Prevent overwrite reserved keys. |
 |  | [no-shared-component-data](./docs/rules/no-shared-component-data.md) | Enforces component's data property to be a function. |
 |  | [no-template-key](./docs/rules/no-template-key.md) | disallow `key` attribute on `<template>`. |
 |  | [require-render-return](./docs/rules/require-render-return.md) | Enforces render function to always return value. |
@@ -139,7 +139,9 @@ The `--fix` option on the command line automatically fixes problems reported by
 | :wrench: | [attribute-hyphenation](./docs/rules/attribute-hyphenation.md) | Define a style for the props casing in templates. |
 |  | [html-quotes](./docs/rules/html-quotes.md) | enforce quotes style of HTML attributes. |
 | :wrench: | [html-self-closing](./docs/rules/html-self-closing.md) | enforce self-closing style. |
+|  | [max-attributes-per-line](./docs/rules/max-attributes-per-line.md) | Define the number of attributes allows per line |
 | :wrench: | [name-property-casing](./docs/rules/name-property-casing.md) | Requires specific casing for the name property in Vue components |
+| :wrench: | [no-multi-spaces](./docs/rules/no-multi-spaces.md) | This rule warns about the usage of extra whitespaces between attributes |
 | :wrench: | [v-bind-style](./docs/rules/v-bind-style.md) | enforce `v-bind` directive style. |
 | :wrench: | [v-on-style](./docs/rules/v-on-style.md) | enforce `v-on` directive style. |
 
@@ -172,6 +174,7 @@ The `--fix` option on the command line automatically fixes problems reported by
 | [no-invalid-v-pre](./rules/no-invalid-v-pre.md) | [valid-v-pre](./rules/valid-v-pre.md) |
 | [no-invalid-v-show](./rules/no-invalid-v-show.md) | [valid-v-show](./rules/valid-v-show.md) |
 | [no-invalid-v-text](./rules/no-invalid-v-text.md) | [valid-v-text](./rules/valid-v-text.md) |
+| [no-reservered-keys](./rules/no-reservered-keys.md) | [no-reserved-keys](./rules/no-reserved-keys.md) |
 
 <!--RULES_TABLE_END-->
 
diff --git a/docs/rules/max-attributes-per-line.md b/docs/rules/max-attributes-per-line.md
new file mode 100644
index 00000000..8493465b
--- /dev/null
+++ b/docs/rules/max-attributes-per-line.md
@@ -0,0 +1,122 @@
+# Define the number of attributes allows per line (max-attributes-per-line)
+
+Limits the maximum number of attributes/properties per line to improve readability.
+
+
+## :book: Rule Details
+
+This rule aims to enforce a number of attributes per line in templates.
+It checks all the elements in a template and verifies that the number of attributes per line does not exceed the defined maximum.
+An attribute is considered to be in a new line when there is a line break between two attributes.
+
+There is a configurable number of attributes that are acceptable in one-line case (default 3), as well as how many attributes are acceptable per line in multi-line case (default 1).
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+<component lorem="1" ipsum="2" dolor="3" sit="4">
+</component>
+
+<component
+  lorem="1" ipsum="2"
+  dolor="3"
+  sit="4"
+>
+</component>
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+<component lorem="1" ipsum="2" dolor="3">
+</component>
+
+<component
+  lorem="1"
+  ipsum="2"
+  dolor="3"
+  sit="4"
+>
+</component>
+
+```
+
+### :wrench:  Options
+
+```
+{
+  "vue/max-attributes-per-line": [{
+    "singleline": 3,
+    "multiline": {
+      max: 1,
+      allowFirstLine: false
+    }
+  }]
+}
+```
+
+#### `allowFirstLine`
+For multi-line declarations, defines if allows attributes to be put in the first line. (Default false)
+
+:-1: Example of **incorrect** code for this setting:
+```html
+// [{ "multiline": { "allowFirstLine": false }}]
+<component foo="John" bar="Smith"
+  foobar={5555555}>
+</component>;
+```
+
+:+1: Example of **correct** code for this setting:
+```html
+// [{ "multiline": { "allowFirstLine": false }}]
+<component
+  foo="John"
+  bar="Smith"
+  foobar={5555555}
+  >
+</component>;
+```
+
+
+#### `singleline`
+Number of maximum attributes per line when the opening tag is in a single line. (Default is 3)
+
+:-1: Example of **incorrect** code for this setting:
+```html
+// [{"singleline": 2,}]
+<component foo="John" bar="Smith" foobar={5555555}></component>;
+```
+
+:+1: Example of **correct** code for this setting:
+```html
+// [{"singleline": 3,}]
+<component foo="John" bar="Smith" foobar={5555555}></component>;
+```
+
+
+#### `multiline`
+Number of maximum attributes per line when a tag is in multiple lines. (Default is 1)
+
+:-1: Example of **incorrect** code for this setting:
+```html
+// [{"multiline": 1}]
+<component foo="John" bar="Smith"
+  foobar={5555555}>
+</component>;
+```
+
+:+1: Example of **correct** code for this setting:
+```html
+// [{"multiline": 1}]
+<component
+  foo="John"
+  bar="Smith"
+  foobar={5555555}
+  >
+</component>;
+```
+
+## When Not To Use It
+
+If you do not want to check the number of attributes declared per line you can disable this rule.
+
diff --git a/docs/rules/mustache-interpolation-spacing.md b/docs/rules/mustache-interpolation-spacing.md
new file mode 100644
index 00000000..4844d15a
--- /dev/null
+++ b/docs/rules/mustache-interpolation-spacing.md
@@ -0,0 +1,67 @@
+# enforce unified spacing in mustache interpolations. (mustache-interpolation-spacing)
+
+- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule aims to enforce unified spacing in mustache interpolations.
+
+:-1: Examples of **incorrect** code for this rule:
+
+```html
+<template>
+  <div>{{   text   }}</div>
+</template>
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```html
+<template>
+  <div>{{ text }}</div>
+</template>
+```
+
+## :wrench: Options
+
+Default spacing is set to `always`
+
+```
+'vue/mustache-interpolation-spacing': [2, 'always'|'never']
+```
+
+### `"always"` - Expect one space between expression and curly brackets.
+
+:+1: Examples of **correct** code`:
+
+```html
+<template>
+  <div>{{ text }}</div>
+</template>
+```
+
+:-1: Examples of **incorrect** code`:
+
+```html
+<template>
+  <div>{{text}}</div>
+</template>
+```
+
+### `"never"` - Expect no spaces between expression and curly brackets.
+
+:+1: Examples of **correct** code`:
+
+```html
+<template>
+  <div>{{text}}</div>
+</template>
+```
+
+:-1: Examples of **incorrect** code`:
+
+```html
+<template>
+  <div>{{ text }}</div>
+</template>
+```
diff --git a/docs/rules/no-reserved-keys.md b/docs/rules/no-reserved-keys.md
new file mode 100644
index 00000000..baf54a6a
--- /dev/null
+++ b/docs/rules/no-reserved-keys.md
@@ -0,0 +1,58 @@
+# Prevent overwrite reserved keys (no-reserved-keys)
+
+This rule prevents to use reserved names from to avoid conflicts and unexpected behavior.
+
+## Rule Details
+
+:-1: Examples of **incorrect** code for this rule:
+
+```js
+export default {
+  props: {
+    $el: String
+  },
+  computed: {
+    $on: {
+      get () {
+      }
+    }
+  },
+  data: {
+    _foo: null
+  },
+  methods: {
+    $nextTick () {
+    }
+  }
+}
+```
+
+## :wrench: Options
+
+This rule has an object option:
+
+`"reserved"`: [] (default) array of dissalowed names inside `groups`.
+
+`"groups"`: [] (default) array of additional groups to search for duplicates.
+
+### Example:
+
+```
+vue/no-reserved-keys: [2, {
+  reserved: ['foo', 'foo2'],
+  groups: ['asyncComputed']
+}]
+```
+
+:-1: Examples of **incorrect** code for this configuration
+
+```js
+export default {
+  asyncComputed: {
+    foo2 () {}
+  },
+  computed: {
+    foo () {}
+  }
+}
+```
diff --git a/lib/recommended-rules.js b/lib/recommended-rules.js
index 02c13c1b..3d4f6048 100644
--- a/lib/recommended-rules.js
+++ b/lib/recommended-rules.js
@@ -10,6 +10,7 @@ module.exports = {
   "vue/html-quotes": "off",
   "vue/html-self-closing": "off",
   "vue/jsx-uses-vars": "error",
+  "vue/max-attributes-per-line": "off",
   "vue/name-property-casing": "off",
   "vue/no-async-in-computed-properties": "off",
   "vue/no-confusing-v-for-v-if": "error",
@@ -29,7 +30,9 @@ module.exports = {
   "vue/no-invalid-v-pre": "off",
   "vue/no-invalid-v-show": "off",
   "vue/no-invalid-v-text": "off",
+  "vue/no-multi-spaces": "off",
   "vue/no-parsing-error": "error",
+  "vue/no-reserved-keys": "off",
   "vue/no-reservered-keys": "off",
   "vue/no-shared-component-data": "off",
   "vue/no-side-effects-in-computed-properties": "off",
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
new file mode 100644
index 00000000..f5309e4a
--- /dev/null
+++ b/lib/rules/max-attributes-per-line.js
@@ -0,0 +1,161 @@
+/**
+ * @fileoverview Define the number of attributes allows per line
+ * @author Filipa Lacerda
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Define the number of attributes allows per line',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          singleline: {
+            anyOf: [
+              {
+                type: 'number',
+                minimum: 1
+              },
+              {
+                type: 'object',
+                properties: {
+                  max: {
+                    type: 'number',
+                    minimum: 1
+                  }
+                },
+                additionalProperties: false
+              }
+            ]
+          },
+          multiline: {
+            anyOf: [
+              {
+                type: 'number',
+                minimum: 1
+              },
+              {
+                type: 'object',
+                properties: {
+                  max: {
+                    type: 'number',
+                    minimum: 1
+                  },
+                  allowFirstLine: {
+                    type: 'boolean'
+                  }
+                },
+                additionalProperties: false
+              }
+            ]
+          }
+        }
+      }
+    ]
+  },
+
+  create: function (context) {
+    const configuration = parseOptions(context.options[0])
+    const multilineMaximum = configuration.multiline
+    const singlelinemMaximum = configuration.singleline
+    const canHaveFirstLine = configuration.allowFirstLine
+
+    utils.registerTemplateBodyVisitor(context, {
+      'VStartTag' (node) {
+        const numberOfAttributes = node.attributes.length
+
+        if (!numberOfAttributes) return
+
+        if (utils.isSingleLine(node) && numberOfAttributes > singlelinemMaximum) {
+          showErrors(node.attributes.slice(singlelinemMaximum), node)
+        }
+
+        if (!utils.isSingleLine(node)) {
+          if (!canHaveFirstLine && node.attributes[0].loc.start.line === node.loc.start.line) {
+            showErrors([node.attributes[0]], node)
+          }
+
+          groupAttrsByLine(node.attributes)
+            .filter(attrs => attrs.length > multilineMaximum)
+            .forEach(attrs => showErrors(attrs.splice(multilineMaximum), node))
+        }
+      }
+    })
+
+    // ----------------------------------------------------------------------
+    // Helpers
+    // ----------------------------------------------------------------------
+    function parseOptions (options) {
+      const defaults = {
+        singleline: 3,
+        multiline: 1,
+        allowFirstLine: false
+      }
+
+      if (options) {
+        if (typeof options.singleline === 'number') {
+          defaults.singleline = options.singleline
+        } else if (options.singleline && options.singleline.max) {
+          defaults.singleline = options.singleline.max
+        }
+
+        if (options.multiline) {
+          if (typeof options.multiline === 'number') {
+            defaults.multiline = options.multiline
+          } else if (typeof options.multiline === 'object') {
+            if (options.multiline.max) {
+              defaults.multiline = options.multiline.max
+            }
+
+            if (options.multiline.allowFirstLine) {
+              defaults.allowFirstLine = options.multiline.allowFirstLine
+            }
+          }
+        }
+      }
+
+      return defaults
+    }
+
+    function showErrors (attributes, node) {
+      attributes.forEach((prop) => {
+        context.report({
+          node: prop,
+          loc: prop.loc,
+          message: 'Attribute "{{propName}}" should be on a new line.',
+          data: {
+            propName: prop.key.name
+          }
+        })
+      })
+    }
+
+    function groupAttrsByLine (attributes) {
+      const propsPerLine = [[attributes[0]]]
+
+      attributes.reduce((previous, current) => {
+        if (previous.loc.end.line === current.loc.start.line) {
+          propsPerLine[propsPerLine.length - 1].push(current)
+        } else {
+          propsPerLine.push([current])
+        }
+        return current
+      })
+
+      return propsPerLine
+    }
+
+    return {}
+  }
+}
diff --git a/lib/rules/mustache-interpolation-spacing.js b/lib/rules/mustache-interpolation-spacing.js
new file mode 100644
index 00000000..4ea93e96
--- /dev/null
+++ b/lib/rules/mustache-interpolation-spacing.js
@@ -0,0 +1,91 @@
+/**
+ * @fileoverview enforce unified spacing in mustache interpolations.
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'enforce unified spacing in mustache interpolations.',
+      category: 'Stylistic Issues',
+      recommended: false
+    },
+    fixable: 'whitespace',
+    schema: [
+      {
+        enum: ['always', 'never']
+      }
+    ]
+  },
+
+  create (context) {
+    const options = context.options[0]
+    const optSpaces = options !== 'never'
+    const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
+
+    // ----------------------------------------------------------------------
+    // Helpers
+    // ----------------------------------------------------------------------
+
+    function checkTokens (leftToken, rightToken) {
+      if (leftToken.loc.end.line === rightToken.loc.start.line) {
+        const spaces = rightToken.loc.start.column - leftToken.loc.end.column
+        const noSpacesFound = spaces === 0
+
+        if (optSpaces === noSpacesFound) {
+          context.report({
+            node: rightToken,
+            loc: {
+              start: leftToken.loc.end,
+              end: rightToken.loc.start
+            },
+            message: 'Found {{spaces}} whitespaces, {{type}} expected.',
+            data: {
+              spaces: spaces === 0 ? 'none' : spaces,
+              type: optSpaces ? '1' : 'none'
+            },
+            fix: (fixer) => fixer.replaceTextRange([leftToken.range[1], rightToken.range[0]], optSpaces ? ' ' : '')
+          })
+        }
+      }
+    }
+
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    utils.registerTemplateBodyVisitor(context, {
+      'VExpressionContainer[expression!=null]' (node) {
+        const tokens = template.getTokens(node, {
+          includeComments: true,
+          filter: token => token.type !== 'HTMLWhitespace' // When there is only whitespace between ignore it
+        })
+
+        const startToken = tokens.shift()
+        if (!startToken || startToken.type !== 'VExpressionStart') return
+        const endToken = tokens.pop()
+        if (!endToken || endToken.type !== 'VExpressionEnd') return
+
+        if (tokens.length > 0) {
+          checkTokens(startToken, tokens[0])
+          checkTokens(tokens[tokens.length - 1], endToken)
+        } else {
+          checkTokens(startToken, endToken)
+        }
+      }
+    })
+
+    return { }
+  }
+}
diff --git a/lib/rules/no-reserved-keys.js b/lib/rules/no-reserved-keys.js
new file mode 100644
index 00000000..bc4a8900
--- /dev/null
+++ b/lib/rules/no-reserved-keys.js
@@ -0,0 +1,74 @@
+/**
+ * @fileoverview Prevent overwrite reserved keys
+ * @author Armano
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const RESERVED_KEYS = require('../utils/vue-reserved.json')
+const GROUP_NAMES = ['props', 'computed', 'data', 'methods']
+
+function create (context) {
+  const options = context.options[0] || {}
+  const reservedKeys = new Set(RESERVED_KEYS.concat(options.reserved || []))
+  const groups = new Set(GROUP_NAMES.concat(options.groups || []))
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  return utils.executeOnVue(context, (obj) => {
+    const properties = utils.iterateProperties(obj, groups)
+    for (const o of properties) {
+      if (o.groupName === 'data' && o.name[0] === '_') {
+        context.report({
+          node: o.node,
+          message: "Keys starting with with '_' are reserved in '{{name}}' group.",
+          data: {
+            name: o.name
+          }
+        })
+      } else if (reservedKeys.has(o.name)) {
+        context.report({
+          node: o.node,
+          message: "Key '{{name}}' is reserved.",
+          data: {
+            name: o.name
+          }
+        })
+      }
+    }
+  })
+}
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'Prevent overwrite reserved keys.',
+      category: 'Possible Errors',
+      recommended: false
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          reserved: {
+            type: 'array'
+          },
+          groups: {
+            type: 'array'
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+
+  create
+}
diff --git a/lib/rules/no-reservered-keys.js b/lib/rules/no-reservered-keys.js
index c0ba613f..e31fe710 100644
--- a/lib/rules/no-reservered-keys.js
+++ b/lib/rules/no-reservered-keys.js
@@ -51,9 +51,11 @@ module.exports = {
     docs: {
       description: 'Prevent overwrite reserved keys.',
       category: 'Possible Errors',
-      recommended: false
+      recommended: false,
+      replacedBy: ['no-reserved-keys']
     },
-    fixable: null,  // or "code" or "whitespace"
+    deprecated: true,
+    fixable: null,
     schema: [
       {
         type: 'object',
diff --git a/lib/utils/index.js b/lib/utils/index.js
index e40c0935..08c8c34c 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -382,7 +382,7 @@ module.exports = {
       callee.object.type === 'Identifier' &&
       callee.object.name === 'Vue' &&
       callee.property.type === 'Identifier' &&
-      callee.property.name === 'component' &&
+      (callee.property.name === 'component' || callee.property.name === 'mixin') &&
       node.arguments.length &&
       node.arguments.slice(-1)[0].type === 'ObjectExpression'
 
@@ -590,5 +590,14 @@ module.exports = {
         }
       }
     }
+  },
+
+  /**
+   * Check whether the component is declared in a single line or not.
+   * @param {ASTNode} node
+   * @returns {boolean}
+   */
+  isSingleLine (node) {
+    return node.loc.start.line === node.loc.end.line
   }
 }
diff --git a/package.json b/package.json
index 12917f31..b12c22b5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "3.11.0",
+  "version": "3.12.0",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {


Test patch:
diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js
new file mode 100644
index 00000000..07d24197
--- /dev/null
+++ b/tests/lib/rules/max-attributes-per-line.js
@@ -0,0 +1,191 @@
+/**
+ * @fileoverview Define the number of attributes allows per line
+ * @author Filipa Lacerda
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/max-attributes-per-line')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('max-attributes-per-line', rule, {
+  valid: [
+    {
+      code: `<template><component></component></template>`
+    },
+    {
+      code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`
+    },
+    {
+      code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
+      options: [{ multiline: { allowFirstLine: true }}]
+    },
+    {
+      code: `<template><component
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`
+    },
+    {
+      code: `<template><component
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`,
+      options: [{ singleline: 1 }]
+    },
+    {
+      code: `<template><component></component></template>`,
+      options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}]
+    },
+    {
+      code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
+      options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}]
+    },
+    {
+      code: `<template><component name="John Doe"
+        age="30">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: true }}]
+    },
+    {
+      code: `<template><component
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}]
+    },
+    {
+      code: `<template><component
+        name="John Doe" age="30"
+        job="Vet">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}]
+    }
+  ],
+
+  invalid: [
+    {
+      code: `<template><component name="John Doe" age="30" job="Vet" petname="Snoopy"></component></template>`,
+      errors: ['Attribute "petname" should be on a new line.']
+    },
+    {
+      code: `<template><component job="Vet"
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`,
+      errors: [{
+        message: 'Attribute "job" should be on a new line.',
+        type: 'VAttribute',
+        line: 1
+      }]
+    },
+    {
+      code: `<template><component name="John Doe" age="30"></component></template>`,
+      options: [{ singleline: { max: 1 }}],
+      errors: [{
+        message: 'Attribute "age" should be on a new line.',
+        type: 'VAttribute',
+        line: 1
+      }]
+    },
+    {
+      code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
+      options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}],
+      errors: [{
+        message: 'Attribute "age" should be on a new line.',
+        type: 'VAttribute',
+        line: 1
+      }, {
+        message: 'Attribute "job" should be on a new line.',
+        type: 'VAttribute',
+        line: 1
+      }]
+    },
+    {
+      code: `<template><component name="John Doe"
+        age="30">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      errors: [{
+        message: 'Attribute "name" should be on a new line.',
+        type: 'VAttribute',
+        line: 1
+      }]
+    },
+    {
+      code: `<template><component
+        name="John Doe" age="30"
+        job="Vet">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      errors: [{
+        message: 'Attribute "age" should be on a new line.',
+        type: 'VAttribute',
+        line: 2
+      }]
+    },
+    {
+      code: `<template><component
+        name="John Doe" age="30"
+        job="Vet">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: 1 }],
+      errors: [{
+        message: 'Attribute "age" should be on a new line.',
+        type: 'VAttribute',
+        line: 2
+      }]
+    },
+    {
+      code: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" petname="Snoopy">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      errors: [{
+        message: 'Attribute "petname" should be on a new line.',
+        type: 'VAttribute',
+        line: 3
+      }]
+    },
+    {
+      code: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" petname="Snoopy" extra="foo">
+        </component>
+      </template>`,
+      options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      errors: [{
+        message: 'Attribute "petname" should be on a new line.',
+        type: 'VAttribute',
+        line: 3
+      }, {
+        message: 'Attribute "extra" should be on a new line.',
+        type: 'VAttribute',
+        line: 3
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/mustache-interpolation-spacing.js b/tests/lib/rules/mustache-interpolation-spacing.js
new file mode 100644
index 00000000..84c8ef97
--- /dev/null
+++ b/tests/lib/rules/mustache-interpolation-spacing.js
@@ -0,0 +1,168 @@
+/**
+ * @fileoverview enforce unified spacing in mustache interpolations.
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/mustache-interpolation-spacing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: 'vue-eslint-parser',
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+ruleTester.run('mustache-interpolation-spacing', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: '<template></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template>             <div id="               "></div>         </template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template> <div :style="  " :class="       foo      " v-if=foo   ></div>      </template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ text }}</div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ }}</div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ }}</div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{}}</div></template>',
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{text}}</div></template>',
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ text }}</div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{         }}</div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{         }}</div></template>',
+      options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{   text   }}</div></template>',
+      options: ['always']
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ text}}</div></template>',
+      output: '<template><div>{{ text }}</div></template>',
+      options: ['always'],
+      errors: [{
+        message: 'Found none whitespaces, 1 expected.',
+        type: 'VExpressionEnd'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{text }}</div></template>',
+      output: '<template><div>{{ text }}</div></template>',
+      options: ['always'],
+      errors: [{
+        message: 'Found none whitespaces, 1 expected.',
+        type: 'Identifier'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ text}}</div></template>',
+      output: '<template><div>{{text}}</div></template>',
+      options: ['never'],
+      errors: [{
+        message: 'Found 1 whitespaces, none expected.',
+        type: 'Identifier'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{text }}</div></template>',
+      output: '<template><div>{{text}}</div></template>',
+      options: ['never'],
+      errors: [{
+        message: 'Found 1 whitespaces, none expected.',
+        type: 'VExpressionEnd'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{text}}</div></template>',
+      output: '<template><div>{{ text }}</div></template>',
+      options: ['always'],
+      errors: [{
+        message: 'Found none whitespaces, 1 expected.',
+        type: 'Identifier'
+      }, {
+        message: 'Found none whitespaces, 1 expected.',
+        type: 'VExpressionEnd'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{ text }}</div></template>',
+      output: '<template><div>{{text}}</div></template>',
+      options: ['never'],
+      errors: [{
+        message: 'Found 1 whitespaces, none expected.',
+        type: 'Identifier'
+      }, {
+        message: 'Found 1 whitespaces, none expected.',
+        type: 'VExpressionEnd'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div>{{   text   }}</div></template>',
+      output: '<template><div>{{text}}</div></template>',
+      options: ['never'],
+      errors: [{
+        message: 'Found 3 whitespaces, none expected.',
+        type: 'Identifier'
+      }, {
+        message: 'Found 3 whitespaces, none expected.',
+        type: 'VExpressionEnd'
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-reserved-keys.js b/tests/lib/rules/no-reserved-keys.js
new file mode 100644
index 00000000..cc116f1d
--- /dev/null
+++ b/tests/lib/rules/no-reserved-keys.js
@@ -0,0 +1,100 @@
+/**
+ * @fileoverview Prevent overwrite reserved keys
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-reserved-keys')
+const RuleTester = require('eslint').RuleTester
+
+const parserOptions = {
+  ecmaVersion: 7,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester()
+ruleTester.run('no-reserved-keys', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['foo'],
+          computed: {
+            bar () {
+            }
+          },
+          data () {
+            return {
+              dat: null
+            }
+          },
+          methods: {
+            _foo () {},
+            test () {
+            }
+          }
+        }
+      `,
+      parserOptions
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          props: {
+            $el: String
+          }
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: "Key '$el' is reserved.",
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          data: {
+            _foo: String
+          }
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: "Keys starting with with '_' are reserved in '_foo' group.",
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          foo: {
+            bar: String
+          }
+        })
+      `,
+      options: [{ reserved: ['bar'], groups: ['foo'] }],
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: "Key 'bar' is reserved.",
+        line: 4
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js
index 38b69633..0dd2d0cf 100644
--- a/tests/lib/utils/vue-component.js
+++ b/tests/lib/utils/vue-component.js
@@ -120,6 +120,12 @@ function invalidTests (ext) {
       parserOptions,
       errors: [makeError(1)]
     },
+    {
+      filename: `test.${ext}`,
+      code: `Vue.mixin({})`,
+      parserOptions,
+      errors: [makeError(1)]
+    },
     {
       filename: `test.${ext}`,
       code: `