Skip to content

Commit b394ca6

Browse files
authored
New: Add vue/padding-line-between-blocks rule (#1021)
1 parent 7608dea commit b394ca6

7 files changed

+651
-1
lines changed

docs/rules/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ For example:
165165
| [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes | |
166166
| [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
167167
| [vue/object-curly-spacing](./object-curly-spacing.md) | enforce consistent spacing inside braces | :wrench: |
168+
| [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: |
168169
| [vue/require-direct-export](./require-direct-export.md) | require the component to be directly exported | |
169170
| [vue/require-name-property](./require-name-property.md) | require a name property in Vue components | |
170171
| [vue/script-indent](./script-indent.md) | enforce consistent indentation in `<script>` | :wrench: |
+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/padding-line-between-blocks
5+
description: require or disallow padding lines between blocks
6+
---
7+
# vue/padding-line-between-blocks
8+
> require or disallow padding lines between blocks
9+
10+
- :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.
11+
12+
## :book: Rule Details
13+
14+
This rule requires or disallows blank lines between the given 2 blocks. Properly blank lines help developers to understand the code.
15+
16+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
17+
18+
```vue
19+
<!-- ✓ GOOD -->
20+
<template>
21+
<div></div>
22+
</template>
23+
24+
<script>
25+
export default {}
26+
</script>
27+
28+
<style></style>
29+
```
30+
31+
</eslint-code-block>
32+
33+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
34+
35+
```vue
36+
<!-- ✗ BAD -->
37+
<template>
38+
<div></div>
39+
</template>
40+
<script>
41+
export default {}
42+
</script>
43+
<style></style>
44+
```
45+
46+
</eslint-code-block>
47+
48+
## :wrench: Options
49+
50+
```json
51+
{
52+
"vue/padding-line-between-blocks": ["error", "always" | "never"]
53+
}
54+
```
55+
56+
- `"always"` (default) ... Requires one or more blank lines. Note it does not count lines that comments exist as blank lines.
57+
- `"never"` ... Disallows blank lines.
58+
59+
### `"always"` (default)
60+
61+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
62+
63+
```vue
64+
<!-- ✓ GOOD -->
65+
<template>
66+
<div></div>
67+
</template>
68+
69+
<script>
70+
export default {}
71+
</script>
72+
73+
<style></style>
74+
```
75+
76+
</eslint-code-block>
77+
78+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error']}">
79+
80+
```vue
81+
<!-- ✗ BAD -->
82+
<template>
83+
<div></div>
84+
</template>
85+
<script>
86+
export default {}
87+
</script>
88+
<style></style>
89+
```
90+
91+
</eslint-code-block>
92+
93+
### `"never"`
94+
95+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error', 'never']}">
96+
97+
```vue
98+
<!-- ✓ GOOD -->
99+
<template>
100+
<div></div>
101+
</template>
102+
<script>
103+
export default {}
104+
</script>
105+
<style></style>
106+
```
107+
108+
</eslint-code-block>
109+
110+
<eslint-code-block fix :rules="{'vue/padding-line-between-blocks': ['error', 'never']}">
111+
112+
```vue
113+
<!-- ✗ BAD -->
114+
<template>
115+
<div></div>
116+
</template>
117+
118+
<script>
119+
export default {}
120+
</script>
121+
122+
<style></style>
123+
```
124+
125+
</eslint-code-block>
126+
127+
## :books: Further reading
128+
129+
- [padding-line-between-statements]
130+
- [lines-between-class-members]
131+
132+
[padding-line-between-statements]: https://eslint.org/docs/rules/padding-line-between-statements
133+
[lines-between-class-members]: https://eslint.org/docs/rules/lines-between-class-members
134+
135+
136+
## :mag: Implementation
137+
138+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/padding-line-between-blocks.js)
139+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/padding-line-between-blocks.js)

lib/configs/no-layout-rules.js

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module.exports = {
2525
'vue/no-multi-spaces': 'off',
2626
'vue/no-spaces-around-equal-signs-in-attribute': 'off',
2727
'vue/object-curly-spacing': 'off',
28+
'vue/padding-line-between-blocks': 'off',
2829
'vue/script-indent': 'off',
2930
'vue/singleline-html-element-content-newline': 'off',
3031
'vue/space-infix-ops': 'off',

lib/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ module.exports = {
6565
'no-v-html': require('./rules/no-v-html'),
6666
'object-curly-spacing': require('./rules/object-curly-spacing'),
6767
'order-in-components': require('./rules/order-in-components'),
68+
'padding-line-between-blocks': require('./rules/padding-line-between-blocks'),
6869
'prop-name-casing': require('./rules/prop-name-casing'),
6970
'require-component-is': require('./rules/require-component-is'),
7071
'require-default-prop': require('./rules/require-default-prop'),

lib/rules/component-tags-order.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ module.exports = {
4545

4646
function getTopLevelHTMLElements () {
4747
if (documentFragment) {
48-
return documentFragment.children
48+
return documentFragment.children.filter(e => e.type === 'VElement')
4949
}
5050
return []
5151
}
+194
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/**
2+
* @fileoverview Require or disallow padding lines between blocks
3+
* @author Yosuke Ota
4+
*/
5+
'use strict'
6+
const utils = require('../utils')
7+
8+
/**
9+
* Split the source code into multiple lines based on the line delimiters.
10+
* @param {string} text Source code as a string.
11+
* @returns {string[]} Array of source code lines.
12+
*/
13+
function splitLines (text) {
14+
return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
15+
}
16+
17+
/**
18+
* Check and report blocks for `never` configuration.
19+
* This autofix removes blank lines between the given 2 blocks.
20+
* @param {RuleContext} context The rule context to report.
21+
* @param {VElement} prevBlock The previous block to check.
22+
* @param {VElement} nextBlock The next block to check.
23+
* @param {Token[]} betweenTokens The array of tokens between blocks.
24+
* @returns {void}
25+
* @private
26+
*/
27+
function verifyForNever (context, prevBlock, nextBlock, betweenTokens) {
28+
if (prevBlock.loc.end.line === nextBlock.loc.start.line) {
29+
// same line
30+
return
31+
}
32+
const tokenOrNodes = [...betweenTokens, nextBlock]
33+
let prev = prevBlock
34+
const paddingLines = []
35+
for (const tokenOrNode of tokenOrNodes) {
36+
const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
37+
if (numOfLineBreaks > 1) {
38+
paddingLines.push([prev, tokenOrNode])
39+
}
40+
prev = tokenOrNode
41+
}
42+
if (!paddingLines.length) {
43+
return
44+
}
45+
46+
context.report({
47+
node: nextBlock,
48+
messageId: 'never',
49+
fix (fixer) {
50+
return paddingLines.map(([prevToken, nextToken]) => {
51+
const start = prevToken.range[1]
52+
const end = nextToken.range[0]
53+
const paddingText = context.getSourceCode().text
54+
.slice(start, end)
55+
const lastSpaces = splitLines(paddingText).pop()
56+
return fixer.replaceTextRange([start, end], '\n' + lastSpaces)
57+
})
58+
}
59+
})
60+
}
61+
62+
/**
63+
* Check and report blocks for `always` configuration.
64+
* This autofix inserts a blank line between the given 2 blocks.
65+
* @param {RuleContext} context The rule context to report.
66+
* @param {VElement} prevBlock The previous block to check.
67+
* @param {VElement} nextBlock The next block to check.
68+
* @param {Token[]} betweenTokens The array of tokens between blocks.
69+
* @returns {void}
70+
* @private
71+
*/
72+
function verifyForAlways (context, prevBlock, nextBlock, betweenTokens) {
73+
const tokenOrNodes = [...betweenTokens, nextBlock]
74+
let prev = prevBlock
75+
let linebreak
76+
for (const tokenOrNode of tokenOrNodes) {
77+
const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
78+
if (numOfLineBreaks > 1) {
79+
// Already padded.
80+
return
81+
}
82+
if (!linebreak && numOfLineBreaks > 0) {
83+
linebreak = prev
84+
}
85+
prev = tokenOrNode
86+
}
87+
88+
context.report({
89+
node: nextBlock,
90+
messageId: 'always',
91+
fix (fixer) {
92+
if (linebreak) {
93+
return fixer.insertTextAfter(linebreak, '\n')
94+
}
95+
return fixer.insertTextAfter(prevBlock, '\n\n')
96+
}
97+
})
98+
}
99+
100+
/**
101+
* Types of blank lines.
102+
* `never` and `always` are defined.
103+
* Those have `verify` method to check and report statements.
104+
* @private
105+
*/
106+
const PaddingTypes = {
107+
never: { verify: verifyForNever },
108+
always: { verify: verifyForAlways }
109+
}
110+
111+
// ------------------------------------------------------------------------------
112+
// Rule Definition
113+
// ------------------------------------------------------------------------------
114+
115+
module.exports = {
116+
meta: {
117+
type: 'layout',
118+
docs: {
119+
description: 'require or disallow padding lines between blocks',
120+
category: undefined,
121+
url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html'
122+
},
123+
fixable: 'whitespace',
124+
schema: [
125+
{
126+
enum: Object.keys(PaddingTypes)
127+
}
128+
],
129+
messages: {
130+
never: 'Unexpected blank line before this block.',
131+
always: 'Expected blank line before this block.'
132+
}
133+
},
134+
create (context) {
135+
const paddingType = PaddingTypes[context.options[0] || 'always']
136+
const documentFragment = context.parserServices.getDocumentFragment && context.parserServices.getDocumentFragment()
137+
138+
let tokens
139+
function getTopLevelHTMLElements () {
140+
if (documentFragment) {
141+
return documentFragment.children.filter(e => e.type === 'VElement')
142+
}
143+
return []
144+
}
145+
146+
function getTokenAndCommentsBetween (prev, next) {
147+
// When there is no <template>, tokenStore.getTokensBetween cannot be used.
148+
if (!tokens) {
149+
tokens = [
150+
...documentFragment.tokens
151+
.filter(token => token.type !== 'HTMLWhitespace'),
152+
...documentFragment.comments
153+
].sort((a, b) => a.range[0] > b.range[0] ? 1 : a.range[0] < b.range[0] ? -1 : 0)
154+
}
155+
156+
let token = tokens.shift()
157+
158+
const results = []
159+
while (token) {
160+
if (prev.range[1] <= token.range[0]) {
161+
if (next.range[0] <= token.range[0]) {
162+
tokens.unshift(token)
163+
break
164+
} else {
165+
results.push(token)
166+
}
167+
}
168+
token = tokens.shift()
169+
}
170+
171+
return results
172+
}
173+
174+
return utils.defineTemplateBodyVisitor(
175+
context,
176+
{},
177+
{
178+
Program (node) {
179+
if (utils.hasInvalidEOF(node)) {
180+
return
181+
}
182+
const elements = [...getTopLevelHTMLElements()]
183+
184+
let prev = elements.shift()
185+
for (const element of elements) {
186+
const betweenTokens = getTokenAndCommentsBetween(prev, element)
187+
paddingType.verify(context, prev, element, betweenTokens)
188+
prev = element
189+
}
190+
}
191+
}
192+
)
193+
}
194+
}

0 commit comments

Comments
 (0)