diff --git a/README.md b/README.md index 871384de1..f3f64a05b 100644 --- a/README.md +++ b/README.md @@ -186,6 +186,13 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | | [order-in-components](./docs/rules/order-in-components.md) | enforce order of properties in components | | | [this-in-template](./docs/rules/this-in-template.md) | enforce usage of `this` in template | + +### Uncategorized + +| | Rule ID | Description | +|:---|:--------|:------------| +| :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--> ## :couple: FAQ diff --git a/docs/rules/html-closing-bracket-newline.md b/docs/rules/html-closing-bracket-newline.md new file mode 100644 index 000000000..f1c5327db --- /dev/null +++ b/docs/rules/html-closing-bracket-newline.md @@ -0,0 +1,95 @@ +# require or disallow a line break before tag's closing brackets (html-closing-bracket-newline) + +- :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. + +People have own preference about the location of closing brackets. +This rule enforces a line break (or no line break) before tag's closing brackets. + +```html +<div + id="foo" + class="bar"> <!-- On the same line with the last attribute. --> +</div> + +<div + id="foo" + class="bar" +> <!-- On the next line. --> +</div> +``` + +## Rule Details + +```json +{ + "html-closing-bracket-newline": ["error", { + "singleline": "never", + "multiline": "never" + }] +} +``` + +- `singleline` ... the configuration for single-line elements. It's a single-line element if the element does not have attributes or the last attribute is on the same line as the opening bracket. + - `"never"` ... disallow line breaks before the closing bracket. This is the default. + - `"always"` ... require one line break before the closing bracket. +- `multiline` ... the configuration for multiline elements. It's a multiline element if the last attribute is not on the same line of the opening bracket. + - `"never"` ... disallow line breaks before the closing bracket. This is the default. + - `"always"` ... require one line break before the closing bracket. + +Plus, you can use [`vue/html-indent`](./html-indent.md) rule to enforce indent-level of the closing brackets. + +:-1: Examples of **incorrect** code for this rule: + +```html +/*eslint html-closing-bracket-newline: "error"*/ + +<div id="foo" class="bar" +> +<div + id="foo" + class="bar" +> +<div + id="foo" + class="bar" + > +``` + +:+1: Examples of **correct** code for this rule: + +```html +/*eslint html-closing-bracket-newline: "error"*/ + +<div id="foo" class="bar"> +<div + id="foo" + class="bar"> +``` + +:-1: Examples of **incorrect** code for `{ "multiline": "always" }`: + +```html +/*eslint html-closing-bracket-newline: ["error", { multiline: always }]*/ + +<div id="foo" class="bar" +> +<div + id="foo" + class="bar"> +``` + +:+1: Examples of **correct** code for `{ "multiline": "always" }`: + +```html +/*eslint html-closing-bracket-newline: ["error", { multiline: always }]*/ + +<div id="foo" class="bar"> +<div + id="foo" + class="bar" +> +<div + id="foo" + class="bar" + > +``` diff --git a/lib/rules/html-closing-bracket-newline.js b/lib/rules/html-closing-bracket-newline.js new file mode 100644 index 000000000..62d735866 --- /dev/null +++ b/lib/rules/html-closing-bracket-newline.js @@ -0,0 +1,92 @@ +/** + * @author Toru Nagashima + * @copyright 2016 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +function getPhrase (lineBreaks) { + switch (lineBreaks) { + case 0: return 'no line breaks' + case 1: return '1 line break' + default: return `${lineBreaks} line breaks` + } +} + +/** + * Creates AST event handlers for html-closing-bracket-newline. + * + * @param {RuleContext} context - The rule context. + * @returns {object} AST event handlers. + */ +function create (context) { + const options = context.options[0] || {} + const template = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore() + + return utils.defineTemplateBodyVisitor(context, { + 'VStartTag, VEndTag' (node) { + const closingBracketToken = template.getLastToken(node) + if (closingBracketToken.type !== 'HTMLSelfClosingTagClose' && closingBracketToken.type !== 'HTMLTagClose') { + return + } + + const prevToken = template.getTokenBefore(closingBracketToken) + const type = (node.loc.start.line === prevToken.loc.end.line) ? 'singleline' : 'multiline' + const expectedLineBreaks = (options[type] === 'always') ? 1 : 0 + const actualLineBreaks = (closingBracketToken.loc.start.line - prevToken.loc.end.line) + + if (actualLineBreaks !== expectedLineBreaks) { + context.report({ + node, + loc: { + start: prevToken.loc.end, + end: closingBracketToken.loc.start + }, + message: 'Expected {{expected}} before closing bracket, but {{actual}} found.', + data: { + expected: getPhrase(expectedLineBreaks), + actual: getPhrase(actualLineBreaks) + }, + fix (fixer) { + const range = [prevToken.range[1], closingBracketToken.range[0]] + const text = '\n'.repeat(expectedLineBreaks) + return fixer.replaceTextRange(range, text) + } + }) + } + } + }) +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + create, + meta: { + docs: { + description: "require or disallow a line break before tag's closing brackets", + category: undefined + }, + fixable: 'whitespace', + schema: [{ + type: 'object', + properties: { + 'singleline': { enum: ['always', 'never'] }, + 'multiline': { enum: ['always', 'never'] } + }, + additionalProperties: false + }] + } +} diff --git a/tests/lib/rules/html-closing-bracket-newline.js b/tests/lib/rules/html-closing-bracket-newline.js new file mode 100644 index 000000000..46e154182 --- /dev/null +++ b/tests/lib/rules/html-closing-bracket-newline.js @@ -0,0 +1,317 @@ +/** + * @author Toru Nagashima + * @copyright 2016 Toru Nagashima. All rights reserved. + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/html-closing-bracket-newline') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: 'vue-eslint-parser', + parserOptions: { + ecmaVersion: 2015 + } +}) + +tester.run('html-closing-bracket-newline', rule, { + valid: [ + `<template><div></div></template>`, + ` + <template> + <div + id=""> + </div> + </template> + `, + { + code: `<template><div></div></template>`, + options: [{ + singleline: 'never', + multiline: 'never' + }] + }, + { + code: ` + <template> + <div + id=""> + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'never' + }] + }, + { + code: ` + <template> + <div + id="" + > + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'always' + }] + }, + { + code: ` + <template> + <div id=""> + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'always' + }] + }, + { + code: ` + <template + > + <div + id=""> + </div + > + </template + > + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + { + code: ` + <template + > + <div id="" + > + </div + > + </template + > + `, + options: [{ + singleline: 'always', + multiline: 'never' + }] + }, + + // Ignore if no closing brackets + ` + <template> + <div + id= + "" + ` + ], + invalid: [ + { + code: ` + <template> + <div + ></div + + > + </template> + `, + output: ` + <template> + <div></div> + </template> + `, + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.', + 'Expected no line breaks before closing bracket, but 2 line breaks found.' + ] + }, + { + code: ` + <template> + <div + id="" + > + </div> + </template> + `, + output: ` + <template> + <div + id=""> + </div> + </template> + `, + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.' + ] + }, + { + code: ` + <template> + <div + ></div + + > + </template> + `, + output: ` + <template> + <div></div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'never' + }], + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.', + 'Expected no line breaks before closing bracket, but 2 line breaks found.' + ] + }, + { + code: ` + <template> + <div + id="" + > + </div> + </template> + `, + output: ` + <template> + <div + id=""> + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'never' + }], + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.' + ] + }, + { + code: ` + <template> + <div + id=""> + </div> + </template> + `, + output: ` + <template> + <div + id="" +> + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'always' + }], + errors: [ + 'Expected 1 line break before closing bracket, but no line breaks found.' + ] + }, + { + code: ` + <template> + <div id="" + > + </div + > + </template> + `, + output: ` + <template> + <div id=""> + </div> + </template> + `, + options: [{ + singleline: 'never', + multiline: 'always' + }], + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.', + 'Expected no line breaks before closing bracket, but 1 line break found.' + ] + }, + { + code: ` + <template + > + <div + id="" + > + </div> + </template + > + `, + output: ` + <template + > + <div + id=""> + </div +> + </template + > + `, + options: [{ + singleline: 'always', + multiline: 'never' + }], + errors: [ + 'Expected no line breaks before closing bracket, but 1 line break found.', + 'Expected 1 line break before closing bracket, but no line breaks found.' + ] + }, + { + code: ` + <template + > + <div id=""> + </div> + </template + > + `, + output: ` + <template + > + <div id="" +> + </div +> + </template + > + `, + options: [{ + singleline: 'always', + multiline: 'never' + }], + errors: [ + 'Expected 1 line break before closing bracket, but no line breaks found.', + 'Expected 1 line break before closing bracket, but no line breaks found.' + ] + } + ] +})