html-closing-bracket-spacing.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  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', 'strongly-recommended'],
  54. url: 'https://eslint.vuejs.org/rules/html-closing-bracket-spacing.html'
  55. },
  56. schema: [
  57. {
  58. type: 'object',
  59. properties: {
  60. startTag: { enum: ['always', 'never'] },
  61. endTag: { enum: ['always', 'never'] },
  62. selfClosingTag: { enum: ['always', 'never'] }
  63. },
  64. additionalProperties: false
  65. }
  66. ],
  67. fixable: 'whitespace'
  68. },
  69. /** @param {RuleContext} context */
  70. create(context) {
  71. const sourceCode = context.getSourceCode()
  72. const tokens =
  73. context.parserServices.getTemplateBodyTokenStore &&
  74. context.parserServices.getTemplateBodyTokenStore()
  75. const options = parseOptions(context.options[0], tokens)
  76. return utils.defineDocumentVisitor(context, {
  77. /** @param {VStartTag | VEndTag} node */
  78. 'VStartTag, VEndTag'(node) {
  79. const type = options.detectType(node)
  80. const lastToken = tokens.getLastToken(node)
  81. const prevToken = tokens.getLastToken(node, 1)
  82. // Skip if EOF exists in the tag or linebreak exists before `>`.
  83. if (
  84. type == null ||
  85. prevToken == null ||
  86. prevToken.loc.end.line !== lastToken.loc.start.line
  87. ) {
  88. return
  89. }
  90. // Check and report.
  91. const hasSpace = prevToken.range[1] !== lastToken.range[0]
  92. if (type === 'always' && !hasSpace) {
  93. context.report({
  94. node,
  95. loc: lastToken.loc,
  96. message: "Expected a space before '{{bracket}}', but not found.",
  97. data: { bracket: sourceCode.getText(lastToken) },
  98. fix: (fixer) => fixer.insertTextBefore(lastToken, ' ')
  99. })
  100. } else if (type === 'never' && hasSpace) {
  101. context.report({
  102. node,
  103. loc: {
  104. start: prevToken.loc.end,
  105. end: lastToken.loc.end
  106. },
  107. message: "Expected no space before '{{bracket}}', but found.",
  108. data: { bracket: sourceCode.getText(lastToken) },
  109. fix: (fixer) =>
  110. fixer.removeRange([prevToken.range[1], lastToken.range[0]])
  111. })
  112. }
  113. }
  114. })
  115. }
  116. }