html-self-closing.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. /**
  2. * @author Toru Nagashima
  3. * @copyright 2016 Toru Nagashima. All rights reserved.
  4. * See LICENSE file in root directory for full license.
  5. */
  6. 'use strict'
  7. // ------------------------------------------------------------------------------
  8. // Requirements
  9. // ------------------------------------------------------------------------------
  10. const utils = require('../utils')
  11. // ------------------------------------------------------------------------------
  12. // Helpers
  13. // ------------------------------------------------------------------------------
  14. /**
  15. * These strings wil be displayed in error messages.
  16. */
  17. const ELEMENT_TYPE = Object.freeze({
  18. NORMAL: 'HTML elements',
  19. VOID: 'HTML void elements',
  20. COMPONENT: 'Vue.js custom components',
  21. SVG: 'SVG elements',
  22. MATH: 'MathML elements'
  23. })
  24. /**
  25. * Normalize the given options.
  26. * @param {Object|undefined} options The raw options object.
  27. * @returns {Object} Normalized options.
  28. */
  29. function parseOptions (options) {
  30. return {
  31. [ELEMENT_TYPE.NORMAL]: (options && options.html && options.html.normal) || 'always',
  32. [ELEMENT_TYPE.VOID]: (options && options.html && options.html.void) || 'never',
  33. [ELEMENT_TYPE.COMPONENT]: (options && options.html && options.html.component) || 'always',
  34. [ELEMENT_TYPE.SVG]: (options && options.svg) || 'always',
  35. [ELEMENT_TYPE.MATH]: (options && options.math) || 'always'
  36. }
  37. }
  38. /**
  39. * Get the elementType of the given element.
  40. * @param {VElement} node The element node to get.
  41. * @returns {string} The elementType of the element.
  42. */
  43. function getElementType (node) {
  44. if (utils.isCustomComponent(node)) {
  45. return ELEMENT_TYPE.COMPONENT
  46. }
  47. if (utils.isHtmlElementNode(node)) {
  48. if (utils.isHtmlVoidElementName(node.name)) {
  49. return ELEMENT_TYPE.VOID
  50. }
  51. return ELEMENT_TYPE.NORMAL
  52. }
  53. if (utils.isSvgElementNode(node)) {
  54. return ELEMENT_TYPE.SVG
  55. }
  56. if (utils.isMathMLElementNode(node)) {
  57. return ELEMENT_TYPE.MATH
  58. }
  59. return 'unknown elements'
  60. }
  61. /**
  62. * Check whether the given element is empty or not.
  63. * This ignores whitespaces, doesn't ignore comments.
  64. * @param {VElement} node The element node to check.
  65. * @param {SourceCode} sourceCode The source code object of the current context.
  66. * @returns {boolean} `true` if the element is empty.
  67. */
  68. function isEmpty (node, sourceCode) {
  69. const start = node.startTag.range[1]
  70. const end = (node.endTag != null) ? node.endTag.range[0] : node.range[1]
  71. return sourceCode.text.slice(start, end).trim() === ''
  72. }
  73. // ------------------------------------------------------------------------------
  74. // Rule Definition
  75. // ------------------------------------------------------------------------------
  76. module.exports = {
  77. meta: {
  78. type: 'layout',
  79. docs: {
  80. description: 'enforce self-closing style',
  81. category: 'strongly-recommended',
  82. url: 'https://eslint.vuejs.org/rules/html-self-closing.html'
  83. },
  84. fixable: 'code',
  85. schema: {
  86. definitions: {
  87. optionValue: {
  88. enum: ['always', 'never', 'any']
  89. }
  90. },
  91. type: 'array',
  92. items: [{
  93. type: 'object',
  94. properties: {
  95. html: {
  96. type: 'object',
  97. properties: {
  98. normal: { $ref: '#/definitions/optionValue' },
  99. void: { $ref: '#/definitions/optionValue' },
  100. component: { $ref: '#/definitions/optionValue' }
  101. },
  102. additionalProperties: false
  103. },
  104. svg: { $ref: '#/definitions/optionValue' },
  105. math: { $ref: '#/definitions/optionValue' }
  106. },
  107. additionalProperties: false
  108. }],
  109. maxItems: 1
  110. }
  111. },
  112. create (context) {
  113. const sourceCode = context.getSourceCode()
  114. const options = parseOptions(context.options[0])
  115. let hasInvalidEOF = false
  116. return utils.defineTemplateBodyVisitor(context, {
  117. 'VElement' (node) {
  118. if (hasInvalidEOF) {
  119. return
  120. }
  121. const elementType = getElementType(node)
  122. const mode = options[elementType]
  123. if (mode === 'always' && !node.startTag.selfClosing && isEmpty(node, sourceCode)) {
  124. context.report({
  125. node,
  126. loc: node.loc,
  127. message: 'Require self-closing on {{elementType}} (<{{name}}>).',
  128. data: { elementType, name: node.rawName },
  129. fix: (fixer) => {
  130. const tokens = context.parserServices.getTemplateBodyTokenStore()
  131. const close = tokens.getLastToken(node.startTag)
  132. if (close.type !== 'HTMLTagClose') {
  133. return null
  134. }
  135. return fixer.replaceTextRange([close.range[0], node.range[1]], '/>')
  136. }
  137. })
  138. }
  139. if (mode === 'never' && node.startTag.selfClosing) {
  140. context.report({
  141. node,
  142. loc: node.loc,
  143. message: 'Disallow self-closing on {{elementType}} (<{{name}}/>).',
  144. data: { elementType, name: node.rawName },
  145. fix: (fixer) => {
  146. const tokens = context.parserServices.getTemplateBodyTokenStore()
  147. const close = tokens.getLastToken(node.startTag)
  148. if (close.type !== 'HTMLSelfClosingTagClose') {
  149. return null
  150. }
  151. if (elementType === ELEMENT_TYPE.VOID) {
  152. return fixer.replaceText(close, '>')
  153. }
  154. // If only `close` is targeted for replacement, it conflicts with `component-name-in-template-casing`,
  155. // so replace the entire element.
  156. // return fixer.replaceText(close, `></${node.rawName}>`)
  157. const elementPart = sourceCode.text.slice(node.range[0], close.range[0])
  158. return fixer.replaceText(node, elementPart + `></${node.rawName}>`)
  159. }
  160. })
  161. }
  162. }
  163. }, {
  164. Program (node) {
  165. hasInvalidEOF = utils.hasInvalidEOF(node)
  166. }
  167. })
  168. }
  169. }