Repository: vuejs/eslint-plugin-vue
Pull request #1022 (2019-12-28 22:35:59)

New: attributes-order adding alphabetical ordering within groupings
This allows an option "alphabetical" to sort attributes within groupings alphabetically 
close: https://github.com/vuejs/eslint-plugin-vue/issues/968


Patch:
diff --git a/.eslintignore b/.eslintignore
index 20f3203c..1905aa4a 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -2,7 +2,7 @@
 /coverage
 /node_modules
 /tests/fixtures
-/tests/integrations/*/node_modules
+/tests/integrations/eslint-plugin-import
 
 !.vuepress
 /docs/.vuepress/dist
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 30c0f93d..1e6d30bd 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -165,9 +165,11 @@ For example:
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
+| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
 | [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported |  |
 | [vue/require-name-property](./require-name-property.md) | require a name property in Vue components |  |
 | [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
+| [vue/sort-keys](./sort-keys.md) | enforce sort-keys within components after the top level details |  |
 | [vue/space-infix-ops](./space-infix-ops.md) | require spacing around infix operators | :wrench: |
 | [vue/space-unary-ops](./space-unary-ops.md) | enforce consistent spacing before or after unary operators | :wrench: |
 | [vue/static-class-names-order](./static-class-names-order.md) | enforce static class names order | :wrench: |
diff --git a/docs/rules/attributes-order.md b/docs/rules/attributes-order.md
index 8c61d368..879ec1d5 100644
--- a/docs/rules/attributes-order.md
+++ b/docs/rules/attributes-order.md
@@ -105,11 +105,57 @@ This rule aims to enforce ordering of component attributes. The default order is
       "OTHER_ATTR",
       "EVENTS",
       "CONTENT"
-    ]
+    ],
+    "alphabetical": true
   }]
 }
 ```
 
+### Alphabetical order 
+<eslint-code-block fix :rules="{'vue/attributes-order': ['error', {alphabetical: true}]}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+    <div
+      a-custom-prop="value"
+      :another-custom-prop="value"
+      :blue-color="false"
+      boolean-prop
+      z-prop="Z"
+      v-on:[c]="functionCall"
+      @change="functionCall"
+      v-on:click="functionCall"
+      @input="functionCall"
+      v-text="textContent">
+    </div>
+
+  <!-- ✗ BAD -->
+    <div
+      z-prop="Z"
+      a-prop="A">
+    </div>
+
+    <div
+      @input="bar"
+      @change="foo">
+    </div>
+
+    <div
+      v-on:click="functionCall"
+      v-on:[c]="functionCall">
+    </div>
+
+    <div
+      :a-prop="A"
+      :z-prop="Z">
+    </div>
+
+</template>
+```
+
+</eslint-code-block>
+
 ### Custom orders
 
 #### `['LIST_RENDERING', 'CONDITIONALS', 'RENDER_MODIFIERS', 'GLOBAL', 'UNIQUE', 'TWO_WAY_BINDING', 'DEFINITION', 'OTHER_DIRECTIVES', 'OTHER_ATTR', 'EVENTS', 'CONTENT']`
diff --git a/docs/rules/match-component-file-name.md b/docs/rules/match-component-file-name.md
index 961937b1..bbf84be7 100644
--- a/docs/rules/match-component-file-name.md
+++ b/docs/rules/match-component-file-name.md
@@ -9,8 +9,7 @@ description: require component name property to match its file name
 
 This rule reports if a component `name` property does not match its file name.
 
-You can define an array of file extensions this rule should verify for 
-the component's name.
+You can define an array of file extensions this rule should verify for the component's name.
 
 ## :book: Rule Details
 
@@ -27,7 +26,7 @@ This rule has some options.
 
 By default this rule will only verify components in a file with a `.jsx` extension.
 
-You can use any combination of `".jsx"`, `".vue"` and `".js"` extensions.
+You can use any combination of `".js"`, `".jsx"`, `".ts"`, `".tsx"`, and `".vue"` extensions.
 
 You can also enforce same case between the component's name and its file name.
 
diff --git a/docs/rules/padding-line-between-blocks.md b/docs/rules/padding-line-between-blocks.md
new file mode 100644
index 00000000..34a1d046
--- /dev/null
+++ b/docs/rules/padding-line-between-blocks.md
@@ -0,0 +1,139 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/padding-line-between-blocks
+description: require or disallow padding lines between blocks
+---
+# vue/padding-line-between-blocks
+> require or disallow padding lines between blocks
+
+- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
+
+## :book: Rule Details
+
+This rule requires or disallows blank lines between the given 2 blocks. Properly blank lines help developers to understand the code.
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style></style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div></div>
+</template>
+<script>
+export default {}
+</script>
+<style></style>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+```json
+{
+  "vue/padding-line-between-blocks": ["error", "always" | "never"]
+}
+```
+
+- `"always"` (default) ... Requires one or more blank lines. Note it does not count lines that comments exist as blank lines.
+- `"never"` ... Disallows blank lines.
+
+### `"always"` (default)
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style></style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div></div>
+</template>
+<script>
+export default {}
+</script>
+<style></style>
+```
+
+</eslint-code-block>
+
+### `"never"`
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error', 'never']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <div></div>
+</template>
+<script>
+export default {}
+</script>
+<style></style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error', 'never']}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <div></div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style></style>
+```
+
+</eslint-code-block>
+
+## :books: Further reading
+
+- [padding-line-between-statements]
+- [lines-between-class-members]
+
+[padding-line-between-statements]: https://eslint.org/docs/rules/padding-line-between-statements
+[lines-between-class-members]: https://eslint.org/docs/rules/lines-between-class-members
+
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-blocks.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-blocks.js)
diff --git a/docs/rules/sort-keys.md b/docs/rules/sort-keys.md
new file mode 100644
index 00000000..38939fd5
--- /dev/null
+++ b/docs/rules/sort-keys.md
@@ -0,0 +1,109 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/sort-keys
+description: enforce sort-keys in a manner that is compatible with order-in-components
+---
+# vue/sort-keys
+> enforce sort-keys within components after the top level details
+
+This rule is almost the same rule as core [sorts-keys] rule but it will not error on top component properties allowing that order to be enforced with `order-in-components`.
+
+## Options
+
+```json
+{
+    "sort-keys": ["error", "asc", {
+        "caseSensitive": true,
+        "ignoreChildrenOf": ["model"],
+        "ignoreGrandchildrenOf": ["computed", "directives", "inject", "props", "watch"],
+        "minKeys": 2,
+        "natural": false
+  }]
+}
+```
+
+The 1st option is `"asc"` or `"desc"`.
+
+* `"asc"` (default) - enforce properties to be in ascending order.
+* `"desc"` - enforce properties to be in descending order.
+
+The 2nd option is an object which has 5 properties.
+
+* `caseSensitive` - if `true`, enforce properties to be in case-sensitive order. Default is `true`.
+* `ignoreChildrenOf` - an array of properties to ignore the children of.  Default is `["model"]`
+* `ignoreGrandchildrenOf` - an array of properties to ignore the grandchildren sort order. Default is `["computed", "directives", "inject", "props", "watch"]`
+* `minKeys` - Specifies the minimum number of keys that an object should have in order for the object's unsorted keys to produce an error. Default is `2`, which means by default all objects with unsorted keys will result in lint errors.
+* `natural` - if `true`, enforce properties to be in natural order. Default is `false`. Natural Order compares strings containing combination of letters and numbers in the way a human being would sort. It basically sorts numerically, instead of sorting alphabetically. So the number 10 comes after the number 3 in Natural Sorting.
+
+While using this rule, you may disable the normal `sort-keys` rule.  This rule will apply to plain js files as well as Vue component scripts.
+
+## :book: Rule Details
+
+This rule forces many dictionary properties to be in alphabetical order while allowing `order-in-components`, property details,
+and other similar fields not to be in alphabetical order.
+
+<eslint-code-block fix :rules="{'vue/sort-keys': ['error']}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  name: 'app',
+  model: {
+    prop: 'checked',
+    event: 'change'
+  },
+  props: {
+    propA: {
+      type: String,
+      default: 'abc',
+    },
+    propB: {
+      type: String,
+      default: 'abc',
+    },
+  },
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/sort-keys': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  name: 'app',
+  model: {
+    prop: 'checked',
+    event: 'change'
+  },
+  props: {
+    propB: {
+      type: String,
+      default: 'abc',
+    },
+    propA: {
+      type: String,
+      default: 'abc',
+    },
+  },
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :books: Further reading
+
+- [sorts-keys]
+
+[sort-keys]: https://eslint.org/docs/rules/sort-keys
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/sort-keys.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/sort-keys.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index e8fededd..01710c07 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -140,7 +140,7 @@ Example **.vscode/settings.json**:
   "eslint.validate": [
     "javascript",
     "javascriptreact",
-    { "language": "vue", "autoFix": true }
+    "vue"
   ]
 }
 ```
diff --git a/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 3bf2fec4..e45c080b 100644
--- a/lib/configs/no-layout-rules.js
+++ b/lib/configs/no-layout-rules.js
@@ -25,6 +25,7 @@ module.exports = {
     'vue/no-multi-spaces': 'off',
     'vue/no-spaces-around-equal-signs-in-attribute': 'off',
     'vue/object-curly-spacing': 'off',
+    'vue/padding-line-between-blocks': 'off',
     'vue/script-indent': 'off',
     'vue/singleline-html-element-content-newline': 'off',
     'vue/space-infix-ops': 'off',
diff --git a/lib/index.js b/lib/index.js
index d1b9bd9e..02e65ba9 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -65,6 +65,7 @@ module.exports = {
     'no-v-html': require('./rules/no-v-html'),
     'object-curly-spacing': require('./rules/object-curly-spacing'),
     'order-in-components': require('./rules/order-in-components'),
+    'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
     'prop-name-casing': require('./rules/prop-name-casing'),
     'require-component-is': require('./rules/require-component-is'),
     'require-default-prop': require('./rules/require-default-prop'),
@@ -78,6 +79,7 @@ module.exports = {
     'return-in-computed-property': require('./rules/return-in-computed-property'),
     'script-indent': require('./rules/script-indent'),
     'singleline-html-element-content-newline': require('./rules/singleline-html-element-content-newline'),
+    'sort-keys': require('./rules/sort-keys'),
     'space-infix-ops': require('./rules/space-infix-ops'),
     'space-unary-ops': require('./rules/space-unary-ops'),
     'static-class-names-order': require('./rules/static-class-names-order'),
diff --git a/lib/rules/attributes-order.js b/lib/rules/attributes-order.js
index afa0b3c5..ac8afe28 100644
--- a/lib/rules/attributes-order.js
+++ b/lib/rules/attributes-order.js
@@ -22,6 +22,14 @@ const ATTRS = {
   CONTENT: 'CONTENT'
 }
 
+function getAttributeName (attribute, sourceCode) {
+  const isBind = attribute.directive && attribute.key.name.name === 'bind'
+  debugger
+  return isBind
+    ? (attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '')
+    : (attribute.directive ? sourceCode.getText(attribute.key.argument) : attribute.key.name)
+}
+
 function getAttributeType (attribute, sourceCode) {
   const isBind = attribute.directive && attribute.key.name.name === 'bind'
   const name = isBind
@@ -64,6 +72,14 @@ function getPosition (attribute, attributePosition, sourceCode) {
   return attributePosition.hasOwnProperty(attributeType) ? attributePosition[attributeType] : -1
 }
 
+function isAlphabetical (prevNode, currNode, sourceCode) {
+  const isSameType = getAttributeType(prevNode, sourceCode) === getAttributeType(currNode, sourceCode)
+  if (isSameType) {
+    return getAttributeName(prevNode, sourceCode) < getAttributeName(currNode, sourceCode)
+  }
+  return true
+}
+
 function create (context) {
   const sourceCode = context.getSourceCode()
   let attributeOrder = [ATTRS.DEFINITION, ATTRS.LIST_RENDERING, ATTRS.CONDITIONALS, ATTRS.RENDER_MODIFIERS, ATTRS.GLOBAL, ATTRS.UNIQUE, ATTRS.TWO_WAY_BINDING, ATTRS.OTHER_DIRECTIVES, ATTRS.OTHER_ATTR, ATTRS.EVENTS, ATTRS.CONTENT]
@@ -110,7 +126,11 @@ function create (context) {
       previousNode = null
     },
     'VAttribute' (node) {
-      if ((currentPosition === -1) || (currentPosition <= getPosition(node, attributePosition, sourceCode))) {
+      let inAlphaOrder = true
+      if (currentPosition !== -1 && (context.options[0] && context.options[0].alphabetical)) {
+        inAlphaOrder = isAlphabetical(previousNode, node, sourceCode)
+      }
+      if ((currentPosition === -1) || ((currentPosition <= getPosition(node, attributePosition, sourceCode)) && inAlphaOrder)) {
         currentPosition = getPosition(node, attributePosition, sourceCode)
         previousNode = node
       } else {
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index d6400dec..7129aa36 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -45,7 +45,7 @@ module.exports = {
 
     function getTopLevelHTMLElements () {
       if (documentFragment) {
-        return documentFragment.children
+        return documentFragment.children.filter(e => e.type === 'VElement')
       }
       return []
     }
diff --git a/lib/rules/padding-line-between-blocks.js b/lib/rules/padding-line-between-blocks.js
new file mode 100644
index 00000000..6f08a5d7
--- /dev/null
+++ b/lib/rules/padding-line-between-blocks.js
@@ -0,0 +1,194 @@
+/**
+ * @fileoverview Require or disallow padding lines between blocks
+ * @author Yosuke Ota
+ */
+'use strict'
+const utils = require('../utils')
+
+/**
+ * Split the source code into multiple lines based on the line delimiters.
+ * @param {string} text Source code as a string.
+ * @returns {string[]} Array of source code lines.
+ */
+function splitLines (text) {
+  return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
+}
+
+/**
+ * Check and report blocks for `never` configuration.
+ * This autofix removes blank lines between the given 2 blocks.
+ * @param {RuleContext} context The rule context to report.
+ * @param {VElement} prevBlock The previous block to check.
+ * @param {VElement} nextBlock The next block to check.
+ * @param {Token[]} betweenTokens The array of tokens between blocks.
+ * @returns {void}
+ * @private
+ */
+function verifyForNever (context, prevBlock, nextBlock, betweenTokens) {
+  if (prevBlock.loc.end.line === nextBlock.loc.start.line) {
+    // same line
+    return
+  }
+  const tokenOrNodes = [...betweenTokens, nextBlock]
+  let prev = prevBlock
+  const paddingLines = []
+  for (const tokenOrNode of tokenOrNodes) {
+    const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
+    if (numOfLineBreaks > 1) {
+      paddingLines.push([prev, tokenOrNode])
+    }
+    prev = tokenOrNode
+  }
+  if (!paddingLines.length) {
+    return
+  }
+
+  context.report({
+    node: nextBlock,
+    messageId: 'never',
+    fix (fixer) {
+      return paddingLines.map(([prevToken, nextToken]) => {
+        const start = prevToken.range[1]
+        const end = nextToken.range[0]
+        const paddingText = context.getSourceCode().text
+          .slice(start, end)
+        const lastSpaces = splitLines(paddingText).pop()
+        return fixer.replaceTextRange([start, end], '\n' + lastSpaces)
+      })
+    }
+  })
+}
+
+/**
+ * Check and report blocks for `always` configuration.
+ * This autofix inserts a blank line between the given 2 blocks.
+ * @param {RuleContext} context The rule context to report.
+ * @param {VElement} prevBlock The previous block to check.
+ * @param {VElement} nextBlock The next block to check.
+ * @param {Token[]} betweenTokens The array of tokens between blocks.
+ * @returns {void}
+ * @private
+ */
+function verifyForAlways (context, prevBlock, nextBlock, betweenTokens) {
+  const tokenOrNodes = [...betweenTokens, nextBlock]
+  let prev = prevBlock
+  let linebreak
+  for (const tokenOrNode of tokenOrNodes) {
+    const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
+    if (numOfLineBreaks > 1) {
+      // Already padded.
+      return
+    }
+    if (!linebreak && numOfLineBreaks > 0) {
+      linebreak = prev
+    }
+    prev = tokenOrNode
+  }
+
+  context.report({
+    node: nextBlock,
+    messageId: 'always',
+    fix (fixer) {
+      if (linebreak) {
+        return fixer.insertTextAfter(linebreak, '\n')
+      }
+      return fixer.insertTextAfter(prevBlock, '\n\n')
+    }
+  })
+}
+
+/**
+ * Types of blank lines.
+ * `never` and `always` are defined.
+ * Those have `verify` method to check and report statements.
+ * @private
+ */
+const PaddingTypes = {
+  never: { verify: verifyForNever },
+  always: { verify: verifyForAlways }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'layout',
+    docs: {
+      description: 'require or disallow padding lines between blocks',
+      category: undefined,
+      url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html'
+    },
+    fixable: 'whitespace',
+    schema: [
+      {
+        enum: Object.keys(PaddingTypes)
+      }
+    ],
+    messages: {
+      never: 'Unexpected blank line before this block.',
+      always: 'Expected blank line before this block.'
+    }
+  },
+  create (context) {
+    const paddingType = PaddingTypes[context.options[0] || 'always']
+    const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
+
+    let tokens
+    function getTopLevelHTMLElements () {
+      if (documentFragment) {
+        return documentFragment.children.filter(e => e.type === 'VElement')
+      }
+      return []
+    }
+
+    function getTokenAndCommentsBetween (prev, next) {
+      // When there is no <template>, tokenStore.getTokensBetween cannot be used.
+      if (!tokens) {
+        tokens = [
+          ...documentFragment.tokens
+            .filter(token => token.type !== 'HTMLWhitespace'),
+          ...documentFragment.comments
+        ].sort((a, b) => a.range[0] > b.range[0] ? 1 : a.range[0] < b.range[0] ? -1 : 0)
+      }
+
+      let token = tokens.shift()
+
+      const results = []
+      while (token) {
+        if (prev.range[1] <= token.range[0]) {
+          if (next.range[0] <= token.range[0]) {
+            tokens.unshift(token)
+            break
+          } else {
+            results.push(token)
+          }
+        }
+        token = tokens.shift()
+      }
+
+      return results
+    }
+
+    return utils.defineTemplateBodyVisitor(
+      context,
+      {},
+      {
+        Program (node) {
+          if (utils.hasInvalidEOF(node)) {
+            return
+          }
+          const elements = [...getTopLevelHTMLElements()]
+
+          let prev = elements.shift()
+          for (const element of elements) {
+            const betweenTokens = getTokenAndCommentsBetween(prev, element)
+            paddingType.verify(context, prev, element, betweenTokens)
+            prev = element
+          }
+        }
+      }
+    )
+  }
+}
diff --git a/lib/rules/sort-keys.js b/lib/rules/sort-keys.js
new file mode 100644
index 00000000..8f900ef0
--- /dev/null
+++ b/lib/rules/sort-keys.js
@@ -0,0 +1,265 @@
+/**
+ * @fileoverview enforce sort-keys in a manner that is compatible with order-in-components
+ * @author Loren Klingman
+ * Original ESLint sort-keys by Toru Nagashima
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const naturalCompare = require('natural-compare')
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Gets the property name of the given `Property` node.
+ *
+ * - If the property's key is an `Identifier` node, this returns the key's name
+ *   whether it's a computed property or not.
+ * - If the property has a static name, this returns the static name.
+ * - Otherwise, this returns null.
+ * @param {ASTNode} node The `Property` node to get.
+ * @returns {string|null} The property name or null.
+ * @private
+ */
+function getPropertyName (node) {
+  const staticName = utils.getStaticPropertyName(node)
+
+  if (staticName !== null) {
+    return staticName
+  }
+
+  return node.key.name || null
+}
+
+/**
+ * Functions which check that the given 2 names are in specific order.
+ *
+ * Postfix `I` is meant insensitive.
+ * Postfix `N` is meant natual.
+ * @private
+ */
+const isValidOrders = {
+  asc (a, b) {
+    return a <= b
+  },
+  ascI (a, b) {
+    return a.toLowerCase() <= b.toLowerCase()
+  },
+  ascN (a, b) {
+    return naturalCompare(a, b) <= 0
+  },
+  ascIN (a, b) {
+    return naturalCompare(a.toLowerCase(), b.toLowerCase()) <= 0
+  },
+  desc (a, b) {
+    return isValidOrders.asc(b, a)
+  },
+  descI (a, b) {
+    return isValidOrders.ascI(b, a)
+  },
+  descN (a, b) {
+    return isValidOrders.ascN(b, a)
+  },
+  descIN (a, b) {
+    return isValidOrders.ascIN(b, a)
+  }
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce sort-keys in a manner that is compatible with order-in-components',
+      category: null,
+      recommended: false,
+      url: 'https://eslint.vuejs.org/rules/sort-keys.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        enum: ['asc', 'desc']
+      },
+      {
+        type: 'object',
+        properties: {
+          caseSensitive: {
+            type: 'boolean',
+            default: true
+          },
+          ignoreChildrenOf: {
+            type: 'array'
+          },
+          ignoreGrandchildrenOf: {
+            type: 'array'
+          },
+          minKeys: {
+            type: 'integer',
+            minimum: 2,
+            default: 2
+          },
+          natural: {
+            type: 'boolean',
+            default: false
+          },
+          runOutsideVue: {
+            type: 'boolean',
+            default: true
+          }
+        },
+        additionalProperties: false
+      }
+    ]
+  },
+
+  create (context) {
+    // Parse options.
+    const options = context.options[1]
+    const order = context.options[0] || 'asc'
+
+    const ignoreGrandchildrenOf = (options && options.ignoreGrandchildrenOf) || ['computed', 'directives', 'inject', 'props', 'watch']
+    const ignoreChildrenOf = (options && options.ignoreChildrenOf) || ['model']
+    const insensitive = options && options.caseSensitive === false
+    const minKeys = options && options.minKeys
+    const natual = options && options.natural
+    const isValidOrder = isValidOrders[
+      order + (insensitive ? 'I' : '') + (natual ? 'N' : '')
+    ]
+
+    // The stack to save the previous property's name for each object literals.
+    let stack = null
+
+    let errors = []
+    const names = {}
+
+    const reportErrors = (isVue) => {
+      if (isVue) {
+        errors = errors.filter((error) => {
+          let parentIsRoot = !error.hasUpper
+          let grandParentIsRoot = !error.grandparent
+          let greatGrandparentIsRoot = !error.greatGrandparent
+
+          const stackPrevChar = stack && stack.prevChar
+          if (stackPrevChar) {
+            parentIsRoot = stackPrevChar === error.parent
+            grandParentIsRoot = stackPrevChar === error.grandparent
+            greatGrandparentIsRoot = stackPrevChar === error.greatGrandparent
+          }
+
+          if (parentIsRoot) {
+            return false
+          } else if (grandParentIsRoot) {
+            return !error.parentIsProperty || !ignoreChildrenOf.includes(names[error.parent])
+          } else if (greatGrandparentIsRoot) {
+            return !error.parentIsProperty || !ignoreGrandchildrenOf.includes(names[error.grandparent])
+          }
+          return true
+        })
+      }
+      errors.forEach((error) => error.errors.forEach((e) => context.report(e)))
+      errors = []
+    }
+
+    const sortTests = {
+      ObjectExpression (node) {
+        if (!stack) {
+          reportErrors(false)
+        }
+        stack = {
+          upper: stack,
+          prevChar: null,
+          prevName: null,
+          numKeys: node.properties.length,
+          parentIsProperty: node.parent.type === 'Property',
+          errors: []
+        }
+      },
+      'ObjectExpression:exit' (node) {
+        errors.push({
+          errors: stack.errors,
+          hasUpper: !!stack.upper,
+          parentIsProperty: node.parent.type === 'Property',
+          parent: stack.upper && stack.upper.prevChar,
+          grandparent: stack.upper && stack.upper.upper && stack.upper.upper.prevChar,
+          greatGrandparent: stack.upper && stack.upper.upper && stack.upper.upper.upper && stack.upper.upper.upper.prevChar
+        })
+        stack = stack.upper
+      },
+      SpreadElement (node) {
+        if (node.parent.type === 'ObjectExpression') {
+          stack.prevName = null
+          stack.prevChar = null
+        }
+      },
+      'Program:exit' () {
+        reportErrors(false)
+      },
+      Property (node) {
+        if (node.parent.type === 'ObjectPattern') {
+          return
+        }
+
+        const prevName = stack.prevName
+        const numKeys = stack.numKeys
+        const thisName = getPropertyName(node)
+
+        if (thisName !== null) {
+          stack.prevName = thisName
+          stack.prevChar = node.range[0]
+          if (Object.prototype.hasOwnProperty.call(names, node.range[0])) {
+            throw new Error('Name clash')
+          }
+          names[node.range[0]] = thisName
+        }
+
+        if (prevName === null || thisName === null || numKeys < minKeys) {
+          return
+        }
+
+        if (!isValidOrder(prevName, thisName)) {
+          stack.errors.push({
+            node,
+            loc: node.key.loc,
+            message: "Expected object keys to be in {{natual}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
+            data: {
+              thisName,
+              prevName,
+              order,
+              insensitive: insensitive ? 'insensitive ' : '',
+              natual: natual ? 'natural ' : ''
+            }
+          })
+        }
+      }
+    }
+
+    const execOnVue = utils.executeOnVue(context, (obj) => {
+      reportErrors(true)
+    })
+
+    const result = { ...sortTests }
+
+    Object.keys(execOnVue).forEach((key) => {
+      // Ensure we call both the callback from sortTests and execOnVue if they both use the same key
+      if (Object.prototype.hasOwnProperty.call(sortTests, key)) {
+        result[key] = (node) => {
+          sortTests[key](node)
+          execOnVue[key](node)
+        }
+      } else {
+        result[key] = execOnVue[key]
+      }
+    })
+
+    return result
+  }
+}
diff --git a/package.json b/package.json
index 10f79543..472689b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-vue",
-  "version": "6.1.1",
+  "version": "6.1.2",
   "description": "Official ESLint plugin for Vue.js",
   "main": "lib/index.js",
   "scripts": {
@@ -47,7 +47,9 @@
     "eslint": "^5.0.0 || ^6.0.0"
   },
   "dependencies": {
-    "vue-eslint-parser": "^7.0.0"
+    "natural-compare": "^1.4.0",
+    "vue-eslint-parser": "^7.0.0",
+    "semver": "^5.6.0"
   },
   "devDependencies": {
     "@types/node": "^4.2.16",
@@ -64,7 +66,6 @@
     "lodash": "^4.17.4",
     "mocha": "^5.2.0",
     "nyc": "^12.0.2",
-    "semver": "^5.6.0",
     "typescript": "^3.5.2",
     "vue-eslint-editor": "^0.1.4",
     "vuepress": "^0.14.5"


Test patch:
diff --git a/tests/lib/rules/attributes-order.js b/tests/lib/rules/attributes-order.js
index 7ac9aaf5..481148f9 100644
--- a/tests/lib/rules/attributes-order.js
+++ b/tests/lib/rules/attributes-order.js
@@ -293,6 +293,25 @@ tester.run('attributes-order', rule, {
             v-text="textContent">
           </div>
         </template>`
+    },
+    {
+      filename: 'test.vue',
+      code:
+        `<template>
+          <div
+            a-custom-prop="value"
+            :another-custom-prop="value"
+            :blue-color="false"
+            boolean-prop
+            z-prop="Z"
+            v-on:[c]="functionCall"
+            @change="functionCall"
+            v-on:click="functionCall"
+            @input="functionCall"
+            v-text="textContent">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }]
     }
   ],
 
