Skip to content

Commit 7faf1a4

Browse files
mysticateamichalsnik
authored andcommitted
New: html-closing-bracket-spacing rule (fixes #229) (#312)
* New: html-closing-bracket-spacing rule (fixes #229) * fix description * add the check of columns * add autofix check
1 parent cdd4163 commit 7faf1a4

File tree

4 files changed

+292
-0
lines changed

4 files changed

+292
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
183183

184184
| | Rule ID | Description |
185185
|:---|:--------|:------------|
186+
| :wrench: | [html-closing-bracket-spacing](./docs/rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets |
186187
| :wrench: | [html-closing-bracket-newline](./docs/rules/html-closing-bracket-newline.md) | require or disallow a line break before tag's closing brackets |
187188

188189
<!--RULES_TABLE_END-->
+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# require or disallow a space before tag's closing brackets (html-closing-bracket-spacing)
2+
3+
- :wrench: The `--fix` option on the [command line](http://eslint.org/docs/user-guide/command-line-interface#fix) can automatically fix some of the problems reported by this rule.
4+
5+
This rule enforces consistent spacing style before closing brackets `>` of tags.
6+
7+
```html
8+
<div class="foo"> or <div class="foo" >
9+
<div class="foo"/> or <div class="foo" />
10+
```
11+
12+
## Rule Details
13+
14+
This rule has options.
15+
16+
```json
17+
{
18+
"html-closing-bracket-spacing": ["error", {
19+
"startTag": "always" | "never",
20+
"endTag": "always" | "never",
21+
"selfClosingTag": "always" | "never"
22+
}]
23+
}
24+
```
25+
26+
- `startTag` (`"always" | "never"`) ... Setting for the `>` of start tags (e.g. `<div>`). Default is `"never"`.
27+
- `"always"` ... requires one or more spaces.
28+
- `"never"` ... disallows spaces.
29+
- `endTag` (`"always" | "never"`) ... Setting for the `>` of end tags (e.g. `</div>`). Default is `"never"`.
30+
- `"always"` ... requires one or more spaces.
31+
- `"never"` ... disallows spaces.
32+
- `selfClosingTag` (`"always" | "never"`) ... Setting for the `/>` of self-closing tags (e.g. `<div/>`). Default is `"always"`.
33+
- `"always"` ... requires one or more spaces.
34+
- `"never"` ... disallows spaces.
35+
36+
Examples of **incorrect** code for this rule:
37+
38+
```html
39+
<!--eslint html-closing-bracket-spacing: "error" -->
40+
41+
<div >
42+
<div foo >
43+
<div foo="bar" >
44+
</div >
45+
<div/>
46+
<div foo/>
47+
<div foo="bar"/>
48+
```
49+
50+
Examples of **correct** code for this rule:
51+
52+
```html
53+
<!--eslint html-closing-bracket-spacing: "error" -->
54+
55+
<div>
56+
<div foo>
57+
<div foo="bar">
58+
</div>
59+
<div />
60+
<div foo />
61+
<div foo="bar" />
62+
```
63+
64+
```html
65+
<!--eslint html-closing-bracket-spacing: ["error", {
66+
"startTag": "always",
67+
"endTag": "always",
68+
"selfClosingTag": "always"
69+
}] -->
70+
71+
<div >
72+
<div foo >
73+
<div foo="bar" >
74+
</div >
75+
<div />
76+
<div foo />
77+
<div foo="bar" />
78+
```
79+
80+
# Related rules
81+
82+
- [vue/no-multi-spaces](./no-multi-spaces.md)
83+
- [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md)
+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/**
2+
* @author Toru Nagashima <https://github.com/mysticatea>
3+
*/
4+
5+
'use strict'
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
const utils = require('../utils')
12+
13+
// -----------------------------------------------------------------------------
14+
// Helpers
15+
// -----------------------------------------------------------------------------
16+
17+
/**
18+
* Normalize options.
19+
* @param {{startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"}} options The options user configured.
20+
* @param {TokenStore} tokens The token store of template body.
21+
* @returns {{startTag:"always"|"never",endTag:"always"|"never",selfClosingTag:"always"|"never"}} The normalized options.
22+
*/
23+
function parseOptions (options, tokens) {
24+
return Object.assign({
25+
startTag: 'never',
26+
endTag: 'never',
27+
selfClosingTag: 'always',
28+
29+
detectType (node) {
30+
const openType = tokens.getFirstToken(node).type
31+
const closeType = tokens.getLastToken(node).type
32+
33+
if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
34+
return this.endTag
35+
}
36+
if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
37+
return this.startTag
38+
}
39+
if (openType === 'HTMLTagOpen' && closeType === 'HTMLSelfClosingTagClose') {
40+
return this.selfClosingTag
41+
}
42+
return null
43+
}
44+
}, options)
45+
}
46+
47+
// -----------------------------------------------------------------------------
48+
// Rule Definition
49+
// -----------------------------------------------------------------------------
50+
51+
module.exports = {
52+
meta: {
53+
docs: {
54+
description: 'require or disallow a space before tag\'s closing brackets',
55+
category: undefined
56+
},
57+
schema: [{
58+
type: 'object',
59+
properties: {
60+
startTag: { enum: ['always', 'never'] },
61+
endTag: { enum: ['always', 'never'] },
62+
selfClosingTag: { enum: ['always', 'never'] }
63+
},
64+
additionalProperties: false
65+
}],
66+
fixable: 'whitespace'
67+
},
68+
69+
create (context) {
70+
const sourceCode = context.getSourceCode()
71+
const tokens =
72+
context.parserServices.getTemplateBodyTokenStore &&
73+
context.parserServices.getTemplateBodyTokenStore()
74+
const options = parseOptions(context.options[0], tokens)
75+
76+
return utils.defineTemplateBodyVisitor(context, {
77+
'VStartTag, VEndTag' (node) {
78+
const type = options.detectType(node)
79+
const lastToken = tokens.getLastToken(node)
80+
const prevToken = tokens.getLastToken(node, 1)
81+
82+
// Skip if EOF exists in the tag or linebreak exists before `>`.
83+
if (type == null || prevToken == null || prevToken.loc.end.line !== lastToken.loc.start.line) {
84+
return
85+
}
86+
87+
// Check and report.
88+
const hasSpace = (prevToken.range[1] !== lastToken.range[0])
89+
if (type === 'always' && !hasSpace) {
90+
context.report({
91+
node,
92+
loc: lastToken.loc,
93+
message: "Expected a space before '{{bracket}}', but not found.",
94+
data: { bracket: sourceCode.getText(lastToken) },
95+
fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
96+
})
97+
} else if (type === 'never' && hasSpace) {
98+
context.report({
99+
node,
100+
loc: {
101+
start: prevToken.loc.end,
102+
end: lastToken.loc.end
103+
},
104+
message: "Expected no space before '{{bracket}}', but found.",
105+
data: { bracket: sourceCode.getText(lastToken) },
106+
fix: (fixer) => fixer.removeRange([prevToken.range[1], lastToken.range[0]])
107+
})
108+
}
109+
}
110+
})
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/**
2+
* @author Toru Nagashima <https://github.com/mysticatea>
3+
*/
4+
5+
'use strict'
6+
7+
// -----------------------------------------------------------------------------
8+
// Requirements
9+
// -----------------------------------------------------------------------------
10+
11+
const RuleTester = require('eslint').RuleTester
12+
const rule = require('../../../lib/rules/html-closing-bracket-spacing')
13+
14+
// -----------------------------------------------------------------------------
15+
// Tests
16+
// -----------------------------------------------------------------------------
17+
18+
var ruleTester = new RuleTester({
19+
parser: 'vue-eslint-parser',
20+
parserOptions: {
21+
ecmaVersion: 2015
22+
}
23+
})
24+
25+
ruleTester.run('html-closing-bracket-spacing', rule, {
26+
valid: [
27+
'',
28+
'<template><div></div><div /></template>',
29+
'<template><div foo></div><div foo /></template>',
30+
'<template><div foo=a></div><div foo=a /></template>',
31+
'<template><div foo="a"></div><div foo="a" /></template>',
32+
{
33+
code: '<template ><div ></div><div /></template>',
34+
options: [{ startTag: 'always' }]
35+
},
36+
{
37+
code: '<template><div></div ><div /></template >',
38+
options: [{ endTag: 'always' }]
39+
},
40+
{
41+
code: '<template><div></div><div/></template>',
42+
options: [{ selfClosingTag: 'never' }]
43+
},
44+
'<template><div',
45+
'<template><div></div',
46+
{
47+
code: '<template><div',
48+
options: [{ startTag: 'never', endTag: 'never' }]
49+
},
50+
{
51+
code: '<template><div></div',
52+
options: [{ startTag: 'never', endTag: 'never' }]
53+
}
54+
],
55+
invalid: [
56+
{
57+
code: '<template>\n <div >\n </div >\n <div/>\n</template>',
58+
output: '<template>\n <div>\n </div>\n <div />\n</template>',
59+
errors: [
60+
{ message: "Expected no space before '>', but found.", line: 2, column: 7, endColumn: 9 },
61+
{ message: "Expected no space before '>', but found.", line: 3, column: 8, endColumn: 10 },
62+
{ message: "Expected a space before '/>', but not found.", line: 4, column: 7, endColumn: 9 }
63+
]
64+
},
65+
{
66+
code: '<template>\n <div foo ></div>\n <div foo/>\n</template>',
67+
output: '<template>\n <div foo></div>\n <div foo />\n</template>',
68+
errors: [
69+
{ message: "Expected no space before '>', but found.", line: 2, column: 11, endColumn: 13 },
70+
{ message: "Expected a space before '/>', but not found.", line: 3, column: 11, endColumn: 13 }
71+
]
72+
},
73+
{
74+
code: '<template>\n <div foo="1" ></div>\n <div foo="1"/>\n</template>',
75+
output: '<template>\n <div foo="1"></div>\n <div foo="1" />\n</template>',
76+
errors: [
77+
{ message: "Expected no space before '>', but found.", line: 2, column: 15, endColumn: 17 },
78+
{ message: "Expected a space before '/>', but not found.", line: 3, column: 15, endColumn: 17 }
79+
]
80+
},
81+
{
82+
code: '<template >\n <div>\n </div>\n <div />\n</template >',
83+
output: '<template >\n <div >\n </div >\n <div/>\n</template >',
84+
options: [{
85+
startTag: 'always',
86+
endTag: 'always',
87+
selfClosingTag: 'never'
88+
}],
89+
errors: [
90+
{ message: "Expected a space before '>', but not found.", line: 2, column: 7, endColumn: 8 },
91+
{ message: "Expected a space before '>', but not found.", line: 3, column: 8, endColumn: 9 },
92+
{ message: "Expected no space before '/>', but found.", line: 4, column: 7, endColumn: 10 }
93+
]
94+
}
95+
]
96+
})

0 commit comments

Comments
 (0)