padding-line-between-blocks.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * @fileoverview Require or disallow padding lines between blocks
  3. * @author Yosuke Ota
  4. */
  5. 'use strict'
  6. const utils = require('../utils')
  7. /**
  8. * Split the source code into multiple lines based on the line delimiters.
  9. * @param {string} text Source code as a string.
  10. * @returns {string[]} Array of source code lines.
  11. */
  12. function splitLines(text) {
  13. return text.split(/\r\n|[\r\n\u2028\u2029]/gu)
  14. }
  15. /**
  16. * Check and report blocks for `never` configuration.
  17. * This autofix removes blank lines between the given 2 blocks.
  18. * @param {RuleContext} context The rule context to report.
  19. * @param {VElement} prevBlock The previous block to check.
  20. * @param {VElement} nextBlock The next block to check.
  21. * @param {Token[]} betweenTokens The array of tokens between blocks.
  22. * @returns {void}
  23. * @private
  24. */
  25. function verifyForNever(context, prevBlock, nextBlock, betweenTokens) {
  26. if (prevBlock.loc.end.line === nextBlock.loc.start.line) {
  27. // same line
  28. return
  29. }
  30. const tokenOrNodes = [...betweenTokens, nextBlock]
  31. /** @type {ASTNode | Token} */
  32. let prev = prevBlock
  33. /** @type {[ASTNode | Token, ASTNode | Token][]} */
  34. const paddingLines = []
  35. for (const tokenOrNode of tokenOrNodes) {
  36. const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
  37. if (numOfLineBreaks > 1) {
  38. paddingLines.push([prev, tokenOrNode])
  39. }
  40. prev = tokenOrNode
  41. }
  42. if (!paddingLines.length) {
  43. return
  44. }
  45. context.report({
  46. node: nextBlock,
  47. messageId: 'never',
  48. *fix(fixer) {
  49. for (const [prevToken, nextToken] of paddingLines) {
  50. const start = prevToken.range[1]
  51. const end = nextToken.range[0]
  52. const paddingText = context.getSourceCode().text.slice(start, end)
  53. const lastSpaces = splitLines(paddingText).pop()
  54. yield fixer.replaceTextRange([start, end], `\n${lastSpaces}`)
  55. }
  56. }
  57. })
  58. }
  59. /**
  60. * Check and report blocks for `always` configuration.
  61. * This autofix inserts a blank line between the given 2 blocks.
  62. * @param {RuleContext} context The rule context to report.
  63. * @param {VElement} prevBlock The previous block to check.
  64. * @param {VElement} nextBlock The next block to check.
  65. * @param {Token[]} betweenTokens The array of tokens between blocks.
  66. * @returns {void}
  67. * @private
  68. */
  69. function verifyForAlways(context, prevBlock, nextBlock, betweenTokens) {
  70. const tokenOrNodes = [...betweenTokens, nextBlock]
  71. /** @type {ASTNode | Token} */
  72. let prev = prevBlock
  73. /** @type {ASTNode | Token | undefined} */
  74. let linebreak
  75. for (const tokenOrNode of tokenOrNodes) {
  76. const numOfLineBreaks = tokenOrNode.loc.start.line - prev.loc.end.line
  77. if (numOfLineBreaks > 1) {
  78. // Already padded.
  79. return
  80. }
  81. if (!linebreak && numOfLineBreaks > 0) {
  82. linebreak = prev
  83. }
  84. prev = tokenOrNode
  85. }
  86. context.report({
  87. node: nextBlock,
  88. messageId: 'always',
  89. fix(fixer) {
  90. if (linebreak) {
  91. return fixer.insertTextAfter(linebreak, '\n')
  92. }
  93. return fixer.insertTextAfter(prevBlock, '\n\n')
  94. }
  95. })
  96. }
  97. /**
  98. * Types of blank lines.
  99. * `never` and `always` are defined.
  100. * Those have `verify` method to check and report statements.
  101. * @private
  102. */
  103. const PaddingTypes = {
  104. never: { verify: verifyForNever },
  105. always: { verify: verifyForAlways }
  106. }
  107. // ------------------------------------------------------------------------------
  108. // Rule Definition
  109. // ------------------------------------------------------------------------------
  110. module.exports = {
  111. meta: {
  112. type: 'layout',
  113. docs: {
  114. description: 'require or disallow padding lines between blocks',
  115. categories: undefined,
  116. url: 'https://eslint.vuejs.org/rules/padding-line-between-blocks.html'
  117. },
  118. fixable: 'whitespace',
  119. schema: [
  120. {
  121. enum: Object.keys(PaddingTypes)
  122. }
  123. ],
  124. messages: {
  125. never: 'Unexpected blank line before this block.',
  126. always: 'Expected blank line before this block.'
  127. }
  128. },
  129. /** @param {RuleContext} context */
  130. create(context) {
  131. if (!context.parserServices.getDocumentFragment) {
  132. return {}
  133. }
  134. const df = context.parserServices.getDocumentFragment()
  135. if (!df) {
  136. return {}
  137. }
  138. const documentFragment = df
  139. /** @type {'always' | 'never'} */
  140. const option = context.options[0] || 'always'
  141. const paddingType = PaddingTypes[option]
  142. /** @type {Token[]} */
  143. let tokens
  144. /**
  145. * @returns {VElement[]}
  146. */
  147. function getTopLevelHTMLElements() {
  148. return documentFragment.children.filter(utils.isVElement)
  149. }
  150. /**
  151. * @param {VElement} prev
  152. * @param {VElement} next
  153. */
  154. function getTokenAndCommentsBetween(prev, next) {
  155. // When there is no <template>, tokenStore.getTokensBetween cannot be used.
  156. if (!tokens) {
  157. tokens = [
  158. ...documentFragment.tokens.filter(
  159. (token) => token.type !== 'HTMLWhitespace'
  160. ),
  161. ...documentFragment.comments
  162. ].sort((a, b) =>
  163. a.range[0] > b.range[0] ? 1 : a.range[0] < b.range[0] ? -1 : 0
  164. )
  165. }
  166. let token = tokens.shift()
  167. const results = []
  168. while (token) {
  169. if (prev.range[1] <= token.range[0]) {
  170. if (next.range[0] <= token.range[0]) {
  171. tokens.unshift(token)
  172. break
  173. } else {
  174. results.push(token)
  175. }
  176. }
  177. token = tokens.shift()
  178. }
  179. return results
  180. }
  181. return utils.defineTemplateBodyVisitor(
  182. context,
  183. {},
  184. {
  185. /** @param {Program} node */
  186. Program(node) {
  187. if (utils.hasInvalidEOF(node)) {
  188. return
  189. }
  190. const elements = [...getTopLevelHTMLElements()]
  191. let prev = elements.shift()
  192. for (const element of elements) {
  193. if (!prev) {
  194. return
  195. }
  196. const betweenTokens = getTokenAndCommentsBetween(prev, element)
  197. paddingType.verify(context, prev, element, betweenTokens)
  198. prev = element
  199. }
  200. }
  201. }
  202. )
  203. }
  204. }