From fb87f96eef9058106a98c15aeda4c3d9d2f3cf79 Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Fri, 4 Aug 2017 18:37:55 +0900 Subject: [PATCH 1/5] New: `html-self-closing-style` rule (fixes #31) --- docs/rules/html-self-closing-style.md | 67 ++++++ lib/rules/html-end-tags.js | 2 +- lib/rules/html-self-closing-style.js | 175 ++++++++++++++ lib/utils/index.js | 19 +- tests/lib/rules/html-self-closing-style.js | 256 +++++++++++++++++++++ 5 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 docs/rules/html-self-closing-style.md create mode 100644 lib/rules/html-self-closing-style.js create mode 100644 tests/lib/rules/html-self-closing-style.js diff --git a/docs/rules/html-self-closing-style.md b/docs/rules/html-self-closing-style.md new file mode 100644 index 000000000..44093fb5f --- /dev/null +++ b/docs/rules/html-self-closing-style.md @@ -0,0 +1,67 @@ +# Enforce self-closing style (html-self-closing-style) + +In Vue.js template, we can use either two styles for elements which don't have their content. + +1. `` +2. `` (self-closing) + +Self-closing is simple and shorter, but it's not supported in raw HTML. +This rule helps you to unify the self-closing style. + +## Rule Details + +This rule has options which specify self-closing style for each context. + +```json +{ + "html-self-closing-style": ["error", { + "html": { + "normal": "never", + "void": "never", + "component": "always" + }, + "svg": "always", + "math": "always" + }] +} +``` + +- `html.normal` (`"never"` by default) ... The style of well-known HTML elements except void elements. +- `html.void` (`"never"` by default) ... The style of well-known HTML void elements. +- `html.component` (`"always"` by default) ... The style of Vue.js custom components. +- `svg`(`"always"` by default) .... The style of well-known SVG elements. +- `math`(`"always"` by default) .... The style of well-known MathML elements. + +Every option can be set to one of the following values: + +- `"always"` ... Require self-closing at elements which don't have their content. +- `"never"` ... Disallow self-closing. +- `"any"` ... Don't enforce self-closing style. + +---- + +:-1: Examples of **incorrect** code for this rule: + +```html +/*eslint html-self-closing-style: "error"*/ + + +``` + +:+1: Examples of **correct** code for this rule: + +```html +/*eslint html-self-closing-style: "error"*/ + + +``` diff --git a/lib/rules/html-end-tags.js b/lib/rules/html-end-tags.js index 4238cca1d..511351095 100644 --- a/lib/rules/html-end-tags.js +++ b/lib/rules/html-end-tags.js @@ -25,7 +25,7 @@ function create (context) { utils.registerTemplateBodyVisitor(context, { VElement (node) { const name = node.name - const isVoid = utils.isVoidElementName(name) + const isVoid = utils.isHtmlVoidElementName(name) const hasEndTag = node.endTag != null if (isVoid && hasEndTag) { diff --git a/lib/rules/html-self-closing-style.js b/lib/rules/html-self-closing-style.js new file mode 100644 index 000000000..62b6c259b --- /dev/null +++ b/lib/rules/html-self-closing-style.js @@ -0,0 +1,175 @@ +/** + * @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 +// ------------------------------------------------------------------------------ + +/** + * Kind strings. + * This strings wil be displayed in error messages. + */ +const KIND = Object.freeze({ + NORMAL: 'HTML elements', + VOID: 'HTML void elements', + COMPONENT: 'Vue.js custom components', + SVG: 'SVG elements', + MATH: 'MathML elements' +}) + +/** + * Normalize the given options. + * @param {Object|undefined} options The raw options object. + * @returns {Object} Normalized options. + */ +function parseOptions (options) { + return { + [KIND.NORMAL]: (options && options.html && options.html.normal) || 'never', + [KIND.VOID]: (options && options.html && options.html.void) || 'never', + [KIND.COMPONENT]: (options && options.html && options.html.component) || 'always', + [KIND.SVG]: (options && options.svg) || 'always', + [KIND.MATH]: (options && options.math) || 'always' + } +} + +/** + * Get the kind of the given element. + * @param {VElement} node The element node to get. + * @returns {string} The kind of the element. + */ +function getKind (node) { + if (utils.isCustomComponent(node)) { + return KIND.COMPONENT + } + if (utils.isHtmlElementNode(node)) { + if (utils.isHtmlVoidElementName(node.name)) { + return KIND.VOID + } + return KIND.NORMAL + } + if (utils.isSvgElementNode(node)) { + return KIND.SVG + } + if (utils.isMathMLElementNode(node)) { + return KIND.MATH + } + return 'unknown elements' +} + +/** + * Check whether the given element is empty or not. + * This ignores whitespaces. + * @param {VElement} node The element node to check. + * @returns {boolean} `true` if the element is empty. + */ +function isEmpty (node) { + return node.children.every(child => child.type === 'VText' && child.value.trim() === '') +} + +/** + * Creates AST event handlers for html-self-closing-style. + * + * @param {RuleContext} context - The rule context. + * @returns {object} AST event handlers. + */ +function create (context) { + const options = parseOptions(context.options[0]) + + utils.registerTemplateBodyVisitor(context, { + 'VElement' (node) { + const kind = getKind(node) + const mode = options[kind] + + if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node)) { + context.report({ + node, + loc: node.loc, + message: 'Require self-closing on {{kind}}.', + data: { kind }, + fix: (fixer) => { + const tokens = context.parserServices.getTemplateBodyTokenStore() + const close = tokens.getLastToken(node.startTag) + if (close.type !== 'HTMLTagClose') { + return null + } + return fixer.replaceTextRange([close.range[0], node.range[1]], '/>') + } + }) + } + + if (mode === 'never' && node.startTag.selfClosing) { + context.report({ + node, + loc: node.loc, + message: 'Disallow self-closing on {{kind}}.', + data: { kind }, + fix: (fixer) => { + const tokens = context.parserServices.getTemplateBodyTokenStore() + const close = tokens.getLastToken(node.startTag) + if (close.type !== 'HTMLSelfClosingTagClose') { + return null + } + if (kind === KIND.VOID) { + return fixer.replaceText(close, '>') + } + return fixer.replaceText(close, `>`) + } + }) + } + } + }) + + return {} +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + create, + meta: { + docs: { + description: 'enforce self-closing style.', + category: 'Stylistic Issues', + recommended: false + }, + fixable: 'code', + schema: { + definitions: { + optionValue: { + enum: ['always', 'never', 'any'] + } + }, + type: 'array', + items: [{ + type: 'object', + properties: { + html: { + type: 'object', + properties: { + normal: { $ref: '#/definitions/optionValue' }, + void: { $ref: '#/definitions/optionValue' }, + component: { $ref: '#/definitions/optionValue' } + }, + additionalProperties: false + }, + svg: { $ref: '#/definitions/optionValue' }, + math: { $ref: '#/definitions/optionValue' } + }, + additionalProperties: false + }], + maxItems: 1 + } + } +} diff --git a/lib/utils/index.js b/lib/utils/index.js index 1e741703b..36036d602 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -183,7 +183,7 @@ module.exports = { assert(node && node.type === 'VElement') return ( - !(this.isKnownHtmlElementNode(node) || this.isSvgElementNode(node) || this.isMathMLElementNode(node)) || + (this.isHtmlElementNode(node) && !this.isHtmlWellKnownElementName(node.name)) || this.hasAttribute(node, 'is') || this.hasDirective(node, 'bind', 'is') ) @@ -194,10 +194,10 @@ module.exports = { * @param {ASTNode} node The node to check. * @returns {boolean} `true` if the node is a HTML element. */ - isKnownHtmlElementNode (node) { + isHtmlElementNode (node) { assert(node && node.type === 'VElement') - return node.namespace === vueEslintParser.AST.NS.HTML && HTML_ELEMENT_NAMES.has(node.name.toLowerCase()) + return node.namespace === vueEslintParser.AST.NS.HTML }, /** @@ -222,12 +222,23 @@ module.exports = { return node.namespace === vueEslintParser.AST.NS.MathML }, + /** + * Check whether the given name is an well-known element or not. + * @param {string} name The name to check. + * @returns {boolean} `true` if the name is an well-known element name. + */ + isHtmlWellKnownElementName (name) { + assert(typeof name === 'string') + + return HTML_ELEMENT_NAMES.has(name.toLowerCase()) + }, + /** * Check whether the given name is a void element name or not. * @param {string} name The name to check. * @returns {boolean} `true` if the name is a void element name. */ - isVoidElementName (name) { + isHtmlVoidElementName (name) { assert(typeof name === 'string') return VOID_ELEMENT_NAMES.has(name.toLowerCase()) diff --git a/tests/lib/rules/html-self-closing-style.js b/tests/lib/rules/html-self-closing-style.js new file mode 100644 index 000000000..8337b3570 --- /dev/null +++ b/tests/lib/rules/html-self-closing-style.js @@ -0,0 +1,256 @@ +/** + * @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-self-closing-style') +const RuleTester = require('eslint').RuleTester + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const tester = new RuleTester({ + parser: 'vue-eslint-parser' +}) + +const ALL_CODE = `` +const anyWith = (opts) => Object.assign( + { + svg: 'any', + math: 'any' + }, + opts, + { + html: Object.assign( + { + normal: 'any', + void: 'any', + component: 'any' + }, + opts.html || {} + ) + } +) + +tester.run('html-self-closing-style', rule, { + valid: [ + // default + '', + '', + '', + '', + '' + + // Other cases are in `invalid` tests. + ], + invalid: [ + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { normal: 'always' }})], + errors: [ + { message: 'Require self-closing on HTML elements.', line: 2 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { normal: 'never' }})], + errors: [ + { message: 'Disallow self-closing on HTML elements.', line: 3 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { void: 'always' }})], + errors: [ + { message: 'Require self-closing on HTML void elements.', line: 4 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { void: 'never' }})], + errors: [ + { message: 'Disallow self-closing on HTML void elements.', line: 5 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { component: 'always' }})], + errors: [ + { message: 'Require self-closing on Vue.js custom components.', line: 6 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ html: { component: 'never' }})], + errors: [ + { message: 'Disallow self-closing on Vue.js custom components.', line: 7 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ svg: 'always' })], + errors: [ + { message: 'Require self-closing on SVG elements.', line: 8 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ svg: 'never' })], + errors: [ + { message: 'Disallow self-closing on SVG elements.', line: 9 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ math: 'always' })], + errors: [ + { message: 'Require self-closing on MathML elements.', line: 10 } + ] + }, + { + code: ALL_CODE, + output: ``, + options: [anyWith({ math: 'never' })], + errors: [ + { message: 'Disallow self-closing on MathML elements.', line: 11 } + ] + } + ] +}) From 4595482bda4f10a7548325be76b802dc9701ceaf Mon Sep 17 00:00:00 2001 From: Toru Nagashima Date: Sat, 5 Aug 2017 19:25:52 +0900 Subject: [PATCH 2/5] add more tests. --- lib/rules/html-self-closing-style.js | 12 ++++--- tests/lib/rules/html-self-closing-style.js | 39 ++++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/lib/rules/html-self-closing-style.js b/lib/rules/html-self-closing-style.js index 62b6c259b..1e49aad27 100644 --- a/lib/rules/html-self-closing-style.js +++ b/lib/rules/html-self-closing-style.js @@ -68,12 +68,15 @@ function getKind (node) { /** * Check whether the given element is empty or not. - * This ignores whitespaces. + * This ignores whitespaces, doesn't ignore comments. * @param {VElement} node The element node to check. * @returns {boolean} `true` if the element is empty. */ -function isEmpty (node) { - return node.children.every(child => child.type === 'VText' && child.value.trim() === '') +function isEmpty (node, sourceCode) { + const start = node.startTag.range[1] + const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1] + + return sourceCode.text.slice(start, end).trim() === '' } /** @@ -83,6 +86,7 @@ function isEmpty (node) { * @returns {object} AST event handlers. */ function create (context) { + const sourceCode = context.getSourceCode() const options = parseOptions(context.options[0]) utils.registerTemplateBodyVisitor(context, { @@ -90,7 +94,7 @@ function create (context) { const kind = getKind(node) const mode = options[kind] - if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node)) { + if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) { context.report({ node, loc: node.loc, diff --git a/tests/lib/rules/html-self-closing-style.js b/tests/lib/rules/html-self-closing-style.js index 8337b3570..1aef7d729 100644 --- a/tests/lib/rules/html-self-closing-style.js +++ b/tests/lib/rules/html-self-closing-style.js @@ -57,11 +57,46 @@ tester.run('html-self-closing-style', rule, { '', '', '', - '' + '', - // Other cases are in `invalid` tests. + // Don't error if there are comments in their content. + { + code: '', + output: null, + options: [{ html: { normal: 'always' }}] + } + + // other cases are in `invalid` tests. ], invalid: [ + // default + { + code: '', + output: '', + errors: ['Disallow self-closing on HTML elements.'] + }, + { + code: '', + output: '', + errors: ['Disallow self-closing on HTML void elements.'] + }, + { + code: '', + output: '', + errors: ['Require self-closing on Vue.js custom components.'] + }, + { + code: '', + output: '', + errors: ['Require self-closing on SVG elements.'] + }, + { + code: '', + output: '', + errors: ['Require self-closing on MathML elements.'] + }, + + // others { code: ALL_CODE, output: ``, options: [anyWith({ html: { void: 'always' }})], errors: [ - { message: 'Require self-closing on HTML void elements.', line: 4 } + { message: 'Require self-closing on HTML void elements ().', line: 4 } ] }, { @@ -170,7 +171,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ html: { void: 'never' }})], errors: [ - { message: 'Disallow self-closing on HTML void elements.', line: 5 } + { message: 'Disallow self-closing on HTML void elements ().', line: 5 } ] }, { @@ -189,7 +190,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ html: { component: 'always' }})], errors: [ - { message: 'Require self-closing on Vue.js custom components.', line: 6 } + { message: 'Require self-closing on Vue.js custom components ().', line: 6 } ] }, { @@ -208,7 +209,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ html: { component: 'never' }})], errors: [ - { message: 'Disallow self-closing on Vue.js custom components.', line: 7 } + { message: 'Disallow self-closing on Vue.js custom components ().', line: 7 } ] }, { @@ -227,7 +228,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ svg: 'always' })], errors: [ - { message: 'Require self-closing on SVG elements.', line: 8 } + { message: 'Require self-closing on SVG elements ().', line: 8 } ] }, { @@ -246,7 +247,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ svg: 'never' })], errors: [ - { message: 'Disallow self-closing on SVG elements.', line: 9 } + { message: 'Disallow self-closing on SVG elements ().', line: 9 } ] }, { @@ -265,7 +266,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ math: 'always' })], errors: [ - { message: 'Require self-closing on MathML elements.', line: 10 } + { message: 'Require self-closing on MathML elements ().', line: 10 } ] }, { @@ -284,7 +285,7 @@ tester.run('html-self-closing', rule, { `, options: [anyWith({ math: 'never' })], errors: [ - { message: 'Disallow self-closing on MathML elements.', line: 11 } + { message: 'Disallow self-closing on MathML elements ().', line: 11 } ] } ]