Skip to content

New: html-self-closing rule (fixes #31) #129

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
Aug 8, 2017
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
67 changes: 67 additions & 0 deletions docs/rules/html-self-closing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Enforce self-closing style (html-self-closing)

In Vue.js template, we can use either two styles for elements which don't have their content.

1. `<your-component></your-component>`
2. `<your-component />` (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": ["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: "error"*/

<template>
<div />
<img />
<your-component></your-component>
<svg><path d=""></path></svg>
</template>
```

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

```html
/*eslint html-self-closing: "error"*/

<template>
<div></div>
<img>
<your-component />
<svg><path d="" /></svg>
</template>
```
2 changes: 1 addition & 1 deletion lib/rules/html-end-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion lib/rules/html-no-self-closing.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ module.exports = {
description: 'disallow self-closing elements.',
category: 'Best Practices',
recommended: false,
replacedBy: []
replacedBy: ['html-self-closing-style']
},
deprecated: true,
fixable: 'code',
Expand Down
179 changes: 179 additions & 0 deletions lib/rules/html-self-closing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @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
// ------------------------------------------------------------------------------

/**
* These strings wil be displayed in error messages.
*/
const ELEMENT_TYPE = 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 {
[ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'never',
[ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
[ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
[ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
[ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
}
}

/**
* Get the elementType of the given element.
* @param {VElement} node The element node to get.
* @returns {string} The elementType of the element.
*/
function getElementType (node) {
if (utils.isCustomComponent(node)) {
return ELEMENT_TYPE.COMPONENT
}
if (utils.isHtmlElementNode(node)) {
if (utils.isHtmlVoidElementName(node.name)) {
return ELEMENT_TYPE.VOID
}
return ELEMENT_TYPE.NORMAL
}
if (utils.isSvgElementNode(node)) {
return ELEMENT_TYPE.SVG
}
if (utils.isMathMLElementNode(node)) {
return ELEMENT_TYPE.MATH
}
return 'unknown elements'
}

/**
* Check whether the given element is empty or not.
* This ignores whitespaces, doesn't ignore comments.
* @param {VElement} node The element node to check.
* @param {SourceCode} sourceCode The source code object of the current context.
* @returns {boolean} `true` if the element is empty.
*/
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() === ''
}

/**
* Creates AST event handlers for html-self-closing.
*
* @param {RuleContext} context - The rule context.
* @returns {object} AST event handlers.
*/
function create (context) {
const sourceCode = context.getSourceCode()
const options = parseOptions(context.options[0])

utils.registerTemplateBodyVisitor(context, {
'VElement' (node) {
const elementType = getElementType(node)
const mode = options[elementType]

if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
context.report({
node,
loc: node.loc,
message: 'Require self-closing on {{elementType}} (<{{name}}>).',
data: { elementType, name: node.rawName },
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 {{elementType}} (<{{name}}/>).',
data: { elementType, name: node.rawName },
fix: (fixer) => {
const tokens = context.parserServices.getTemplateBodyTokenStore()
const close = tokens.getLastToken(node.startTag)
if (close.type !== 'HTMLSelfClosingTagClose') {
return null
}
if (elementType === ELEMENT_TYPE.VOID) {
return fixer.replaceText(close, '>')
}
return fixer.replaceText(close, `></${node.rawName}>`)
}
})
}
}
})

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
}
}
}
19 changes: 15 additions & 4 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
)
Expand All @@ -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
},

/**
Expand All @@ -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())
Expand Down
Loading