html-closing-bracket-spacing.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /**
  2. * @author Toru Nagashima <https://github.com/mysticatea>
  3. */
  4. 'use strict'
  5. const utils = require('../utils')
  6. /**
  7. * @typedef { {startTag?:"always"|"never",endTag?:"always"|"never",selfClosingTag?:"always"|"never"} } Options
  8. */
  9. /**
  10. * Normalize options.
  11. * @param {Options} options The options user configured.
  12. * @param {ParserServices.TokenStore} tokens The token store of template body.
  13. * @returns {Options & { detectType: (node: VStartTag | VEndTag) => 'never' | 'always' | null }} The normalized options.
  14. */
  15. function parseOptions(options, tokens) {
  16. const opts = Object.assign(
  17. {
  18. startTag: 'never',
  19. endTag: 'never',
  20. selfClosingTag: 'always'
  21. },
  22. options
  23. )
  24. return Object.assign(opts, {
  25. /**
  26. * @param {VStartTag | VEndTag} node
  27. * @returns {'never' | 'always' | null}
  28. */
  29. detectType(node) {
  30. const openType = tokens.getFirstToken(node).type
  31. const closeType = tokens.getLastToken(node).type
  32. if (openType === 'HTMLEndTagOpen' && closeType === 'HTMLTagClose') {
  33. return opts.endTag
  34. }
  35. if (openType === 'HTMLTagOpen' && closeType === 'HTMLTagClose') {
  36. return opts.startTag
  37. }
  38. if (
  39. openType === 'HTMLTagOpen' &&
  40. closeType === 'HTMLSelfClosingTagClose'
  41. ) {
  42. return opts.selfClosingTag
  43. }
  44. return null
  45. }
  46. })
  47. }
  48. module.exports = {
  49. meta: {
  50. type: 'layout',
  51. docs: {
  52. description: "require or disallow a space before tag's closing brackets",
  53. categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
  54. url: 'https://eslint.vuejs.org/rules/html-closing-bracket-spacing.html'
  55. },
  56. fixable: 'whitespace',
  57. schema: [
  58. {
  59. type: 'object',
  60. properties: {
  61. startTag: { enum: ['always', 'never'] },
  62. endTag: { enum: ['always', 'never'] },
  63. selfClosingTag: { enum: ['always', 'never'] }
  64. },
  65. additionalProperties: false
  66. }
  67. ],
  68. messages: {
  69. missing: "Expected a space before '{{bracket}}', but not found.",
  70. unexpected: "Expected no space before '{{bracket}}', but found."
  71. }
  72. },
  73. /** @param {RuleContext} context */
  74. create(context) {
  75. const sourceCode = context.getSourceCode()
  76. const tokens =
  77. sourceCode.parserServices.getTemplateBodyTokenStore &&
  78. sourceCode.parserServices.getTemplateBodyTokenStore()
  79. const options = parseOptions(context.options[0], tokens)
  80. return utils.defineDocumentVisitor(context, {
  81. /** @param {VStartTag | VEndTag} node */
  82. 'VStartTag, VEndTag'(node) {
  83. const type = options.detectType(node)
  84. const lastToken = tokens.getLastToken(node)
  85. const prevToken = tokens.getLastToken(node, 1)
  86. // Skip if EOF exists in the tag or linebreak exists before `>`.
  87. if (
  88. type == null ||
  89. prevToken == null ||
  90. prevToken.loc.end.line !== lastToken.loc.start.line
  91. ) {
  92. return
  93. }
  94. // Check and report.
  95. const hasSpace = prevToken.range[1] !== lastToken.range[0]
  96. if (type === 'always' && !hasSpace) {
  97. context.report({
  98. node,
  99. loc: lastToken.loc,
  100. messageId: 'missing',
  101. data: { bracket: sourceCode.getText(lastToken) },
  102. fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
  103. })
  104. } else if (type === 'never' && hasSpace) {
  105. context.report({
  106. node,
  107. loc: {
  108. start: prevToken.loc.end,
  109. end: lastToken.loc.end
  110. },
  111. messageId: 'unexpected',
  112. data: { bracket: sourceCode.getText(lastToken) },
  113. fix: (fixer) =>
  114. fixer.removeRange([prevToken.range[1], lastToken.range[0]])
  115. })
  116. }
  117. }
  118. })
  119. }
  120. }