diff --git a/README.md b/README.md index c9e1b0f10..6de32cf66 100644 --- a/README.md +++ b/README.md @@ -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 | diff --git a/docs/rules/html-closing-bracket-spacing.md b/docs/rules/html-closing-bracket-spacing.md new file mode 100644 index 000000000..52799dd0f --- /dev/null +++ b/docs/rules/html-closing-bracket-spacing.md @@ -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 +
or
+
or
+``` + +## 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. `
`). Default is `"never"`. + - `"always"` ... requires one or more spaces. + - `"never"` ... disallows spaces. +- `endTag` (`"always" | "never"`) ... Setting for the `>` of end tags (e.g. `
`). Default is `"never"`. + - `"always"` ... requires one or more spaces. + - `"never"` ... disallows spaces. +- `selfClosingTag` (`"always" | "never"`) ... Setting for the `/>` of self-closing tags (e.g. `
`). Default is `"always"`. + - `"always"` ... requires one or more spaces. + - `"never"` ... disallows spaces. + +Examples of **incorrect** code for this rule: + +```html + + +
+
+
+
+
+
+
+``` + +Examples of **correct** code for this rule: + +```html + + +
+
+
+
+
+
+
+``` + +```html + + +
+
+
+
+
+
+
+``` + +# Related rules + +- [vue/no-multi-spaces](./no-multi-spaces.md) +- [vue/html-closing-bracket-newline](./html-closing-bracket-newline.md) diff --git a/lib/rules/html-closing-bracket-spacing.js b/lib/rules/html-closing-bracket-spacing.js new file mode 100644 index 000000000..919a1b12c --- /dev/null +++ b/lib/rules/html-closing-bracket-spacing.js @@ -0,0 +1,112 @@ +/** + * @author Toru Nagashima + */ + +'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]]) + }) + } + } + }) + } +} diff --git a/tests/lib/rules/html-closing-bracket-spacing.js b/tests/lib/rules/html-closing-bracket-spacing.js new file mode 100644 index 000000000..1609d1cd0 --- /dev/null +++ b/tests/lib/rules/html-closing-bracket-spacing.js @@ -0,0 +1,96 @@ +/** + * @author Toru Nagashima + */ + +'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: [ + '', + '', + '', + '', + '', + { + code: '', + options: [{ startTag: 'always' }] + }, + { + code: '', + options: [{ endTag: 'always' }] + }, + { + code: '', + options: [{ selfClosingTag: 'never' }] + }, + '', + output: '', + 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: '', + output: '', + 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: '', + output: '', + 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: '', + output: '', + 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 } + ] + } + ] +})