html-closing-bracket-newline.js 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  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. const utils = require('../utils')
  8. /**
  9. * @param {number} lineBreaks
  10. */
  11. function getPhrase(lineBreaks) {
  12. switch (lineBreaks) {
  13. case 0: {
  14. return 'no line breaks'
  15. }
  16. case 1: {
  17. return '1 line break'
  18. }
  19. default: {
  20. return `${lineBreaks} line breaks`
  21. }
  22. }
  23. }
  24. /**
  25. * @typedef LineBreakBehavior
  26. * @type {('always'|'never')}
  27. */
  28. /**
  29. * @typedef LineType
  30. * @type {('singleline'|'multiline')}
  31. */
  32. /**
  33. * @typedef RuleOptions
  34. * @type {object}
  35. * @property {LineBreakBehavior} singleline - The behavior for single line tags.
  36. * @property {LineBreakBehavior} multiline - The behavior for multiline tags.
  37. * @property {object} selfClosingTag
  38. * @property {LineBreakBehavior} selfClosingTag.singleline - The behavior for single line self closing tags.
  39. * @property {LineBreakBehavior} selfClosingTag.multiline - The behavior for multiline self closing tags.
  40. */
  41. /**
  42. * @param {VStartTag | VEndTag} node - The node representing a start or end tag.
  43. * @param {RuleOptions} options - The options for line breaks.
  44. * @param {LineType} type - The type of line break.
  45. * @returns {number} - The expected line breaks.
  46. */
  47. function getExpectedLineBreaks(node, options, type) {
  48. const isSelfClosingTag = node.type === 'VStartTag' && node.selfClosing
  49. if (
  50. isSelfClosingTag &&
  51. options.selfClosingTag &&
  52. options.selfClosingTag[type]
  53. ) {
  54. return options.selfClosingTag[type] === 'always' ? 1 : 0
  55. }
  56. return options[type] === 'always' ? 1 : 0
  57. }
  58. module.exports = {
  59. meta: {
  60. type: 'layout',
  61. docs: {
  62. description:
  63. "require or disallow a line break before tag's closing brackets",
  64. categories: ['vue3-strongly-recommended', 'vue2-strongly-recommended'],
  65. url: 'https://eslint.vuejs.org/rules/html-closing-bracket-newline.html'
  66. },
  67. fixable: 'whitespace',
  68. schema: [
  69. {
  70. type: 'object',
  71. properties: {
  72. singleline: { enum: ['always', 'never'] },
  73. multiline: { enum: ['always', 'never'] },
  74. selfClosingTag: {
  75. type: 'object',
  76. properties: {
  77. singleline: { enum: ['always', 'never'] },
  78. multiline: { enum: ['always', 'never'] }
  79. },
  80. additionalProperties: false,
  81. minProperties: 1
  82. }
  83. },
  84. additionalProperties: false
  85. }
  86. ],
  87. messages: {
  88. expectedBeforeClosingBracket:
  89. 'Expected {{expected}} before closing bracket, but {{actual}} found.'
  90. }
  91. },
  92. /** @param {RuleContext} context */
  93. create(context) {
  94. const options = Object.assign(
  95. {},
  96. {
  97. singleline: 'never',
  98. multiline: 'always'
  99. },
  100. context.options[0] || {}
  101. )
  102. const sourceCode = context.getSourceCode()
  103. const template =
  104. sourceCode.parserServices.getTemplateBodyTokenStore &&
  105. sourceCode.parserServices.getTemplateBodyTokenStore()
  106. return utils.defineDocumentVisitor(context, {
  107. /** @param {VStartTag | VEndTag} node */
  108. 'VStartTag, VEndTag'(node) {
  109. const closingBracketToken = template.getLastToken(node)
  110. if (
  111. closingBracketToken.type !== 'HTMLSelfClosingTagClose' &&
  112. closingBracketToken.type !== 'HTMLTagClose'
  113. ) {
  114. return
  115. }
  116. const prevToken = template.getTokenBefore(closingBracketToken)
  117. const type =
  118. node.loc.start.line === prevToken.loc.end.line
  119. ? 'singleline'
  120. : 'multiline'
  121. const expectedLineBreaks = getExpectedLineBreaks(node, options, type)
  122. const actualLineBreaks =
  123. closingBracketToken.loc.start.line - prevToken.loc.end.line
  124. if (actualLineBreaks !== expectedLineBreaks) {
  125. context.report({
  126. node,
  127. loc: {
  128. start: prevToken.loc.end,
  129. end: closingBracketToken.loc.start
  130. },
  131. messageId: 'expectedBeforeClosingBracket',
  132. data: {
  133. expected: getPhrase(expectedLineBreaks),
  134. actual: getPhrase(actualLineBreaks)
  135. },
  136. fix(fixer) {
  137. /** @type {Range} */
  138. const range = [prevToken.range[1], closingBracketToken.range[0]]
  139. const text = '\n'.repeat(expectedLineBreaks)
  140. return fixer.replaceTextRange(range, text)
  141. }
  142. })
  143. }
  144. }
  145. })
  146. }
  147. }