diff --git a/docs/rules/README.md b/docs/rules/README.md
index 30c0f93d1..22617d2e6 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -165,6 +165,7 @@ 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 `
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+```
+
+
+
+## :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)
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+```
+
+
+
+### `"never"`
+
+
+
+```vue
+
+
+
+
+
+
+```
+
+
+
+
+
+```vue
+
+
+
+
+
+
+
+
+```
+
+
+
+## :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/lib/configs/no-layout-rules.js b/lib/configs/no-layout-rules.js
index 3bf2fec4e..e45c080bc 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 d1b9bd9ef..eda1d2b74 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'),
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index d6400dec0..7129aa36e 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 000000000..6f08a5d7a
--- /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 , 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/tests/lib/rules/padding-line-between-blocks.js b/tests/lib/rules/padding-line-between-blocks.js
new file mode 100644
index 000000000..f81b2bbf7
--- /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: [
+ `
+
+
+
+
+
+ `,
+ `
+
+
+
+ `,
+ {
+ code: `
+
+
+
+ `,
+ options: ['never']
+ },
+ // comments
+ `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ {
+ code: `
+
+
+
+
+
+
+ `,
+ options: ['never']
+ },
+ // same line
+ {
+ code: ``,
+ options: ['never']
+ },
+ {
+ code: `
+
+ `,
+ options: ['never']
+ },
+ // no template
+ `
+
+
+
+ `,
+ {
+ code: `
+
+
+ `,
+ options: ['never']
+ }
+ ],
+ invalid: [
+ {
+ code: `
+
+
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ 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: `
+
+
+
+
+
+ `,
+ options: ['never'],
+ output: `
+
+
+
+ `,
+ 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: `
+
+ `,
+ output: `
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Expected blank line before this block.',
+ line: 2
+ },
+ {
+ message: 'Expected blank line before this block.',
+ line: 2
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+
+ `,
+ output: `
+
+
+
+
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Expected blank line before this block.',
+ line: 4
+ },
+ {
+ message: 'Expected blank line before this block.',
+ line: 7
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ options: ['never'],
+ output: `
+
+
+
+
+
+
+
+
+ `,
+ 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: `
+ TEXT
+
+ `,
+ output: `
+ TEXT
+
+
+
+
+ `,
+ errors: [
+ {
+ message: 'Expected blank line before this block.',
+ line: 3
+ },
+ {
+ message: 'Expected blank line before this block.',
+ line: 4
+ }
+ ]
+ },
+ {
+ code: `
+
+
+
+
+
+ TEXT TEXT
+
+
+
+
+
+
+ TEXT
+
+
+
+ TEXT
+
+
+ `,
+ options: ['never'],
+ output: `
+
+
+ TEXT TEXT
+
+
+ TEXT
+ TEXT
+
+ `,
+ errors: [
+ {
+ message: 'Unexpected blank line before this block.',
+ line: 23
+ }
+ ]
+ }
+ ]
+})