Skip to content

New: html-closing-bracket-spacing rule (fixes #229) #312

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi

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

<!--RULES_TABLE_END-->
Expand Down
83 changes: 83 additions & 0 deletions docs/rules/html-closing-bracket-spacing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# require or disallow a space before tag's closing brackets (html-closing-bracket-spacing)

- :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.

This rule enforces consistent spacing style before closing brackets `>` of tags.

```html
<div class="foo"> or <div class="foo" >
<div class="foo"/> or <div class="foo" />
```

## Rule Details

This rule has options.

```json
{
"html-closing-bracket-spacing": ["error", {
"startTag": "always" | "never",
"endTag": "always" | "never",
"selfClosingTag": "always" | "never"
}]
}
```

- `startTag` (`"always" | "never"`) ... Setting for the `>` of start tags (e.g. `<div>`). Default is `"never"`.
- `"always"` ... requires one or more spaces.
- `"never"` ... disallows spaces.
- `endTag` (`"always" | "never"`) ... Setting for the `>` of end tags (e.g. `</div>`). Default is `"never"`.
- `"always"` ... requires one or more spaces.
- `"never"` ... disallows spaces.
- `selfClosingTag` (`"always" | "never"`) ... Setting for the `/>` of self-closing tags (e.g. `<div/>`). Default is `"always"`.
- `"always"` ... requires one or more spaces.
- `"never"` ... disallows spaces.

Examples of **incorrect** code for this rule:

```html
<!--eslint html-closing-bracket-spacing: "error" -->

<div >
<div foo >
<div foo="bar" >
</div >
<div/>
<div foo/>
<div foo="bar"/>
```

Examples of **correct** code for this rule:

```html
<!--eslint html-closing-bracket-spacing: "error" -->

<div>
<div foo>
<div foo="bar">
</div>
<div />
<div foo />
<div foo="bar" />
```

```html
<!--eslint html-closing-bracket-spacing: ["error", {
"startTag": "always",
"endTag": "always",
"selfClosingTag": "always"
}] -->

<div >
<div foo >
<div foo="bar" >
</div >
<div />
<div foo />
<div foo="bar" />
```

# Related rules

- [vue/no-multi-spaces](./no-multi-spaces.md)
- [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md)
112 changes: 112 additions & 0 deletions lib/rules/html-closing-bracket-spacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/

'use strict'

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

const utils = require('../utils')

// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------

/**
* Normalize options.
* @param {{startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"}} options The options user configured.
* @param {TokenStore} tokens The token store of template body.
* @returns {{startTag:"always"|"never",endTag:"always"|"never",selfClosingTag:"always"|"never"}} The normalized options.
*/
function parseOptions (options, tokens) {
return Object.assign({
startTag: 'never',
endTag: 'never',
selfClosingTag: 'always',

detectType (node) {
const openType = tokens.getFirstToken(node).type
const closeType = tokens.getLastToken(node).type

if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
return this.endTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
return this.startTag
}
if (openType === 'HTMLTagOpen' && closeType === 'HTMLSelfClosingTagClose') {
return this.selfClosingTag
}
return null
}
}, options)
}

// -----------------------------------------------------------------------------
// Rule Definition
// -----------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'require or disallow a space before tag\'s closing brackets',
category: undefined
},
schema: [{
type: 'object',
properties: {
startTag: { enum: ['always', 'never'] },
endTag: { enum: ['always', 'never'] },
selfClosingTag: { enum: ['always', 'never'] }
},
additionalProperties: false
}],
fixable: 'whitespace'
},