@@ -318,23 +337,23 @@ tester.run('attributes-order', rule, {
     {
       filename: 'test.vue',
       code:
-        `<template>
-          <div
-            model="baz"
-            v-model="toggle"
-            propOne="bar"
-            :id="foo">
-          </div>
-        </template>`,
+          `<template>
+            <div
+              model="baz"
+              v-model="toggle"
+              propOne="bar"
+              :id="foo">
+            </div>
+          </template>`,
       output:
-        `<template>
-          <div
-            v-model="toggle"
-            model="baz"
-            :id="foo"
-            propOne="bar">
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-model="toggle"
+              model="baz"
+              :id="foo"
+              propOne="bar">
+            </div>
+          </template>`,
       errors: [{
         message: 'Attribute "v-model" should go before "model".',
         type: 'VDirectiveKey'
@@ -347,25 +366,25 @@ tester.run('attributes-order', rule, {
     {
       filename: 'test.vue',
       code:
-        `<template>
-          <div
-            :bindingProp="foo"
-            model="baz"
-            v-on="functionCall"
-            v-model="toggle"
-            propOne="bar">
-          </div>
-        </template>`,
+          `<template>
+            <div
+              :bindingProp="foo"
+              model="baz"
+              v-on="functionCall"
+              v-model="toggle"
+              propOne="bar">
+            </div>
+          </template>`,
       output:
-        `<template>
-          <div
-            :bindingProp="foo"
-            model="baz"
-            v-model="toggle"
-            v-on="functionCall"
-            propOne="bar">
-          </div>
-        </template>`,
+          `<template>
+            <div
+              :bindingProp="foo"
+              model="baz"
+              v-model="toggle"
+              v-on="functionCall"
+              propOne="bar">
+            </div>
+          </template>`,
       errors: [{
         message: 'Attribute "v-model" should go before "v-on".',
         type: 'VDirectiveKey'
@@ -389,17 +408,17 @@ tester.run('attributes-order', rule, {
       code: '<template><div ref="header" propone="prop" is="header" ></div></template>',
       options: [
         { order:
-          ['LIST_RENDERING',
-            'CONDITIONALS',
-            'RENDER_MODIFIERS',
-            'GLOBAL',
-            'UNIQUE',
-            'TWO_WAY_BINDING',
-            'DEFINITION',
-            'OTHER_DIRECTIVES',
-            'OTHER_ATTR',
-            'EVENTS',
-            'CONTENT']
+            ['LIST_RENDERING',
+              'CONDITIONALS',
+              'RENDER_MODIFIERS',
+              'GLOBAL',
+              'UNIQUE',
+              'TWO_WAY_BINDING',
+              'DEFINITION',
+              'OTHER_DIRECTIVES',
+              'OTHER_ATTR',
+              'EVENTS',
+              'CONTENT']
         }],
       output: '<template><div ref="header" is="header" propone="prop" ></div></template>',
       errors: [{
@@ -410,17 +429,17 @@ tester.run('attributes-order', rule, {
     {
       filename: 'test.vue',
       code:
-        `<template>
-          <div v-cloak
-            is="header">
-          </div>
-        </template>`,
+          `<template>
+            <div v-cloak
+              is="header">
+            </div>
+          </template>`,
       output:
-        `<template>
-          <div is="header"
-            v-cloak>
-          </div>
-        </template>`,
+          `<template>
+            <div is="header"
+              v-cloak>
+            </div>
+          </template>`,
       errors: [{
         message: 'Attribute "is" should go before "v-cloak".',
         type: 'VIdentifier'
@@ -429,37 +448,37 @@ tester.run('attributes-order', rule, {
     {
       filename: 'test.vue',
       code:
-        `<template>
-          <div
-            v-if="!visible"
-            v-for="item in items"
-            v-once
-            is="header"
-            v-on:click="functionCall"
-            ref="header"
-            :prop="headerData"
-            v-text="textContent"
-            id="uniqueID"
-            myProp="prop"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-if="!visible"
+              v-for="item in items"
+              v-once
+              is="header"
+              v-on:click="functionCall"
+              ref="header"
+              :prop="headerData"
+              v-text="textContent"
+              id="uniqueID"
+              myProp="prop"
+              >
+            </div>
+          </template>`,
       output:
-        `<template>
-          <div
-            v-for="item in items"
-            v-if="!visible"
-            is="header"
-            v-once
-            ref="header"
-            v-on:click="functionCall"
-            :prop="headerData"
-            id="uniqueID"
-            v-text="textContent"
-            myProp="prop"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-for="item in items"
+              v-if="!visible"
+              is="header"
+              v-once
+              ref="header"
+              v-on:click="functionCall"
+              :prop="headerData"
+              id="uniqueID"
+              v-text="textContent"
+              myProp="prop"
+              >
+            </div>
+          </template>`,
       errors: [
         {
           message: 'Attribute "v-for" should go before "v-if".',
@@ -490,53 +509,53 @@ tester.run('attributes-order', rule, {
     {
       filename: 'test.vue',
       code:
-        `<template>
-          <div
-            v-if="!visible"
-            v-for="item in items"
-            v-once
-            is="header"
-            v-on:click="functionCall"
-            ref="header"
-            :prop="headerData"
-            v-text="textContent"
-            id="uniqueID"
-            myProp="prop"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-if="!visible"
+              v-for="item in items"
+              v-once
+              is="header"
+              v-on:click="functionCall"
+              ref="header"
+              :prop="headerData"
+              v-text="textContent"
+              id="uniqueID"
+              myProp="prop"
+              >
+            </div>
+          </template>`,
       options: [
         { order:
-          [
-            'EVENTS',
-            'TWO_WAY_BINDING',
-            'UNIQUE',
-            'DEFINITION',
-            'CONDITIONALS',
-            'LIST_RENDERING',
-            'RENDER_MODIFIERS',
-            'GLOBAL',
-            'OTHER_ATTR',
-            'OTHER_DIRECTIVES',
-            'CONTENT'
-          ]
+            [
+              'EVENTS',
+              'TWO_WAY_BINDING',
+              'UNIQUE',
+              'DEFINITION',
+              'CONDITIONALS',
+              'LIST_RENDERING',
+              'RENDER_MODIFIERS',
+              'GLOBAL',
+              'OTHER_ATTR',
+              'OTHER_DIRECTIVES',
+              'CONTENT'
+            ]
         }],
       output:
-        `<template>
-          <div
-            v-if="!visible"
-            v-for="item in items"
-            is="header"
-            v-once
-            v-on:click="functionCall"
-            ref="header"
-            :prop="headerData"
-            id="uniqueID"
-            v-text="textContent"
-            myProp="prop"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-if="!visible"
+              v-for="item in items"
+              is="header"
+              v-once
+              v-on:click="functionCall"
+              ref="header"
+              :prop="headerData"
+              id="uniqueID"
+              v-text="textContent"
+              myProp="prop"
+              >
+            </div>
+          </template>`,
       errors: [
         {
           message: 'Attribute "is" should go before "v-once".',
@@ -562,39 +581,39 @@ tester.run('attributes-order', rule, {
     },
     {
       code:
-        `<template>
-          <div
-            class="content"
-            v-if="!visible"
-            :class="className"
-            v-text="textContent"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              class="content"
+              v-if="!visible"
+              :class="className"
+              v-text="textContent"
+              >
+            </div>
+          </template>`,
       options: [
         { order:
-          [
-            'CONDITIONALS',
-            'LIST_RENDERING',
-            'RENDER_MODIFIERS',
-            'DEFINITION',
-            'EVENTS',
-            'UNIQUE',
-            ['BINDING', 'OTHER_ATTR'],
-            'CONTENT',
-            'GLOBAL'
-          ]
+            [
+              'CONDITIONALS',
+              'LIST_RENDERING',
+              'RENDER_MODIFIERS',
+              'DEFINITION',
+              'EVENTS',
+              'UNIQUE',
+              ['BINDING', 'OTHER_ATTR'],
+              'CONTENT',
+              'GLOBAL'
+            ]
         }],
       output:
-        `<template>
-          <div
-            v-if="!visible"
-            class="content"
-            :class="className"
-            v-text="textContent"
-            >
-          </div>
-        </template>`,
+          `<template>
+            <div
+              v-if="!visible"
+              class="content"
+              :class="className"
+              v-text="textContent"
+              >
+            </div>
+          </template>`,
       errors: [
         {
           message: 'Attribute "v-if" should go before "class".',
@@ -604,29 +623,161 @@ tester.run('attributes-order', rule, {
     },
     {
       code:
-        `<template>
-          <my-component
-            v-if="!visible"
-            v-model="content"
-            v-slot="textContent"
-            >
-          </my-component>
-        </template>`,
+          `<template>
+            <my-component
+              v-if="!visible"
+              v-model="content"
+              v-slot="textContent"
+              >
+            </my-component>
+          </template>`,
       output:
-        `<template>
-          <my-component
-            v-if="!visible"
-            v-slot="textContent"
-            v-model="content"
-            >
-          </my-component>
-        </template>`,
+          `<template>
+            <my-component
+              v-if="!visible"
+              v-slot="textContent"
+              v-model="content"
+              >
+            </my-component>
+          </template>`,
       errors: [
         {
           message: 'Attribute "v-slot" should go before "v-model".',
           nodeType: 'VIdentifier'
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+      `<template>
+          <div
+            z-prop="Z"
+            a-prop="A">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            a-prop="A"
+            z-prop="Z">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute "a-prop" should go before "z-prop".',
+        type: 'VIdentifier'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+      `<template>
+          <div
+            :z-prop="Z"
+            :a-prop="A">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            :a-prop="A"
+            :z-prop="Z">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute ":a-prop" should go before ":z-prop".',
+        type: 'VDirectiveKey'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        `<template>
+          <div
+            @input="bar"
+            @change="foo">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            @change="foo"
+            @input="bar">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute "@change" should go before "@input".',
+        type: 'VDirectiveKey'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        `<template>
+          <div
+            z-prop="value"
+            boolean-prop>
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            boolean-prop
+            z-prop="value">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute "boolean-prop" should go before "z-prop".',
+        type: 'VIdentifier'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        `<template>
+          <div
+            v-on:click="functionCall"
+            v-on:[c]="functionCall">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            v-on:[c]="functionCall"
+            v-on:click="functionCall">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute "v-on:[c]" should go before "v-on:click".',
+        type: 'VDirectiveKey'
+      }]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        `<template>
+          <div
+            v-text="textContent"
+            v-on:click="functionCall">
+          </div>
+        </template>`,
+      options: [{ alphabetical: true }],
+      output:
+        `<template>
+          <div
+            v-on:click="functionCall"
+            v-text="textContent">
+          </div>
+        </template>`,
+      errors: [{
+        message: 'Attribute "v-on:click" should go before "v-text".',
+        type: 'VDirectiveKey'
+      }]
     }
   ]
 })
diff --git a/tests/lib/rules/padding-line-between-blocks.js b/tests/lib/rules/padding-line-between-blocks.js
new file mode 100644
index 00000000..f81b2bbf
--- /dev/null
+++ b/tests/lib/rules/padding-line-between-blocks.js
@@ -0,0 +1,314 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/padding-line-between-blocks')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020 }
+})
+
+tester.run('padding-line-between-blocks', rule, {
+  valid: [
+    `
+    <template></template>
+
+    <script></script>
+
+    <style></style>
+    `,
+    `<template></template>
+
+    <script></script>
+
+    <style></style>`,
+    {
+      code: `
+      <template></template>
+      <script></script>
+      <style></style>
+      `,
+      options: ['never']
+    },
+    // comments
+    `
+    <template></template>
+
+    <!-- comment -->
+    <script></script>
+    <!-- comment -->
+
+    <style></style>
+    <!-- comment -->
+
+    <!-- comment -->
+    <i18n></i18n>
+    `,
+    {
+      code: `
+      <template></template>
+      <!-- comment -->
+      <script></script>
+      <!-- comment -->
+      <!-- comment -->
+      <style></style>
+      `,
+      options: ['never']
+    },
+    // same line
+    {
+      code: `<template></template><script></script><style></style>`,
+      options: ['never']
+    },
+    {
+      code: `<template>
+
+      </template><script>
+
+      </script>`,
+      options: ['never']
+    },
+    // no template
+    `
+    <script></script>
+
+    <style></style>
+    `,
+    {
+      code: `
+      <script></script>
+      <style></style>
+      `,
+      options: ['never']
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template></template>
+      <script></script>
+      <style></style>
+      `,
+      output: `
+      <template></template>
+
+      <script></script>
+
+      <style></style>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this block.',
+          line: 3,
+          column: 7,
+          endLine: 3,
+          endColumn: 24
+        },
+        {
+          message: 'Expected blank line before this block.',
+          line: 4,
+          column: 7,
+          endLine: 4,
+          endColumn: 22
+        }
+      ]
+    },
+    {
+      code: `
+      <template></template>
+
+      <script></script>
+
+      <style></style>
+      `,
+      options: ['never'],
+      output: `
+      <template></template>
+      <script></script>
+      <style></style>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 4,
+          column: 7,
+          endLine: 4,
+          endColumn: 24
+        },
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 6,
+          column: 7,
+          endLine: 6,
+          endColumn: 22
+        }
+      ]
+    },
+    {
+      code: `
+      <template></template><script></script><style></style>
+      `,
+      output: `
+      <template></template>
+
+<script></script>
+
+<style></style>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this block.',
+          line: 2
+        },
+        {
+          message: 'Expected blank line before this block.',
+          line: 2
+        }
+      ]
+    },
+    {
+      code: `
+      <template></template>
+      <!-- comment -->
+      <script></script>
+      <!-- comment -->
+      <!-- comment -->
+      <style></style>
+      `,
+      output: `
+      <template></template>
+
+      <!-- comment -->
+      <script></script>
+
+      <!-- comment -->
+      <!-- comment -->
+      <style></style>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this block.',
+          line: 4
+        },
+        {
+          message: 'Expected blank line before this block.',
+          line: 7
+        }
+      ]
+    },
+    {
+      code: `
+      <template></template>
+
+      <!-- comment -->
+      <script></script>
+      <!-- comment -->
+
+      <style></style>
+      <!-- comment -->
+
+      <!-- comment -->
+      <i18n></i18n>
+      `,
+      options: ['never'],
+      output: `
+      <template></template>
+      <!-- comment -->
+      <script></script>
+      <!-- comment -->
+      <style></style>
+      <!-- comment -->
+      <!-- comment -->
+      <i18n></i18n>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 5
+        },
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 8
+        },
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 12
+        }
+      ]
+    },
+    {
+      code: `
+      <template></template>TEXT
+      <!-- comment --><script></script><!-- comment
+      comment --><style></style>
+      `,
+      output: `
+      <template></template>TEXT
+
+      <!-- comment --><script></script>
+
+<!-- comment
+      comment --><style></style>
+      `,
+      errors: [
+        {
+          message: 'Expected blank line before this block.',
+          line: 3
+        },
+        {
+          message: 'Expected blank line before this block.',
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+      <script></script>
+
+
+      <!-- comment -->
+
+      TEXT TEXT
+
+      <!-- comment
+
+
+      comment -->
+
+      <!-- comment -->
+
+
+      TEXT
+
+
+
+      TEXT
+
+      <style></style>
+      `,
+      options: ['never'],
+      output: `
+      <script></script>
+      <!-- comment -->
+      TEXT TEXT
+      <!-- comment
+
+
+      comment -->
+      <!-- comment -->
+      TEXT
+      TEXT
+      <style></style>
+      `,
+      errors: [
+        {
+          message: 'Unexpected blank line before this block.',
+          line: 23
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/sort-keys.js b/tests/lib/rules/sort-keys.js
new file mode 100644
index 00000000..c8094b5b
--- /dev/null
+++ b/tests/lib/rules/sort-keys.js
@@ -0,0 +1,1195 @@
+/**
+ * @fileoverview Enforces sort-keys within components after the top level details
+ * @author Loren Klingman
+ */
+'use strict'
+
+const rule = require('../../../lib/rules/sort-keys')
+const RuleTester = require('eslint').RuleTester
+
+const ruleTester = new RuleTester()
+
+const parserOptions = {
+  ecmaVersion: 2018,
+  sourceType: 'module'
+}
+
+ruleTester.run('sort-keys', rule, {
+
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        const obj = {
+          foo() {
+            Vue.component('my-component', {
+              name: 'app',
+              data() {}
+            })
+          }
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'app',
+          props: {
+            propA: Number,
+          },
+          ...a,
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'propsOrder.vue',
+      code: `
+        export default {
+          name: 'app',
+          model: {
+            prop: 'checked',
+            event: 'change'
+          },
+          props: {
+            propA: {
+              type: String,
+              default: 'abc',
+            },
+            propB: {
+              type: String,
+              default: 'abc',
+            },
+          },
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {}
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default 'example-text'
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.jsx',
+      code: `
+        export default {
+          name: 'app',
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+        }
+      `,
+      parserOptions
+    },
+    {
+      filename: 'test.js',
+      code: `
+        Vue.component('example')
+      `,
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const { component } = Vue;
+        component('smart-list', {
+          name: 'app',
+          components: {},
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          }
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue({
+          el: '#app',
+          name: 'app',
+          components: {},
+          data () {
+            return {
+              msg: 'Welcome to Your Vue.js App'
+            }
+          }
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 }
+    },
+    {
+      filename: 'test.js',
+      code: `
+        new Vue()
+      `,
+      parserOptions: { ecmaVersion: 6 }
+    },
+    // default (asc)
+    { code: "var obj = {'':1, [``]:2}", options: [], parserOptions: { ecmaVersion: 6 }},
+    { code: "var obj = {[``]:1, '':2}", options: [], parserOptions: { ecmaVersion: 6 }},
+    { code: "var obj = {'':1, a:2}", options: [] },
+    { code: 'var obj = {[``]:1, a:2}', options: [], parserOptions: { ecmaVersion: 6 }},
+    { code: 'var obj = {_:2, a:1, b:3} // default', options: [] },
+    { code: 'var obj = {a:1, b:3, c:2}', options: [] },
+    { code: 'var obj = {a:2, b:3, b_:1}', options: [] },
+    { code: 'var obj = {C:3, b_:1, c:2}', options: [] },
+    { code: 'var obj = {$:1, A:3, _:2, a:4}', options: [] },
+    { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: [] },
+    { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: [] },
+
+    // ignore non-simple computed properties.
+    { code: 'var obj = {a:1, b:3, [a + b]: -1, c:2}', options: [], parserOptions: { ecmaVersion: 6 }},
+    { code: "var obj = {'':1, [f()]:2, a:3}", options: [], parserOptions: { ecmaVersion: 6 }},
+    { code: "var obj = {a:1, [b++]:2, '':3}", options: ['desc'], parserOptions: { ecmaVersion: 6 }},
+
+    // ignore properties separated by spread properties
+    { code: 'var obj = {a:1, ...z, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {b:1, ...z, a:1}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {...a, b:1, ...c, d:1}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {...a, b:1, ...d, ...c, e:2, z:5}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {b:1, ...c, ...d, e:2}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: "var obj = {a:1, ...z, '':2}", options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: "var obj = {'':1, ...z, 'a':2}", options: ['desc'], parserOptions: { ecmaVersion: 2018 }},
+
+    // not ignore properties not separated by spread properties
+    { code: 'var obj = {...z, a:1, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {...z, ...c, a:1, b:1}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {a:1, b:1, ...z}', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'var obj = {...z, ...x, a:1, ...c, ...d, f:5, e:4}', options: ['desc'], parserOptions: { ecmaVersion: 2018 }},
+
+    // works when spread occurs somewhere other than an object literal
+    { code: 'function fn(...args) { return [...args].length; }', options: [], parserOptions: { ecmaVersion: 2018 }},
+    { code: 'function g() {}; function f(...args) { return g(...args); }', options: [], parserOptions: { ecmaVersion: 2018 }},
+
+    // ignore destructuring patterns.
+    { code: 'let {a, b} = {}', options: [], parserOptions: { ecmaVersion: 6 }},
+
+    // nested
+    { code: 'var obj = {a:1, b:{x:1, y:1}, c:1}', options: [] },
+
+    // asc
+    { code: 'var obj = {_:2, a:1, b:3} // asc', options: ['asc'] },
+    { code: 'var obj = {a:1, b:3, c:2}', options: ['asc'] },
+    { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc'] },
+    { code: 'var obj = {C:3, b_:1, c:2}', options: ['asc'] },
+    { code: 'var obj = {$:1, A:3, _:2, a:4}', options: ['asc'] },
+    { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ['asc'] },
+    { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc'] },
+
+    // asc, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {a:1, c:2, b:3}', options: ['asc', { minKeys: 4 }] },
+
+    // asc, insensitive
+    { code: 'var obj = {_:2, a:1, b:3} // asc, insensitive', options: ['asc', { caseSensitive: false }] },
+    { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { caseSensitive: false }] },
+    { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { caseSensitive: false }] },
+    { code: 'var obj = {b_:1, C:3, c:2}', options: ['asc', { caseSensitive: false }] },
+    { code: 'var obj = {b_:1, c:3, C:2}', options: ['asc', { caseSensitive: false }] },
+    { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { caseSensitive: false }] },
+    { code: "var obj = {1:1, '11':2, 2:4, A:3}", options: ['asc', { caseSensitive: false }] },
+    { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { caseSensitive: false }] },
+
+    // asc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {$:1, A:3, _:2, a:4}', options: ['asc', { caseSensitive: false, minKeys: 5 }] },
+
+    // asc, natural
+    { code: 'var obj = {_:2, a:1, b:3} // asc, natural', options: ['asc', { natural: true }] },
+    { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { natural: true }] },
+    { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { natural: true }] },
+    { code: 'var obj = {C:3, b_:1, c:2}', options: ['asc', { natural: true }] },
+    { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { natural: true }] },
+    { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ['asc', { natural: true }] },
+    { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { natural: true }] },
+
+    // asc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {b_:1, a:2, b:3}', options: ['asc', { natural: true, minKeys: 4 }] },
+
+    // asc, natural, insensitive
+    { code: 'var obj = {_:2, a:1, b:3} // asc, natural, insensitive', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {a:1, b:3, c:2}', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {a:2, b:3, b_:1}', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {b_:1, C:3, c:2}', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {b_:1, c:3, C:2}', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: "var obj = {1:1, 2:4, '11':2, A:3}", options: ['asc', { natural: true, caseSensitive: false }] },
+    { code: "var obj = {'#':1, 'Z':2, À:3, è:4}", options: ['asc', { natural: true, caseSensitive: false }] },
+
+    // asc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {a:1, _:2, b:3}', options: ['asc', { natural: true, caseSensitive: false, minKeys: 4 }] },
+
+    // desc
+    { code: 'var obj = {b:3, a:1, _:2} // desc', options: ['desc'] },
+    { code: 'var obj = {c:2, b:3, a:1}', options: ['desc'] },
+    { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc'] },
+    { code: 'var obj = {c:2, b_:1, C:3}', options: ['desc'] },
+    { code: 'var obj = {a:4, _:2, A:3, $:1}', options: ['desc'] },
+    { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ['desc'] },
+    { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc'] },
+
+    // desc, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {a:1, c:2, b:3}', options: ['desc', { minKeys: 4 }] },
+
+    // desc, insensitive
+    { code: 'var obj = {b:3, a:1, _:2} // desc, insensitive', options: ['desc', { caseSensitive: false }] },
+    { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { caseSensitive: false }] },
+    { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { caseSensitive: false }] },
+    { code: 'var obj = {c:2, C:3, b_:1}', options: ['desc', { caseSensitive: false }] },
+    { code: 'var obj = {C:2, c:3, b_:1}', options: ['desc', { caseSensitive: false }] },
+    { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { caseSensitive: false }] },
+    { code: "var obj = {A:3, 2:4, '11':2, 1:1}", options: ['desc', { caseSensitive: false }] },
+    { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { caseSensitive: false }] },
+
+    // desc, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {$:1, _:2, A:3, a:4}', options: ['desc', { caseSensitive: false, minKeys: 5 }] },
+
+    // desc, natural
+    { code: 'var obj = {b:3, a:1, _:2} // desc, natural', options: ['desc', { natural: true }] },
+    { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { natural: true }] },
+    { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { natural: true }] },
+    { code: 'var obj = {c:2, b_:1, C:3}', options: ['desc', { natural: true }] },
+    { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { natural: true }] },
+    { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ['desc', { natural: true }] },
+    { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { natural: true }] },
+
+    // desc, natural, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {b_:1, a:2, b:3}', options: ['desc', { natural: true, minKeys: 4 }] },
+
+    // desc, natural, insensitive
+    { code: 'var obj = {b:3, a:1, _:2} // desc, natural, insensitive', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {c:2, b:3, a:1}', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {b_:1, b:3, a:2}', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {c:2, C:3, b_:1}', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {C:2, c:3, b_:1}', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: 'var obj = {a:4, A:3, _:2, $:1}', options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: "var obj = {A:3, '11':2, 2:4, 1:1}", options: ['desc', { natural: true, caseSensitive: false }] },
+    { code: "var obj = {è:4, À:3, 'Z':2, '#':1}", options: ['desc', { natural: true, caseSensitive: false }] },
+
+    // desc, natural, insensitive, minKeys should ignore unsorted keys when number of keys is less than minKeys
+    { code: 'var obj = {a:1, _:2, b:3}', options: ['desc', { natural: true, caseSensitive: false, minKeys: 4 }] }
+  ],
+
+  invalid: [
+    // default (asc)
+    {
+      code: "var obj = {a:1, '':2} // default",
+      errors: ["Expected object keys to be in ascending order. '' should be before 'a'."]
+    },
+    {
+      code: 'var obj = {a:1, [``]:2} // default',
+      parserOptions: { ecmaVersion: 6 },
+      errors: ["Expected object keys to be in ascending order. '' should be before 'a'."]
+    },
+    {
+      code: 'var obj = {a:1, _:2, b:3} // default',
+      errors: ["Expected object keys to be in ascending order. '_' should be before 'a'."]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      errors: ["Expected object keys to be in ascending order. 'a' should be before 'b_'."]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      errors: ["Expected object keys to be in ascending order. 'C' should be before 'c'."]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      errors: ["Expected object keys to be in ascending order. 'A' should be before '_'."]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      errors: ["Expected object keys to be in ascending order. '11' should be before 'A'."]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      errors: ["Expected object keys to be in ascending order. 'Z' should be before 'À'."]
+    },
+
+    // not ignore properties not separated by spread properties
+    {
+      code: 'var obj = {...z, c:1, b:1}',
+      options: [],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."]
+    },
+    {
+      code: 'var obj = {...z, ...c, d:4, b:1, ...y, ...f, e:2, a:1}',
+      options: [],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: [
+        "Expected object keys to be in ascending order. 'b' should be before 'd'.",
+        "Expected object keys to be in ascending order. 'a' should be before 'e'."
+      ]
+    },
+    {
+      code: 'var obj = {c:1, b:1, ...a}',
+      options: [],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."]
+    },
+    {
+      code: 'var obj = {...z, ...a, c:1, b:1}',
+      options: [],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in ascending order. 'b' should be before 'c'."]
+    },
+    {
+      code: 'var obj = {...z, b:1, a:1, ...d, ...c}',
+      options: [],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in ascending order. 'a' should be before 'b'."]
+    },
+    {
+      code: 'var obj = {...z, a:2, b:0, ...x, ...c}',
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in descending order. 'b' should be before 'a'."]
+    },
+    {
+      code: 'var obj = {...z, a:2, b:0, ...x}',
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in descending order. 'b' should be before 'a'."]
+    },
+    {
+      code: "var obj = {...z, '':1, a:2}",
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 2018 },
+      errors: ["Expected object keys to be in descending order. 'a' should be before ''."]
+    },
+
+    // ignore non-simple computed properties, but their position shouldn't affect other comparisons.
+    {
+      code: "var obj = {a:1, [b+c]:2, '':3}",
+      parserOptions: { ecmaVersion: 6 },
+      errors: ["Expected object keys to be in ascending order. '' should be before 'a'."]
+    },
+    {
+      code: "var obj = {'':1, [b+c]:2, a:3}",
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 6 },
+      errors: ["Expected object keys to be in descending order. 'a' should be before ''."]
+    },
+    {
+      code: "var obj = {b:1, [f()]:2, '':3, a:4}",
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 6 },
+      errors: ["Expected object keys to be in descending order. 'a' should be before ''."]
+    },
+
+    // not ignore simple computed properties.
+    {
+      code: 'var obj = {a:1, b:3, [a]: -1, c:2}',
+      parserOptions: { ecmaVersion: 6 },
+      errors: ["Expected object keys to be in ascending order. 'a' should be before 'b'."]
+    },
+
+    // nested
+    {
+      code: 'var obj = {a:1, c:{y:1, x:1}, b:1}',
+      errors: [
+        "Expected object keys to be in ascending order. 'x' should be before 'y'.",
+        "Expected object keys to be in ascending order. 'b' should be before 'c'."
+      ]
+    },
+
+    // asc
+    {
+      code: 'var obj = {a:1, _:2, b:3} // asc',
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. '_' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. 'b' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. 'a' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. 'C' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. 'A' should be before '_'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. '11' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['asc'],
+      errors: [
+        "Expected object keys to be in ascending order. 'Z' should be before 'À'."
+      ]
+    },
+
+    // asc, minKeys should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['asc', { minKeys: 3 }],
+      errors: [
+        "Expected object keys to be in ascending order. '_' should be before 'a'."
+      ]
+    },
+
+    // asc, insensitive
+    {
+      code: 'var obj = {a:1, _:2, b:3} // asc, insensitive',
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. '_' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. 'b' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. 'a' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, A:3, _:2, a:4}',
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. '_' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. '11' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['asc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. 'Z' should be before 'À'."
+      ]
+    },
+
+    // asc, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['asc', { caseSensitive: false, minKeys: 3 }],
+      errors: [
+        "Expected object keys to be in insensitive ascending order. '_' should be before 'a'."
+      ]
+    },
+
+    // asc, natural
+    {
+      code: 'var obj = {a:1, _:2, b:3} // asc, natural',
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. '_' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. 'b' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. 'a' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. 'C' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, A:3, _:2, a:4}',
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. '_' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. '11' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['asc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural ascending order. 'Z' should be before 'À'."
+      ]
+    },
+
+    // asc, natural, minKeys should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['asc', { natural: true, minKeys: 2 }],
+      errors: [
+        "Expected object keys to be in natural ascending order. '_' should be before 'a'."
+      ]
+    },
+
+    // asc, natural, insensitive
+    {
+      code: 'var obj = {a:1, _:2, b:3} // asc, natural, insensitive',
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. '_' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. 'b' should be before 'c'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. 'a' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, A:3, _:2, a:4}',
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. '_' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, '11':2, 2:4, A:3}",
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. '2' should be before '11'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['asc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. 'Z' should be before 'À'."
+      ]
+    },
+
+    // asc, natural, insensitive, minKeys should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['asc', { natural: true, caseSensitive: false, minKeys: 3 }],
+      errors: [
+        "Expected object keys to be in natural insensitive ascending order. '_' should be before 'a'."
+      ]
+    },
+
+    // desc
+    {
+      code: "var obj = {'':1, a:'2'} // desc",
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'a' should be before ''."
+      ]
+    },
+    {
+      code: "var obj = {[``]:1, a:'2'} // desc",
+      options: ['desc'],
+      parserOptions: { ecmaVersion: 6 },
+      errors: [
+        "Expected object keys to be in descending order. 'a' should be before ''."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, _:2, b:3} // desc',
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'b' should be before '_'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'c' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'b' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'c' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. '_' should be before '$'.",
+        "Expected object keys to be in descending order. 'a' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. '2' should be before '1'.",
+        "Expected object keys to be in descending order. 'A' should be before '2'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['desc'],
+      errors: [
+        "Expected object keys to be in descending order. 'À' should be before '#'.",
+        "Expected object keys to be in descending order. 'è' should be before 'Z'."
+      ]
+    },
+
+    // desc, minKeys should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['desc', { minKeys: 3 }],
+      errors: [
+        "Expected object keys to be in descending order. 'b' should be before '_'."
+      ]
+    },
+
+    // desc, insensitive
+    {
+      code: 'var obj = {a:1, _:2, b:3} // desc, insensitive',
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'b' should be before '_'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'c' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'b' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'c' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. '_' should be before '$'.",
+        "Expected object keys to be in insensitive descending order. 'A' should be before '_'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. '2' should be before '1'.",
+        "Expected object keys to be in insensitive descending order. 'A' should be before '2'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['desc', { caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'À' should be before '#'.",
+        "Expected object keys to be in insensitive descending order. 'è' should be before 'Z'."
+      ]
+    },
+
+    // desc, insensitive should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['desc', { caseSensitive: false, minKeys: 2 }],
+      errors: [
+        "Expected object keys to be in insensitive descending order. 'b' should be before '_'."
+      ]
+    },
+
+    // desc, natural
+    {
+      code: 'var obj = {a:1, _:2, b:3} // desc, natural',
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'b' should be before '_'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'c' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'b' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'c' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. '_' should be before '$'.",
+        "Expected object keys to be in natural descending order. 'A' should be before '_'.",
+        "Expected object keys to be in natural descending order. 'a' should be before 'A'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, A:3, '11':2}",
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. '2' should be before '1'.",
+        "Expected object keys to be in natural descending order. 'A' should be before '2'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['desc', { natural: true }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'À' should be before '#'.",
+        "Expected object keys to be in natural descending order. 'è' should be before 'Z'."
+      ]
+    },
+
+    // desc, natural should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['desc', { natural: true, minKeys: 3 }],
+      errors: [
+        "Expected object keys to be in natural descending order. 'b' should be before '_'."
+      ]
+    },
+
+    // desc, natural, insensitive
+    {
+      code: 'var obj = {a:1, _:2, b:3} // desc, natural, insensitive',
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'b' should be before '_'."
+      ]
+    },
+    {
+      code: 'var obj = {a:1, c:2, b:3}',
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'c' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, a:2, b:3}',
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'b' should be before 'a'."
+      ]
+    },
+    {
+      code: 'var obj = {b_:1, c:2, C:3}',
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'c' should be before 'b_'."
+      ]
+    },
+    {
+      code: 'var obj = {$:1, _:2, A:3, a:4}',
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. '_' should be before '$'.",
+        "Expected object keys to be in natural insensitive descending order. 'A' should be before '_'."
+      ]
+    },
+    {
+      code: "var obj = {1:1, 2:4, '11':2, A:3}",
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. '2' should be before '1'.",
+        "Expected object keys to be in natural insensitive descending order. '11' should be before '2'.",
+        "Expected object keys to be in natural insensitive descending order. 'A' should be before '11'."
+      ]
+    },
+    {
+      code: "var obj = {'#':1, À:3, 'Z':2, è:4}",
+      options: ['desc', { natural: true, caseSensitive: false }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'À' should be before '#'.",
+        "Expected object keys to be in natural insensitive descending order. 'è' should be before 'Z'."
+      ]
+    },
+
+    // desc, natural, insensitive should error when number of keys is greater than or equal to minKeys
+    {
+      code: 'var obj = {a:1, _:2, b:3}',
+      options: ['desc', { natural: true, caseSensitive: false, minKeys: 2 }],
+      errors: [
+        "Expected object keys to be in natural insensitive descending order. 'b' should be before '_'."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          name: 'app',
+          props: {
+            z: Number,
+            propA: Number,
+          },
+          ...a,
+          data () {
+            return {
+              zd: 1,
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+        }
+      `,
+      parserOptions,
+
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'propA\' should be before \'z\'.',
+        line: 6
+      }, {
+        message: 'Expected object keys to be in ascending order. \'msg\' should be before \'zd\'.',
+        line: 12
+      }]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+            return {
+              isActive: false,
+            };
+          },
+          methods: {
+            toggleMenu() {
+              this.isActive = !this.isActive;
+            },
+            closeMenu() {
+              this.isActive = false;
+            }
+          },
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'closeMenu\' should be before \'toggleMenu\'.',
+        line: 12
+      }]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+        export default {
+          methods: {
+            toggleMenu() {
+              return {
+                // These should have errors since they are not part of the vue component
+                model: {
+                  prop: 'checked',
+                  event: 'change'
+                },
+                props: {
+                  propA: {
+                    z: 1,
+                    a: 2
+                  },
+                },
+              };
+            }
+          },
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'event\' should be before \'prop\'.',
+        line: 9
+      }, {
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'z\'.',
+        line: 14
+      }]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+        const dict = { zd: 1, a: 2 };
+
+        export default {
+          data() {
+            return {
+              z: 2,
+              isActive: false,
+            };
+          },
+          name: 'burger',
+        };
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.',
+        line: 2
+      }, {
+        message: 'Expected object keys to be in ascending order. \'isActive\' should be before \'z\'.',
+        line: 8
+      }]
+    },
+    {
+      filename: 'example.vue',
+      code: `
+        export default {
+          data() {
+            return {
+              z: 2,
+              isActive: false,
+            };
+          },
+          name: 'burger',
+        };
+
+        const dict = { zd: 1, a: 2 };
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'isActive\' should be before \'z\'.',
+        line: 6
+      }, {
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.',
+        line: 12
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const dict = { zd: 1, a: 2 };
+        new Vue({
+          name: 'app',
+          el: '#app',
+          data () {
+            return {
+              z: 2,
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          components: {},
+          template: '<div></div>'
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.',
+        line: 2
+      },
+      {
+        message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const dict = { zd: 1, a: 2 };
+
+        Vue.component('smart-list', {
+          name: 'app',
+          data () {
+            return {
+              z: 2,
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          components: {},
+          template: '<div></div>'
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.',
+        line: 2
+      },
+      {
+        message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'test.js',
+      code: `
+        const dict = { zd: 1, a: 2 };
+        const { component } = Vue;
+        component('smart-list', {
+          name: 'app',
+          data () {
+            return {
+              z: 2,
+              msg: 'Welcome to Your Vue.js App'
+            }
+          },
+          components: {},
+          template: '<div></div>'
+        })
+      `,
+      parserOptions: { ecmaVersion: 6 },
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'zd\'.',
+        line: 2
+      },
+      {
+        message: 'Expected object keys to be in ascending order. \'msg\' should be before \'z\'.',
+        line: 9
+      }]
+    },
+    {
+      filename: 'propsOrder.vue',
+      code: `
+        export default {
+          name: 'app',
+          props: {
+            propA: {
+              type: String,
+              default: 'abc',
+            },
+          },
+        }
+      `,
+      options: ['asc', { ignoreGrandchildrenOf: [] }],
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'default\' should be before \'type\'.',
+        line: 7
+      }]
+    },
+    {
+      filename: 'propsOrder.vue',
+      code: `
+        const obj = {
+          z: 1,
+          foo() {
+            Vue.component('my-component', {
+              name: 'app',
+              data: {}
+            })
+          },
+          a: 2
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'foo\' should be before \'z\'.',
+        line: 4
+      }, {
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'foo\'.',
+        line: 10
+      }]
+    },
+    {
+      filename: 'propsOrder.vue',
+      code: `
+        export default {
+          computed: {
+            foo () {
+              return {
+                b,
+                a
+              }
+            }
+          }
+        }
+      `,
+      parserOptions,
+      errors: [{
+        message: 'Expected object keys to be in ascending order. \'a\' should be before \'b\'.',
+        line: 7
+      }]
+    }
+  ]
+})