Skip to content

Commit 0c0ef9a

Browse files
author
barthy
committed
added rule/empty-line-between-options
1 parent 8f09420 commit 0c0ef9a

File tree

6 files changed

+360
-0
lines changed

6 files changed

+360
-0
lines changed

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ For example:
313313
| [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: |
314314
| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: |
315315
| [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | |
316+
| [vue/empty-line-between-options](./empty-line-between-options.md) | enforce empty lines between top-level options | :wrench: |
316317
| [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | |
317318
| [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: |
318319
| [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: |
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
pageClass: rule-details
3+
sidebarDepth: 0
4+
title: vue/empty-line-between-options
5+
description: enforce empty lines between top-level options
6+
---
7+
# vue/empty-line-between-options
8+
9+
> enforce empty lines between top-level options
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
- :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.
13+
14+
## :book: Rule Details
15+
16+
This rule ....
17+
18+
<eslint-code-block fix :rules="{'vue/empty-line-between-options': ['error']}">
19+
20+
```vue
21+
<template>
22+
23+
</template>
24+
```
25+
26+
</eslint-code-block>
27+
28+
## :wrench: Options
29+
30+
Nothing.
31+
32+
## :mag: Implementation
33+
34+
- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/empty-line-between-options.js)
35+
- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/empty-line-between-options.js)

lib/configs/no-layout-rules.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module.exports = {
1515
'vue/comma-spacing': 'off',
1616
'vue/comma-style': 'off',
1717
'vue/dot-location': 'off',
18+
'vue/empty-line-between-options': 'off',
1819
'vue/first-attribute-linebreak': 'off',
1920
'vue/func-call-spacing': 'off',
2021
'vue/html-closing-bracket-newline': 'off',

lib/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module.exports = {
2929
'custom-event-name-casing': require('./rules/custom-event-name-casing'),
3030
'dot-location': require('./rules/dot-location'),
3131
'dot-notation': require('./rules/dot-notation'),
32+
'empty-line-between-options': require('./rules/empty-line-between-options'),
3233
eqeqeq: require('./rules/eqeqeq'),
3334
'experimental-script-setup-vars': require('./rules/experimental-script-setup-vars'),
3435
'first-attribute-linebreak': require('./rules/first-attribute-linebreak'),
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
/**
2+
* @author Barthy Bonhomme <[email protected]> <github.com/barthy-koeln>
3+
* @author Sergio Arbeo <[email protected]>
4+
* Adapted from https://github.com/DockYard/eslint-plugin-ember-suave/blob/master/lib/rules/lines-between-object-properties.js
5+
*
6+
* See LICENSE file in root directory for full license.
7+
*/
8+
'use strict'
9+
10+
// ------------------------------------------------------------------------------
11+
// Requirements
12+
// ------------------------------------------------------------------------------
13+
14+
const path = require('path')
15+
const utils = require('../utils')
16+
17+
// ------------------------------------------------------------------------------
18+
// Helpers
19+
// ------------------------------------------------------------------------------
20+
21+
// ...
22+
23+
// ------------------------------------------------------------------------------
24+
// Rule Definition
25+
// ------------------------------------------------------------------------------
26+
27+
module.exports = {
28+
meta: {
29+
type: 'layout',
30+
docs: {
31+
description: 'enforce empty lines between top-level options',
32+
categories: undefined,
33+
url: 'https://eslint.vuejs.org/rules/empty-line-between-options.html'
34+
},
35+
fixable: 'whitespace',
36+
schema: [{ enum: ['always', 'never'] }],
37+
messages: {
38+
never: 'Unexpected blank line between Vue component options.',
39+
always: 'Expected blank line between Vue component options.'
40+
}
41+
},
42+
/** @param {RuleContext} context */
43+
create(context) {
44+
const isVueFile = utils.isVueFile(context.getFilename())
45+
if (!isVueFile) {
46+
return {}
47+
}
48+
49+
const sourceCode = context.getSourceCode()
50+
const shouldPad = (context.options[0] || 'always') === 'always'
51+
52+
const fixFunctions = {
53+
/**
54+
* Removes newlines between component options
55+
* @param {RuleFixer} fixer
56+
* @param {Token} currentLast
57+
* @param {Token} nextFirst
58+
* @return {Fix}
59+
*/
60+
never(fixer, currentLast, nextFirst) {
61+
return fixer.replaceTextRange(
62+
[currentLast.range[1], nextFirst.range[0]],
63+
',\n'
64+
)
65+
},
66+
/**
67+
* Add newlines between component options
68+
* @param {RuleFixer} fixer
69+
* @param {Token} currentLast
70+
* @return {Fix}
71+
*/
72+
always(fixer, currentLast /*, nextFirst*/) {
73+
const tokenAfterLastToken = sourceCode.getTokenAfter(currentLast)
74+
const tokenToLineBreakAfter =
75+
tokenAfterLastToken.value === ',' ? tokenAfterLastToken : currentLast
76+
77+
return fixer.insertTextAfter(tokenToLineBreakAfter, '\n')
78+
}
79+
}
80+
81+
/**
82+
* Checks if there is an empty line between two tokens
83+
* @param {Token} first The first token
84+
* @param {Token} second The second token
85+
* @returns {boolean} True if there is at least a line between the tokens
86+
*/
87+
function isPaddingBetweenTokens(first, second) {
88+
const comments = sourceCode.getCommentsBefore(second)
89+
const len = comments.length
90+
91+
// If there is no comments
92+
if (len === 0) {
93+
const linesBetweenFstAndSnd =
94+
second.loc.start.line - first.loc.end.line - 1
95+
96+
return linesBetweenFstAndSnd >= 1
97+
}
98+
99+
// If there are comments
100+
let sumOfCommentLines = 0 // the numbers of lines of comments
101+
let prevCommentLineNum = -1 // line number of the end of the previous comment
102+
103+
for (let i = 0; i < len; i++) {
104+
const commentLinesOfThisComment =
105+
comments[i].loc.end.line - comments[i].loc.start.line + 1
106+
107+
sumOfCommentLines += commentLinesOfThisComment
108+
109+
/*
110+
* If this comment and the previous comment are in the same line,
111+
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
112+
*/
113+
if (prevCommentLineNum === comments[i].loc.start.line) {
114+
sumOfCommentLines -= 1
115+
}
116+
117+
prevCommentLineNum = comments[i].loc.end.line
118+
}
119+
120+
/*
121+
* If the first block and the first comment are in the same line,
122+
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
123+
*/
124+
if (first.loc.end.line === comments[0].loc.start.line) {
125+
sumOfCommentLines -= 1
126+
}
127+
128+
/*
129+
* If the last comment and the second block are in the same line,
130+
* the count of comment lines is duplicated. So decrement sumOfCommentLines.
131+
*/
132+
if (comments[len - 1].loc.end.line === second.loc.start.line) {
133+
sumOfCommentLines -= 1
134+
}
135+
136+
const linesBetweenFstAndSnd =
137+
second.loc.start.line - first.loc.end.line - 1
138+
139+
return linesBetweenFstAndSnd - sumOfCommentLines >= 1
140+
}
141+
142+
/**
143+
* Report error based on configuration
144+
* @param {ASTNode} node Where to report errors
145+
* @param {boolean} isPadded True if the option is followed by an empty line
146+
* @param {Token} currentLast End of checked token
147+
* @param {Token} nextFirst Start of next token
148+
*/
149+
function reportError(node, isPadded, currentLast, nextFirst) {
150+
const key = isPadded ? 'never' : 'always'
151+
const fixFunction = fixFunctions[key]
152+
153+
context.report({
154+
node,
155+
messageId: key,
156+
fix: (fixer) => fixFunction(fixer, currentLast, nextFirst)
157+
})
158+
}
159+
160+
/**
161+
* Compares options and decides what to do
162+
* @param {ASTNode} option current option to check
163+
* @param {ASTNode} nextNode next node to check against
164+
*/
165+
function checkOption(option, nextNode) {
166+
const currentLast = sourceCode.getLastToken(option)
167+
const nextFirst = sourceCode.getFirstToken(nextNode)
168+
const isPadded = isPaddingBetweenTokens(currentLast, nextFirst)
169+
170+
if (shouldPad === isPadded) {
171+
return
172+
}
173+
174+
reportError(nextNode, isPadded, currentLast, nextFirst)
175+
}
176+
177+
return {
178+
/**
179+
* @param {import('vue-eslint-parser/ast').Node} node
180+
*/
181+
ObjectExpression(node) {
182+
if (node.parent && node.parent.type !== 'ExportDefaultDeclaration') {
183+
return
184+
}
185+
186+
const { properties } = node
187+
188+
for (let i = 0; i < properties.length - 1; i++) {
189+
const property = properties[i]
190+
const nextProperty = properties[i + 1]
191+
192+
checkOption(property, nextProperty)
193+
}
194+
}
195+
}
196+
}
197+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* @author Barthy Bonhomme <[email protected]> (https://github.com/barthy-koeln)
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const RuleTester = require('eslint').RuleTester
8+
const rule = require('../../../lib/rules/empty-line-between-options')
9+
10+
const tester = new RuleTester({
11+
parser: require.resolve('vue-eslint-parser'),
12+
parserOptions: {
13+
ecmaVersion: 2020,
14+
sourceType: 'module'
15+
}
16+
})
17+
18+
tester.run('empty-line-between-options', rule, {
19+
valid: [
20+
{
21+
filename: 'test.vue',
22+
code: `
23+
<template>
24+
25+
</template>
26+
27+
<script>
28+
export default {
29+
name: 'AComponentOfSorts',
30+
31+
/**
32+
* @return {Object}
33+
*/
34+
data(){
35+
return {}
36+
},
37+
38+
/* red */ i18n: {},
39+
40+
config: {} /* green */,
41+
42+
/* who */ /* writes */
43+
/* comments like this */
44+
computed: {}
45+
}
46+
</script>
47+
`
48+
}
49+
],
50+
invalid: [
51+
{
52+
filename: 'test.vue',
53+
code: `
54+
<template>
55+
56+
</template>
57+
58+
<script>
59+
export default {
60+
name: 'AComponentOfSorts',
61+
/**
62+
* @return {Object}
63+
*/
64+
data(){
65+
return {}
66+
},
67+
/* red */ i18n: {},
68+
config: {} /* green */,
69+
/* who */ /* writes */
70+
/* comments like this */
71+
computed: {}
72+
}
73+
</script>
74+
`,
75+
output: `
76+
<template>
77+
78+
</template>
79+
80+
<script>
81+
export default {
82+
name: 'AComponentOfSorts',
83+
84+
/**
85+
* @return {Object}
86+
*/
87+
data(){
88+
return {}
89+
},
90+
91+
/* red */ i18n: {},
92+
93+
config: {} /* green */,
94+
95+
/* who */ /* writes */
96+
/* comments like this */
97+
computed: {}
98+
}
99+
</script>
100+
`,
101+
errors: [
102+
{
103+
message: 'Expected blank line between Vue component options.',
104+
line: 12,
105+
column: 9
106+
},
107+
{
108+
message: 'Expected blank line between Vue component options.',
109+
line: 15,
110+
column: 19
111+
},
112+
{
113+
message: 'Expected blank line between Vue component options.',
114+
line: 16,
115+
column: 9
116+
},
117+
{
118+
message: 'Expected blank line between Vue component options.',
119+
line: 19,
120+
column: 9
121+
}
122+
]
123+
}
124+
]
125+
})

0 commit comments

Comments
 (0)