create (context) {
const sourceCode = context.getSourceCode()
const tokens =
context.parserServices.getTemplateBodyTokenStore &&
context.parserServices.getTemplateBodyTokenStore()
const options = parseOptions(context.options[0], tokens)

return utils.defineTemplateBodyVisitor(context, {
'VStartTag, VEndTag' (node) {
const type = options.detectType(node)
const lastToken = tokens.getLastToken(node)
const prevToken = tokens.getLastToken(node, 1)

// Skip if EOF exists in the tag or linebreak exists before `>`.
if (type == null || prevToken == null || prevToken.loc.end.line !== lastToken.loc.start.line) {
return
}

// Check and report.
const hasSpace = (prevToken.range[1] !== lastToken.range[0])
if (type === 'always' && !hasSpace) {
context.report({
node,
loc: lastToken.loc,
message: "Expected a space before '{{bracket}}', but not found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
})
} else if (type === 'never' && hasSpace) {
context.report({
node,
loc: {
start: prevToken.loc.end,
end: lastToken.loc.end
},
message: "Expected no space before '{{bracket}}', but found.",
data: { bracket: sourceCode.getText(lastToken) },
fix: (fixer) => fixer.removeRange([prevToken.range[1], lastToken.range[0]])
})
}
}
})
}
}
96 changes: 96 additions & 0 deletions tests/lib/rules/html-closing-bracket-spacing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* @author Toru Nagashima <https://github.com/mysticatea>
*/

'use strict'

// -----------------------------------------------------------------------------
// Requirements
// -----------------------------------------------------------------------------

const RuleTester = require('eslint').RuleTester
const rule = require('../../../lib/rules/html-closing-bracket-spacing')

// -----------------------------------------------------------------------------
// Tests
// -----------------------------------------------------------------------------

var ruleTester = new RuleTester({
parser: 'vue-eslint-parser',
parserOptions: {
ecmaVersion: 2015
}
})

ruleTester.run('html-closing-bracket-spacing', rule, {
valid: [
'',
'<template><div></div><div /></template>',
'<template><div foo></div><div foo /></template>',
'<template><div foo=a></div><div foo=a /></template>',
'<template><div foo="a"></div><div foo="a" /></template>',
{
code: '<template ><div ></div><div /></template>',
options: [{ startTag: 'always' }]
},
{
code: '<template><div></div ><div /></template >',
options: [{ endTag: 'always' }]
},
{
code: '<template><div></div><div/></template>',
options: [{ selfClosingTag: 'never' }]
},
'<template><div',
'<template><div></div',
{
code: '<template><div',
options: [{ startTag: 'never', endTag: 'never' }]
},
{
code: '<template><div></div',
options: [{ startTag: 'never', endTag: 'never' }]
}
],
invalid: [
{
code: '<template>\n <div >\n </div >\n <div/>\n</template>',
output: '<template>\n <div>\n </div>\n <div />\n</template>',
errors: [
{ message: "Expected no space before '>', but found.", line: 2, column: 7, endColumn: 9 },
{ message: "Expected no space before '>', but found.", line: 3, column: 8, endColumn: 10 },
{ message: "Expected a space before '/>', but not found.", line: 4, column: 7, endColumn: 9 }
]
},
{
code: '<template>\n <div foo ></div>\n <div foo/>\n</template>',
output: '<template>\n <div foo></div>\n <div foo />\n</template>',
errors: [
{ message: "Expected no space before '>', but found.", line: 2, column: 11, endColumn: 13 },
{ message: "Expected a space before '/>', but not found.", line: 3, column: 11, endColumn: 13 }
]
},
{
code: '<template>\n <div foo="1" ></div>\n <div foo="1"/>\n</template>',
output: '<template>\n <div foo="1"></div>\n <div foo="1" />\n</template>',
errors: [
{ message: "Expected no space before '>', but found.", line: 2, column: 15, endColumn: 17 },
{ message: "Expected a space before '/>', but not found.", line: 3, column: 15, endColumn: 17 }
]
},
{
code: '<template >\n <div>\n </div>\n <div />\n</template >',
output: '<template >\n <div >\n </div >\n <div/>\n</template >',
options: [{
startTag: 'always',
endTag: 'always',
selfClosingTag: 'never'
}],
errors: [
{ message: "Expected a space before '>', but not found.", line: 2, column: 7, endColumn: 8 },
{ message: "Expected a space before '>', but not found.", line: 3, column: 8, endColumn: 9 },
{ message: "Expected no space before '/>', but found.", line: 4, column: 7, endColumn: 10 }
]
}
]
})