Repository: vuejs/eslint-plugin-vue
Pull request #289 (2017-12-10 19:18:01)

[New] Add `prop-name-casing`
This PR implements rule proposed in #252

michalsnik: Thanks for your work @bfkbx960 ! We really appreciate it :) We're going to merge new rules once stable v4 is out 🚀 
bfkbx960: @michalsnik suggestions applied, thank you for code review.
In my local environment the test will succeed but it seems to fail here :(
michalsnik: Thanks @bfkbx960! If you remove `node_modules`, set node to version 4, install all dependencies once again and run `npm test` on node 4 - it's failing. It's working on master though.
michalsnik: or it's not working even on master now o.O which is soo weird, as yeasterday I released 4.1.0 and everything passed

Patch:
diff --git a/README.md b/README.md
index d6b4d738..26d89faf 100644
--- a/README.md
+++ b/README.md
@@ -48,6 +48,8 @@ module.exports = {
 All component-related rules are being applied to code that passes any of the following checks:
 
 * `Vue.component()` expression
+* `Vue.extend()` expression
+* `Vue.mixin()` expression
 * `export default {}` in `.vue` or `.jsx` file
 
 If you however want to take advantage of our rules in any of your custom objects that are Vue components, you might need to use special comment `// @vue/component` that marks object in the next line as a Vue component in any file, e.g.:
diff --git a/docs/rules/no-multi-spaces.md b/docs/rules/no-multi-spaces.md
index 196bee56..d68fe02b 100644
--- a/docs/rules/no-multi-spaces.md
+++ b/docs/rules/no-multi-spaces.md
@@ -5,7 +5,7 @@
 
 The `--fix` option on the command line can automatically fix some of the problems reported by this rule.
 
-This rule aims to remove multiple spaces in a row between attributes witch are not used for indentation.
+This rule aims to remove multiple spaces in a row between attributes which are not used for indentation.
 
 ## Rule Details
 
diff --git a/docs/rules/prop-name-casing.md b/docs/rules/prop-name-casing.md
new file mode 100644
index 00000000..fcd8207f
--- /dev/null
+++ b/docs/rules/prop-name-casing.md
@@ -0,0 +1,35 @@
+# enforce specific casing for the Prop name in Vue components(prop-name-casing)
+
+This rule would enforce proper casing of props in vue components(camelCase).
+
+## :book: Rule Details
+
+(https://vuejs.org/v2/style-guide/#Prop-name-casing-strongly-recommended).
+
+:+1: Examples of **correct** code for `camelCase`:
+
+```js
+export default {
+  props: {
+    greetingText: String
+  }
+}
+```
+
+:-1: Examples of **incorrect** code for `camelCase`:
+
+```js
+export default {
+  props: {
+    'greeting-text': String
+  }
+}
+```
+
+## :wrench: Options
+
+Default casing is set to `camelCase`.
+
+```
+"vue/prop-name-casing": ["error", "camelCase|snake_case"]
+```
diff --git a/lib/rules/max-attributes-per-line.js b/lib/rules/max-attributes-per-line.js
index 9a54fd1e..0d05f0be 100644
--- a/lib/rules/max-attributes-per-line.js
+++ b/lib/rules/max-attributes-per-line.js
@@ -16,7 +16,7 @@ module.exports = {
       category: 'strongly-recommended',
       url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/max-attributes-per-line.md'
     },
-    fixable: null,
+    fixable: 'whitespace', // or "code" or "whitespace"
     schema: [
       {
         type: 'object',
@@ -129,14 +129,15 @@ module.exports = {
     }
 
     function showErrors (attributes, node) {
-      attributes.forEach((prop) => {
+      attributes.forEach((prop, i) => {
         context.report({
           node: prop,
           loc: prop.loc,
           message: 'Attribute "{{propName}}" should be on a new line.',
           data: {
             propName: prop.key.name
-          }
+          },
+          fix: i === 0 ? (fixer) => fixer.insertTextBefore(prop, '\n') : undefined
         })
       })
     }
diff --git a/lib/rules/order-in-components.js b/lib/rules/order-in-components.js
index 6bd0fb22..3d9a21af 100644
--- a/lib/rules/order-in-components.js
+++ b/lib/rules/order-in-components.js
@@ -5,6 +5,7 @@
 'use strict'
 
 const utils = require('../utils')
+const Traverser = require('eslint/lib/util/traverser')
 
 const defaultOrder = [
   'el',
@@ -56,6 +57,75 @@ function getOrderMap (order) {
   return orderMap
 }
 
+function isComma (node) {
+  return node.type === 'Punctuator' && node.value === ','
+}
+
+const ARITHMETIC_OPERATORS = ['+', '-', '*', '/', '%', '**']
+const BITWISE_OPERATORS = ['&', '|', '^', '~', '<<', '>>', '>>>']
+const COMPARISON_OPERATORS = ['==', '!=', '===', '!==', '>', '>=', '<', '<=']
+const RELATIONAL_OPERATORS = ['in', 'instanceof']
+const ALL_BINARY_OPERATORS = [].concat(
+  ARITHMETIC_OPERATORS,
+  BITWISE_OPERATORS,
+  COMPARISON_OPERATORS,
+  RELATIONAL_OPERATORS
+)
+const LOGICAL_OPERATORS = ['&&', '||']
+
+/*
+ * Result `true` if the node is sure that there are no side effects
+ *
+ * Currently known side effects types
+ *
+ * node.type === 'CallExpression'
+ * node.type === 'NewExpression'
+ * node.type === 'UpdateExpression'
+ * node.type === 'AssignmentExpression'
+ * node.type === 'TaggedTemplateExpression'
+ * node.type === 'UnaryExpression' && node.operator === 'delete'
+ *
+ * @param  {ASTNode} node target node
+ * @param  {Object} visitorKeys sourceCode.visitorKey
+ * @returns {Boolean} no side effects
+ */
+function isNotSideEffectsNode (node, visitorKeys) {
+  let result = true
+  new Traverser().traverse(node, {
+    visitorKeys,
+    enter (node, parent) {
+      if (
+        node.type === 'FunctionExpression' ||
+        node.type === 'Identifier' ||
+        node.type === 'Literal' ||
+        // es2015
+        node.type === 'ArrowFunctionExpression' ||
+        node.type === 'TemplateElement'
+      ) {
+        // no side effects node
+        this.skip()
+      } else if (
+        node.type !== 'Property' &&
+        node.type !== 'ObjectExpression' &&
+        node.type !== 'ArrayExpression' &&
+        (node.type !== 'UnaryExpression' || ['!', '~', '+', '-', 'typeof'].indexOf(node.operator) < 0) &&
+        (node.type !== 'BinaryExpression' || ALL_BINARY_OPERATORS.indexOf(node.operator) < 0) &&
+        (node.type !== 'LogicalExpression' || LOGICAL_OPERATORS.indexOf(node.operator) < 0) &&
+        node.type !== 'MemberExpression' &&
+        node.type !== 'ConditionalExpression' &&
+        // es2015
+        node.type !== 'SpreadElement' &&
+        node.type !== 'TemplateLiteral'
+      ) {
+        // Can not be sure that a node has no side effects
+        result = false
+        this.break()
+      }
+    }
+  })
+  return result
+}
+
 // ------------------------------------------------------------------------------
 // Rule Definition
 // ------------------------------------------------------------------------------
@@ -67,7 +137,7 @@ module.exports = {
       category: 'recommended',
       url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v4.2.2/docs/rules/order-in-components.md'
     },
-    fixable: null,
+    fixable: 'code', // null or "code" or "whitespace"
     schema: [
       {
         type: 'object',
@@ -86,6 +156,7 @@ module.exports = {
     const order = options.order || defaultOrder
     const extendedOrder = order.map(property => groups[property] || property)
     const orderMap = getOrderMap(extendedOrder)
+    const sourceCode = context.getSourceCode()
 
     function checkOrder (propertiesNodes, orderMap) {
       const properties = propertiesNodes
@@ -109,6 +180,35 @@ module.exports = {
               name: property.name,
               firstUnorderedPropertyName: firstUnorderedProperty.name,
               line
+            },
+            fix (fixer) {
+              const propertyNode = property.parent
+              const firstUnorderedPropertyNode = firstUnorderedProperty.parent
+              const hasSideEffectsPossibility = propertiesNodes
+                .slice(
+                  propertiesNodes.indexOf(firstUnorderedPropertyNode),
+                  propertiesNodes.indexOf(propertyNode) + 1
+                )
+                .some((property) => !isNotSideEffectsNode(property, sourceCode.visitorKeys))
+              if (hasSideEffectsPossibility) {
+                return undefined
+              }
+              const comma = sourceCode.getTokenAfter(propertyNode)
+              const hasAfterComma = isComma(comma)
+
+              const codeStart = sourceCode.getTokenBefore(propertyNode).range[1] // to include comments
+              const codeEnd = hasAfterComma ? comma.range[1] : propertyNode.range[1]
+
+              const propertyCode = sourceCode.text.slice(codeStart, codeEnd) + (hasAfterComma ? '' : ',')
+              const insertTarget = sourceCode.getTokenBefore(firstUnorderedPropertyNode)
+              // If we can upgrade requirements to `eslint@>4.1.0`, this code can be replaced by:
+              // return [
+              //   fixer.removeRange([codeStart, codeEnd]),
+              //   fixer.insertTextAfter(insertTarget, propertyCode)
+              // ]
+              const insertStart = insertTarget.range[1]
+              const newCode = propertyCode + sourceCode.text.slice(insertStart, codeStart)
+              return fixer.replaceTextRange([insertStart, codeEnd], newCode)
             }
           })
         }
diff --git a/lib/rules/prop-name-casing.js b/lib/rules/prop-name-casing.js
new file mode 100644
index 00000000..02070af2
--- /dev/null
+++ b/lib/rules/prop-name-casing.js
@@ -0,0 +1,75 @@
+/**
+ * @fileoverview Requires specific casing for the Prop name in Vue components
+ * @author Yu Kimura
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+const allowedCaseOptions = ['camelCase', 'snake_case']
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+function create (context) {
+  const options = context.options[0]
+  const caseType = allowedCaseOptions.indexOf(options) !== -1 ? options : 'camelCase'
+  const converter = casing.getConverter(caseType)
+
+  // ----------------------------------------------------------------------
+  // Public
+  // ----------------------------------------------------------------------
+
+  return utils.executeOnVue(context, (obj) => {
+    const node = obj.properties.find(p =>
+      p.type === 'Property' &&
+      p.key.type === 'Identifier' &&
+      p.key.name === 'props' &&
+      (p.value.type === 'ObjectExpression' || p.value.type === 'ArrayExpression')
+    )
+
+    if (!node) return
+
+    const items = node.value.type === 'ObjectExpression' ? node.value.properties : node.value.elements
+    for (const item of items) {
+      if (item.type !== 'Property') {
+        return
+      }
+
+      const propName = item.key.type === 'Literal' ? item.key.value : item.key.name
+      const convertedName = converter(propName)
+      if (convertedName !== propName) {
+        context.report({
+          node: item,
+          message: 'Prop "{{name}}" is not in {{caseType}}.',
+          data: {
+            name: propName,
+            caseType: caseType
+          }
+        })
+      }
+    }
+  })
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'enforce specific casing for the Prop name in Vue components',
+      category: undefined // 'strongly-recommended'
+    },
+    fixable: null,  // or "code" or "whitespace"
+    schema: [
+      {
+        enum: allowedCaseOptions
+      }
+    ]
+  },
+
+  create
+}
diff --git a/lib/rules/valid-v-on.js b/lib/rules/valid-v-on.js
index 498fd54f..f84412bb 100644
--- a/lib/rules/valid-v-on.js
+++ b/lib/rules/valid-v-on.js
@@ -23,6 +23,88 @@ const VALID_MODIFIERS = new Set([
 const VERB_MODIFIERS = new Set([
   'stop', 'prevent'
 ])
+// https://www.w3.org/TR/uievents-key/
+const KEY_ALIASES = new Set([
+  'unidentified', 'alt', 'alt-graph', 'caps-lock', 'control', 'fn', 'fn-lock',
+  'meta', 'num-lock', 'scroll-lock', 'shift', 'symbol', 'symbol-lock', 'hyper',
+  'super', 'enter', 'tab', 'arrow-down', 'arrow-left', 'arrow-right',
+  'arrow-up', 'end', 'home', 'page-down', 'page-up', 'backspace', 'clear',
+  'copy', 'cr-sel', 'cut', 'delete', 'erase-eof', 'ex-sel', 'insert', 'paste',
+  'redo', 'undo', 'accept', 'again', 'attn', 'cancel', 'context-menu', 'escape',
+  'execute', 'find', 'help', 'pause', 'select', 'zoom-in', 'zoom-out',
+  'brightness-down', 'brightness-up', 'eject', 'log-off', 'power',
+  'print-screen', 'hibernate', 'standby', 'wake-up', 'all-candidates',
+  'alphanumeric', 'code-input', 'compose', 'convert', 'dead', 'final-mode',
+  'group-first', 'group-last', 'group-next', 'group-previous', 'mode-change',
+  'next-candidate', 'non-convert', 'previous-candidate', 'process',
+  'single-candidate', 'hangul-mode', 'hanja-mode', 'junja-mode', 'eisu',
+  'hankaku', 'hiragana', 'hiragana-katakana', 'kana-mode', 'kanji-mode',
+  'katakana', 'romaji', 'zenkaku', 'zenkaku-hankaku', 'f1', 'f2', 'f3', 'f4',
+  'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'soft1', 'soft2', 'soft3',
+  'soft4', 'channel-down', 'channel-up', 'close', 'mail-forward', 'mail-reply',
+  'mail-send', 'media-close', 'media-fast-forward', 'media-pause',
+  'media-play-pause', 'media-record', 'media-rewind', 'media-stop',
+  'media-track-next', 'media-track-previous', 'new', 'open', 'print', 'save',
+  'spell-check', 'key11', 'key12', 'audio-balance-left', 'audio-balance-right',
+  'audio-bass-boost-down', 'audio-bass-boost-toggle', 'audio-bass-boost-up',
+  'audio-fader-front', 'audio-fader-rear', 'audio-surround-mode-next',
+  'audio-treble-down', 'audio-treble-up', 'audio-volume-down',
+  'audio-volume-up', 'audio-volume-mute', 'microphone-toggle',
+  'microphone-volume-down', 'microphone-volume-up', 'microphone-volume-mute',
+  'speech-correction-list', 'speech-input-toggle', 'launch-application1',
+  'launch-application2', 'launch-calendar', 'launch-contacts', 'launch-mail',
+  'launch-media-player', 'launch-music-player', 'launch-phone',
+  'launch-screen-saver', 'launch-spreadsheet', 'launch-web-browser',
+  'launch-web-cam', 'launch-word-processor', 'browser-back',
+  'browser-favorites', 'browser-forward', 'browser-home', 'browser-refresh',
+  'browser-search', 'browser-stop', 'app-switch', 'call', 'camera',
+  'camera-focus', 'end-call', 'go-back', 'go-home', 'headset-hook',
+  'last-number-redial', 'notification', 'manner-mode', 'voice-dial', 't-v',
+  't-v3-d-mode', 't-v-antenna-cable', 't-v-audio-description',
+  't-v-audio-description-mix-down', 't-v-audio-description-mix-up',
+  't-v-contents-menu', 't-v-data-service', 't-v-input', 't-v-input-component1',
+  't-v-input-component2', 't-v-input-composite1', 't-v-input-composite2',
+  't-v-input-h-d-m-i1', 't-v-input-h-d-m-i2', 't-v-input-h-d-m-i3',
+  't-v-input-h-d-m-i4', 't-v-input-v-g-a1', 't-v-media-context', 't-v-network',
+  't-v-number-entry', 't-v-power', 't-v-radio-service', 't-v-satellite',
+  't-v-satellite-b-s', 't-v-satellite-c-s', 't-v-satellite-toggle',
+  't-v-terrestrial-analog', 't-v-terrestrial-digital', 't-v-timer',
+  'a-v-r-input', 'a-v-r-power', 'color-f0-red', 'color-f1-green',
+  'color-f2-yellow', 'color-f3-blue', 'color-f4-grey', 'color-f5-brown',
+  'closed-caption-toggle', 'dimmer', 'display-swap', 'd-v-r', 'exit',
+  'favorite-clear0', 'favorite-clear1', 'favorite-clear2', 'favorite-clear3',
+  'favorite-recall0', 'favorite-recall1', 'favorite-recall2',
+  'favorite-recall3', 'favorite-store0', 'favorite-store1', 'favorite-store2',
+  'favorite-store3', 'guide', 'guide-next-day', 'guide-previous-day', 'info',
+  'instant-replay', 'link', 'list-program', 'live-content', 'lock',
+  'media-apps', 'media-last', 'media-skip-backward', 'media-skip-forward',
+  'media-step-backward', 'media-step-forward', 'media-top-menu', 'navigate-in',
+  'navigate-next', 'navigate-out', 'navigate-previous', 'next-favorite-channel',
+  'next-user-profile', 'on-demand', 'pairing', 'pin-p-down', 'pin-p-move',
+  'pin-p-toggle', 'pin-p-up', 'play-speed-down', 'play-speed-reset',
+  'play-speed-up', 'random-toggle', 'rc-low-battery', 'record-speed-next',
+  'rf-bypass', 'scan-channels-toggle', 'screen-mode-next', 'settings',
+  'split-screen-toggle', 's-t-b-input', 's-t-b-power', 'subtitle', 'teletext',
+  'video-mode-next', 'wink', 'zoom-toggle', 'audio-volume-down',
+  'audio-volume-up', 'audio-volume-mute', 'browser-back', 'browser-forward',
+  'channel-down', 'channel-up', 'context-menu', 'eject', 'end', 'enter', 'home',
+  'media-fast-forward', 'media-play', 'media-play-pause', 'media-record',
+  'media-rewind', 'media-stop', 'media-next-track', 'media-pause',
+  'media-previous-track', 'power', 'unidentified'
+])
+
+function isValidModifier (modifier) {
+  return (
+    // built-in aliases
+    VALID_MODIFIERS.has(modifier) ||
+    // keyCode
+    Number.isInteger(parseInt(modifier, 10)) ||
+    // keyAlias (an Unicode character)
+    Array.from(modifier).length === 1 ||
+    // keyAlias (special keys)
+    KEY_ALIASES.has(modifier)
+  )
+}
 
 // ------------------------------------------------------------------------------
 // Rule Definition
@@ -43,7 +125,7 @@ module.exports = {
     return utils.defineTemplateBodyVisitor(context, {
       "VAttribute[directive=true][key.name='on']" (node) {
         for (const modifier of node.key.modifiers) {
-          if (!VALID_MODIFIERS.has(modifier) && !Number.isInteger(parseInt(modifier, 10))) {
+          if (!isValidModifier(modifier)) {
             context.report({
               node,
               loc: node.loc,
diff --git a/lib/utils/casing.js b/lib/utils/casing.js
index 2baca235..db73a5a2 100644
--- a/lib/utils/casing.js
+++ b/lib/utils/casing.js
@@ -14,6 +14,18 @@ function kebabCase (str) {
     .toLowerCase()
 }
 
+/**
+ * Convert text to snake_case
+ * @param {string} str Text to be converted
+ * @return {string}
+ */
+function snakeCase (str) {
+  return str
+    .replace(/([a-z])([A-Z])/g, match => match[0] + '_' + match[1])
+    .replace(invalidChars, '_')
+    .toLowerCase()
+}
+
 /**
  * Convert text to camelCase
  * @param {string} str Text to be converted
@@ -42,6 +54,7 @@ function pascalCase (str) {
 
 const convertersMap = {
   'kebab-case': kebabCase,
+  'snake_case': snakeCase,
   'camelCase': camelCase,
   'PascalCase': pascalCase
 }
diff --git a/lib/utils/indent-common.js b/lib/utils/indent-common.js
index c8719a98..dfc89d57 100644
--- a/lib/utils/indent-common.js
+++ b/lib/utils/indent-common.js
@@ -18,6 +18,7 @@ const KNOWN_NODES = new Set(['ArrayExpression', 'ArrayPattern', 'ArrowFunctionEx
 const LT_CHAR = /[\r\n\u2028\u2029]/
 const LINES = /[^\r\n\u2028\u2029]+(?:$|\r\n|[\r\n\u2028\u2029])/g
 const BLOCK_COMMENT_PREFIX = /^\s*\*/
+const TRIVIAL_PUNCTUATOR = /^[(){}[\],;]$/
 
 /**
  * Normalize options.
@@ -244,6 +245,21 @@ function isClosingToken (token) {
   )
 }
 
+/**
+ * Check whether a given token is trivial or not.
+ * @param {Token} token The token to check.
+ * @returns {boolean} `true` if the token is trivial.
+ */
+function isTrivialToken (token) {
+  return token != null && (
+    (token.type === 'Punctuator' && TRIVIAL_PUNCTUATOR.test(token.value)) ||
+    token.type === 'HTMLTagOpen' ||
+    token.type === 'HTMLEndTagOpen' ||
+    token.type === 'HTMLTagClose' ||
+    token.type === 'HTMLSelfClosingTagClose'
+  )
+}
+
 /**
  * Creates AST event handlers for html-indent.
  *
@@ -262,6 +278,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
    * @param {Token|Token[]} token The token to set.
    * @param {number} offset The offset of the tokens.
    * @param {Token} baseToken The token of the base offset.
+   * @param {boolean} [trivial=false] The flag for trivial tokens.
    * @returns {void}
    */
   function setOffset (token, offset, baseToken) {
@@ -352,9 +369,7 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
         if (lastToken != null) {
           t = lastToken
           while ((t = tokenStore.getTokenAfter(t)) != null && t.range[1] <= elementTokens.firstToken.range[0]) {
-            if (lastToken.loc.end.line !== t.loc.start.line) {
-              alignTokens.push(t)
-            }
+            alignTokens.push(t)
           }
         }
         alignTokens.push(elementTokens.firstToken)
@@ -550,13 +565,15 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
    * @returns {number} Correct indentation. If it failed to calculate then `Number.MAX_SAFE_INTEGER`.
    */
   function getExpectedIndent (tokens) {
+    const trivial = isTrivialToken(tokens[0])
     let expectedIndent = Number.MAX_SAFE_INTEGER
 
     for (let i = 0; i < tokens.length; ++i) {
       const token = tokens[i]
       const offsetInfo = offsets.get(token)
 
-      if (offsetInfo != null) {
+      // If the first token is not trivial then ignore trivial following tokens.
+      if (offsetInfo != null && (trivial || !isTrivialToken(token))) {
         if (offsetInfo.expectedIndent != null) {
           expectedIndent = Math.min(expectedIndent, offsetInfo.expectedIndent)
         } else {
@@ -631,8 +648,16 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
    */
   function validateCore (token, expectedIndent, optionalExpectedIndent) {
     const line = token.loc.start.line
-    const actualIndent = token.loc.start.column
     const indentText = getIndentText(token)
+
+    // If there is no line terminator after the `<script>` start tag,
+    // `indentText` contains non-whitespace characters.
+    // In that case, do nothing in order to prevent removing the `<script>` tag.
+    if (indentText.trim() !== '') {
+      return
+    }
+
+    const actualIndent = token.loc.start.column
     const unit = (options.indentChar === '\t' ? 'tab' : 'space')
 
     for (let i = 0; i < indentText.length; ++i) {
@@ -1421,24 +1446,23 @@ module.exports.defineVisitor = function create (context, tokenStore, defaultOpti
     // Process semicolons.
     ':statement' (node) {
       const info = offsets.get(tokenStore.getFirstToken(node))
+      const lastToken = tokenStore.getLastToken(node)
       if (info == null) {
         return
       }
+      if (isSemicolon(lastToken)) {
+        offsets.set(lastToken, info)
+      }
 
       // Set to the semicolon of the previous token for semicolon-free style.
       // E.g.,
       //   foo
       //   ;[1,2,3].forEach(f)
-      const tokens = [
-        tokenStore.getTokenBefore(node),
-        tokenStore.getLastToken(node)
-      ].filter(isSemicolon)
-
-      // Set offsets if the semicolon is at beginning of line.
-      for (const token of tokens) {
-        const prevToken = tokenStore.getTokenBefore(token)
-        if (prevToken == null || token.loc.end.line !== prevToken.loc.start.line) {
-          offsets.set(token, info)
+      const prevToken = tokenStore.getTokenBefore(node)
+      if (isSemicolon(prevToken)) {
+        const prevPrevToken = tokenStore.getTokenBefore(prevToken)
+        if (prevPrevToken == null || prevToken.loc.end.line !== prevPrevToken.loc.start.line) {
+          offsets.set(prevToken, info)
         }
       }
     },
diff --git a/lib/utils/index.js b/lib/utils/index.js
index 4667c437..677fff6b 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -384,12 +384,15 @@ module.exports = {
       callee.object.type === 'Identifier' &&
       callee.object.name === 'Vue' &&
       callee.property.type === 'Identifier' &&
-      (callee.property.name === 'component' || callee.property.name === 'mixin') &&
-      node.arguments.length &&
+      ['component', 'mixin', 'extend'].indexOf(callee.property.name) > -1 &&
+      node.arguments.length >= 1 &&
       node.arguments.slice(-1)[0].type === 'ObjectExpression'
 
-    const isDestructedVueComponent = callee.type === 'Identifier' &&
-      callee.name === 'component'
+    const isDestructedVueComponent = node.type === 'CallExpression' &&
+      callee.type === 'Identifier' &&
+      callee.name === 'component' &&
+      node.arguments.length >= 1 &&
+      node.arguments.slice(-1)[0].type === 'ObjectExpression'
 
     return isFullVueComponent || isDestructedVueComponent
   },


Test patch:
diff --git a/tests/fixtures/html-indent/v-start-tag-02.vue b/tests/fixtures/html-indent/v-start-tag-02.vue
new file mode 100644
index 00000000..7ac1ee0d
--- /dev/null
+++ b/tests/fixtures/html-indent/v-start-tag-02.vue
@@ -0,0 +1,5 @@
+<!--{}-->
+<template>
+  <div
+    aaa>
+</template>
diff --git a/tests/fixtures/html-indent/v-start-tag-03.vue b/tests/fixtures/html-indent/v-start-tag-03.vue
new file mode 100644
index 00000000..4a4d918a
--- /dev/null
+++ b/tests/fixtures/html-indent/v-start-tag-03.vue
@@ -0,0 +1,6 @@
+<!--{}-->
+<template>
+  <div
+    aaa
+    bbb>
+</template>
diff --git a/tests/fixtures/html-indent/v-start-tag-04.vue b/tests/fixtures/html-indent/v-start-tag-04.vue
new file mode 100644
index 00000000..69ac40a5
--- /dev/null
+++ b/tests/fixtures/html-indent/v-start-tag-04.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<template>
+  <div
+    aaa
+    bbb
+  >
+</template>
diff --git a/tests/fixtures/html-indent/v-start-tag-05.vue b/tests/fixtures/html-indent/v-start-tag-05.vue
new file mode 100644
index 00000000..5d7d7155
--- /dev/null
+++ b/tests/fixtures/html-indent/v-start-tag-05.vue
@@ -0,0 +1,5 @@
+<!--{}-->
+<template>
+  <div
+    aaa/>
+</template>
diff --git a/tests/fixtures/script-indent/array-expression-02.vue b/tests/fixtures/script-indent/array-expression-02.vue
new file mode 100644
index 00000000..d398bde9
--- /dev/null
+++ b/tests/fixtures/script-indent/array-expression-02.vue
@@ -0,0 +1,5 @@
+<!--{}-->
+<script>
+var a = [
+  1]
+</script>
diff --git a/tests/fixtures/script-indent/function-declaration-04.vue b/tests/fixtures/script-indent/function-declaration-04.vue
new file mode 100644
index 00000000..d4ce7897
--- /dev/null
+++ b/tests/fixtures/script-indent/function-declaration-04.vue
@@ -0,0 +1,7 @@
+<!--{}-->
+<script>
+function foo(
+  a)
+{
+  foo()}
+</script>
diff --git a/tests/fixtures/script-indent/no-linebreak-script.vue b/tests/fixtures/script-indent/no-linebreak-script.vue
new file mode 100644
index 00000000..b8555f2d
--- /dev/null
+++ b/tests/fixtures/script-indent/no-linebreak-script.vue
@@ -0,0 +1,3 @@
+<!--{}-->
+<script>var a
+</script>
diff --git a/tests/fixtures/script-indent/object-expression-03.vue b/tests/fixtures/script-indent/object-expression-03.vue
new file mode 100644
index 00000000..f65063ec
--- /dev/null
+++ b/tests/fixtures/script-indent/object-expression-03.vue
@@ -0,0 +1,5 @@
+<!--{}-->
+<script>
+var obj = {
+  x: 1}
+</script>
diff --git a/tests/lib/rules/max-attributes-per-line.js b/tests/lib/rules/max-attributes-per-line.js
index 773f1b66..0e8ab915 100644
--- a/tests/lib/rules/max-attributes-per-line.js
+++ b/tests/lib/rules/max-attributes-per-line.js
@@ -91,6 +91,8 @@ ruleTester.run('max-attributes-per-line', rule, {
   invalid: [
     {
       code: `<template><component name="John Doe" age="30"></component></template>`,
+      output: `<template><component name="John Doe" 
+age="30"></component></template>`,
       errors: ['Attribute "age" should be on a new line.']
     },
     {
@@ -99,6 +101,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         age="30">
         </component>
       </template>`,
+      output: `<template><component 
+job="Vet"
+        name="John Doe"
+        age="30">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "job" should be on a new line.',
         type: 'VAttribute',
@@ -108,6 +116,8 @@ ruleTester.run('max-attributes-per-line', rule, {
     {
       code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
       options: [{ singleline: { max: 2 }}],
+      output: `<template><component name="John Doe" age="30" 
+job="Vet"></component></template>`,
       errors: [{
         message: 'Attribute "job" should be on a new line.',
         type: 'VAttribute',
@@ -117,6 +127,8 @@ ruleTester.run('max-attributes-per-line', rule, {
     {
       code: `<template><component name="John Doe" age="30" job="Vet"></component></template>`,
       options: [{ singleline: 1, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component name="John Doe" 
+age="30" job="Vet"></component></template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -133,6 +145,11 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component 
+name="John Doe"
+        age="30">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "name" should be on a new line.',
         type: 'VAttribute',
@@ -146,6 +163,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 1, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" 
+age="30"
+        job="Vet">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -159,6 +182,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: 1 }],
+      output: `<template><component
+        name="John Doe" 
+age="30"
+        job="Vet">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "age" should be on a new line.',
         type: 'VAttribute',
@@ -172,6 +201,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" 
+petname="Snoopy">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "petname" should be on a new line.',
         type: 'VAttribute',
@@ -185,6 +220,12 @@ ruleTester.run('max-attributes-per-line', rule, {
         </component>
       </template>`,
       options: [{ singleline: 3, multiline: { max: 2, allowFirstLine: false }}],
+      output: `<template><component
+        name="John Doe" age="30"
+        job="Vet" pet="dog" 
+petname="Snoopy" extra="foo">
+        </component>
+      </template>`,
       errors: [{
         message: 'Attribute "petname" should be on a new line.',
         type: 'VAttribute',
diff --git a/tests/lib/rules/no-async-in-computed-properties.js b/tests/lib/rules/no-async-in-computed-properties.js
index e7cd0269..746ba319 100644
--- a/tests/lib/rules/no-async-in-computed-properties.js
+++ b/tests/lib/rules/no-async-in-computed-properties.js
@@ -60,6 +60,20 @@ ruleTester.run('no-async-in-computed-properties', rule, {
         }
       `,
       parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        async function resolveComponents(components) {
+          return await Promise.all(components.map(async (component) => {
+              if(typeof component === 'function') {
+                    return await component()
+                }
+                return component;
+          }));
+        }
+      `,
+      parserOptions: parserOptions8
     }
   ],
 
diff --git a/tests/lib/rules/order-in-components.js b/tests/lib/rules/order-in-components.js
index 48501541..2eff407e 100644
--- a/tests/lib/rules/order-in-components.js
+++ b/tests/lib/rules/order-in-components.js
@@ -144,6 +144,19 @@ ruleTester.run('order-in-components', rule, {
         }
       `,
       parserOptions,
+      output: `
+        export default {
+          name: 'app',
+          props: {
+            propA: Number,
+          },
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+        }
+      `,
       errors: [{
         message: 'The "props" property should be above the "data" property on line 4.',
         line: 9
@@ -170,6 +183,24 @@ ruleTester.run('order-in-components', rule, {
         }
       `,
       parserOptions: { ecmaVersion: 6, sourceType: 'module', ecmaFeatures: { jsx: true }},
+      output: `
+        export default {
+          name: 'app',
+          render (h) {
+            return (
+              <span>{ this.msg }</span>
+            )
+          },
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          props: {
+            propA: Number,
+          },
+        }
+      `,
       errors: [{
         message: 'The "name" property should be above the "render" property on line 3.',
         line: 8
@@ -196,6 +227,18 @@ ruleTester.run('order-in-components', rule, {
         })
       `,
       parserOptions: { ecmaVersion: 6 },
+      output: `
+        Vue.component('smart-list', {
+          name: 'app',
+          components: {},
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          template: '<div></div>'
+        })
+      `,
       errors: [{
         message: 'The "components" property should be above the "data" property on line 4.',
         line: 9
@@ -217,6 +260,19 @@ ruleTester.run('order-in-components', rule, {
         })
       `,
       parserOptions: { ecmaVersion: 6 },
+      output: `
+        const { component } = Vue;
+        component('smart-list', {
+          name: 'app',
+          components: {},
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          template: '<div></div>'
+        })
+      `,
       errors: [{
         message: 'The "components" property should be above the "data" property on line 5.',
         line: 10
@@ -238,6 +294,19 @@ ruleTester.run('order-in-components', rule, {
         })
       `,
       parserOptions: { ecmaVersion: 6 },
+      output: `
+        new Vue({
+          el: '#app',
+          name: 'app',
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          components: {},
+          template: '<div></div>'
+        })
+      `,
       errors: [{
         message: 'The "el" property should be above the "name" property on line 3.',
         line: 4
@@ -267,6 +336,24 @@ ruleTester.run('order-in-components', rule, {
         };
       `,
       parserOptions,
+      output: `
+        export default {
+          name: 'burger',
+          data() {
+            return {
+              isActive: false,
+            };
+          },
+          methods: {
+            toggleMenu() {
+              this.isActive = !this.isActive;
+            },
+            closeMenu() {
+              this.isActive = false;
+            }
+          },
+        };
+      `,
       errors: [{
         message: 'The "name" property should be above the "data" property on line 3.',
         line: 16
@@ -283,11 +370,353 @@ ruleTester.run('order-in-components', rule, {
         };
       `,
       parserOptions,
+      output: `
+        export default {
+          data() {
+          },
+          test: 'ok',
+          name: 'burger',
+        };
+      `,
       options: [{ order: ['data', 'test', 'name'] }],
       errors: [{
         message: 'The "test" property should be above the "name" property on line 5.',
         line: 6
       }]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+        export default {
+          /** data provider */
+          data() {
+          },
+          /** name of vue component */
+          name: 'burger'
+        };
+      `,
+      parserOptions,
+      output: `
+        export default {
+          /** name of vue component */
+          name: 'burger',
+          /** data provider */
+          data() {
+          },
+        };
+      `,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 4.',
+        line: 7
+      }]
+    },
+    {
+      filename: 'example.vue',
+      code: `export default {data(){},name:'burger'};`,
+      parserOptions,
+      output: `export default {name:'burger',data(){},};`,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 1.',
+        line: 1
+      }]
+    },
+    {
+      // side-effects CallExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: obj.fn(),
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects NewExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: new MyClass(),
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects UpdateExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: i++,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects AssignmentExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: i = 0,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects TaggedTemplateExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: template\`\${foo}\`,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects key
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          [obj.fn()]: 'test',
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects object deep props
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: {test: obj.fn()},
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects array elements
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: [obj.fn(), 1],
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects call at middle
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: obj.fn().prop,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects delete
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: delete obj.prop,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects within BinaryExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: fn() + a + b,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects within ConditionalExpression
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: a ? fn() : null,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // side-effects within TemplateLiteral
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          test: \`test \${fn()} \${a}\`,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: null,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 6
+      }]
+    },
+    {
+      // without side-effects
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          name: 'burger',
+          test: fn(),
+        };
+      `,
+      parserOptions,
+      output: `
+        export default {
+          name: 'burger',
+          data() {
+          },
+          test: fn(),
+        };
+      `,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 5
+      }]
+    },
+    {
+      // don't side-effects
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+          },
+          testArray: [1, 2, 3, true, false, 'a', 'b', 'c'],
+          testRegExp: /[a-z]*/,
+          testSpreadElement: [...array],
+          testOperator: (!!(a - b + c * d / e % f)) || (a && b),
+          testArrow: (a) => a,
+          testConditional: a ? b : c,
+          testYield: function* () {},
+          testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      output: `
+        export default {
+          name: 'burger',
+          data() {
+          },
+          testArray: [1, 2, 3, true, false, 'a', 'b', 'c'],
+          testRegExp: /[a-z]*/,
+          testSpreadElement: [...array],
+          testOperator: (!!(a - b + c * d / e % f)) || (a && b),
+          testArrow: (a) => a,
+          testConditional: a ? b : c,
+          testYield: function* () {},
+          testTemplate: \`a:\${a},b:\${b},c:\${c}.\`,
+        };
+      `,
+      errors: [{
+        message: 'The "name" property should be above the "data" property on line 3.',
+        line: 13
+      }]
     }
   ]
 })
diff --git a/tests/lib/rules/prop-name-casing.js b/tests/lib/rules/prop-name-casing.js
new file mode 100644
index 00000000..3df60c8b
--- /dev/null
+++ b/tests/lib/rules/prop-name-casing.js
@@ -0,0 +1,200 @@
+/**
+ * @fileoverview Define a style for the name property casing for consistency purposes
+ * @author Yu Kimura
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/prop-name-casing')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const parserOptions = {
+  ecmaVersion: 6,
+  sourceType: 'module',
+  ecmaFeatures: { experimentalObjectRestSpread: true }
+}
+
+const ruleTester = new RuleTester()
+ruleTester.run('prop-name-casing', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['greetingText']
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: some_props
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            ...some_props,
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['greetingText']
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: ['greetingText']
+        }
+      `,
+      options: ['snake_case'],
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            greetingText: String
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        props: {
+          greetingText: String
+        }
+      }
+      `,
+      options: ['camelCase'],
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      export default {
+        props: {
+          greeting_text: String
+        }
+      }
+      `,
+      options: ['snake_case'],
+      parserOptions
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            greeting_text: String
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting_text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            greeting_text: String
+          }
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting_text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            greetingText: String
+          }
+        }
+      `,
+      options: ['snake_case'],
+      parserOptions,
+      errors: [{
+        message: 'Prop "greetingText" is not in snake_case.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            'greeting-text': String
+          }
+        }
+      `,
+      options: ['camelCase'],
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting-text" is not in camelCase.',
+        type: 'Property',
+        line: 4
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          props: {
+            'greeting-text': String
+          }
+        }
+      `,
+      options: ['snake_case'],
+      parserOptions,
+      errors: [{
+        message: 'Prop "greeting-text" is not in snake_case.',
+        type: 'Property',
+        line: 4
+      }]
+    }
+  ]
+})
diff --git a/tests/lib/rules/valid-v-on.js b/tests/lib/rules/valid-v-on.js
index e7179c10..46dd8b4e 100644
--- a/tests/lib/rules/valid-v-on.js
+++ b/tests/lib/rules/valid-v-on.js
@@ -43,6 +43,30 @@ tester.run('valid-v-on', rule, {
       filename: 'test.vue',
       code: '<template><div @keydown.27="foo"></div></template>'
     },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.enter="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.arrow-down="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.esc="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.a="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.b="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div @keydown.a.b.c="foo"></div></template>'
+    },
     {
       filename: 'test.vue',
       code: '<template><el-from @submit.native.prevent></el-form></template>'
diff --git a/tests/lib/utils/casing.js b/tests/lib/utils/casing.js
index 743d24fd..e9cbe921 100644
--- a/tests/lib/utils/casing.js
+++ b/tests/lib/utils/casing.js
@@ -32,4 +32,13 @@ describe('getConverter()', () => {
     assert.ok(converter('FooBar') === 'foo-bar')
     assert.ok(converter('Foo1Bar') === 'foo1bar')
   })
+
+  it('should conver string to snake_case', () => {
+    const converter = casing.getConverter('snake_case')
+
+    assert.ok(converter('fooBar') === 'foo_bar')
+    assert.ok(converter('foo-bar') === 'foo_bar')
+    assert.ok(converter('FooBar') === 'foo_bar')
+    assert.ok(converter('Foo1Bar') === 'foo1bar')
+  })
 })
diff --git a/tests/lib/utils/vue-component.js b/tests/lib/utils/vue-component.js
index 0dd2d0cf..87b8738a 100644
--- a/tests/lib/utils/vue-component.js
+++ b/tests/lib/utils/vue-component.js
@@ -126,6 +126,12 @@ function invalidTests (ext) {
       parserOptions,
       errors: [makeError(1)]
     },
+    {
+      filename: `test.${ext}`,
+      code: `Vue.extend({})`,
+      parserOptions,
+      errors: [makeError(1)]
+    },
     {
       filename: `test.${ext}`,
       code: `