lines-between-class-members.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. /**
  2. * @fileoverview Rule to check empty newline between class members
  3. * @author 薛定谔的猫<hh_2013@foxmail.com>
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const astUtils = require("./utils/ast-utils");
  10. //------------------------------------------------------------------------------
  11. // Rule Definition
  12. //------------------------------------------------------------------------------
  13. /** @type {import('../shared/types').Rule} */
  14. module.exports = {
  15. meta: {
  16. type: "layout",
  17. docs: {
  18. description: "Require or disallow an empty line between class members",
  19. recommended: false,
  20. url: "https://eslint.org/docs/rules/lines-between-class-members"
  21. },
  22. fixable: "whitespace",
  23. schema: [
  24. {
  25. enum: ["always", "never"]
  26. },
  27. {
  28. type: "object",
  29. properties: {
  30. exceptAfterSingleLine: {
  31. type: "boolean",
  32. default: false
  33. }
  34. },
  35. additionalProperties: false
  36. }
  37. ],
  38. messages: {
  39. never: "Unexpected blank line between class members.",
  40. always: "Expected blank line between class members."
  41. }
  42. },
  43. create(context) {
  44. const options = [];
  45. options[0] = context.options[0] || "always";
  46. options[1] = context.options[1] || { exceptAfterSingleLine: false };
  47. const sourceCode = context.getSourceCode();
  48. /**
  49. * Gets a pair of tokens that should be used to check lines between two class member nodes.
  50. *
  51. * In most cases, this returns the very last token of the current node and
  52. * the very first token of the next node.
  53. * For example:
  54. *
  55. * class C {
  56. * x = 1; // curLast: `;` nextFirst: `in`
  57. * in = 2
  58. * }
  59. *
  60. * There is only one exception. If the given node ends with a semicolon, and it looks like
  61. * a semicolon-less style's semicolon - one that is not on the same line as the preceding
  62. * token, but is on the line where the next class member starts - this returns the preceding
  63. * token and the semicolon as boundary tokens.
  64. * For example:
  65. *
  66. * class C {
  67. * x = 1 // curLast: `1` nextFirst: `;`
  68. * ;in = 2
  69. * }
  70. * When determining the desired layout of the code, we should treat this semicolon as
  71. * a part of the next class member node instead of the one it technically belongs to.
  72. * @param {ASTNode} curNode Current class member node.
  73. * @param {ASTNode} nextNode Next class member node.
  74. * @returns {Token} The actual last token of `node`.
  75. * @private
  76. */
  77. function getBoundaryTokens(curNode, nextNode) {
  78. const lastToken = sourceCode.getLastToken(curNode);
  79. const prevToken = sourceCode.getTokenBefore(lastToken);
  80. const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes
  81. const isSemicolonLessStyle = (
  82. astUtils.isSemicolonToken(lastToken) &&
  83. !astUtils.isTokenOnSameLine(prevToken, lastToken) &&
  84. astUtils.isTokenOnSameLine(lastToken, nextToken)
  85. );
  86. return isSemicolonLessStyle
  87. ? { curLast: prevToken, nextFirst: lastToken }
  88. : { curLast: lastToken, nextFirst: nextToken };
  89. }
  90. /**
  91. * Return the last token among the consecutive tokens that have no exceed max line difference in between, before the first token in the next member.
  92. * @param {Token} prevLastToken The last token in the previous member node.
  93. * @param {Token} nextFirstToken The first token in the next member node.
  94. * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
  95. * @returns {Token} The last token among the consecutive tokens.
  96. */
  97. function findLastConsecutiveTokenAfter(prevLastToken, nextFirstToken, maxLine) {
  98. const after = sourceCode.getTokenAfter(prevLastToken, { includeComments: true });
  99. if (after !== nextFirstToken && after.loc.start.line - prevLastToken.loc.end.line <= maxLine) {
  100. return findLastConsecutiveTokenAfter(after, nextFirstToken, maxLine);
  101. }
  102. return prevLastToken;
  103. }
  104. /**
  105. * Return the first token among the consecutive tokens that have no exceed max line difference in between, after the last token in the previous member.
  106. * @param {Token} nextFirstToken The first token in the next member node.
  107. * @param {Token} prevLastToken The last token in the previous member node.
  108. * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
  109. * @returns {Token} The first token among the consecutive tokens.
  110. */
  111. function findFirstConsecutiveTokenBefore(nextFirstToken, prevLastToken, maxLine) {
  112. const before = sourceCode.getTokenBefore(nextFirstToken, { includeComments: true });
  113. if (before !== prevLastToken && nextFirstToken.loc.start.line - before.loc.end.line <= maxLine) {
  114. return findFirstConsecutiveTokenBefore(before, prevLastToken, maxLine);
  115. }
  116. return nextFirstToken;
  117. }
  118. /**
  119. * Checks if there is a token or comment between two tokens.
  120. * @param {Token} before The token before.
  121. * @param {Token} after The token after.
  122. * @returns {boolean} True if there is a token or comment between two tokens.
  123. */
  124. function hasTokenOrCommentBetween(before, after) {
  125. return sourceCode.getTokensBetween(before, after, { includeComments: true }).length !== 0;
  126. }
  127. return {
  128. ClassBody(node) {
  129. const body = node.body;
  130. for (let i = 0; i < body.length - 1; i++) {
  131. const curFirst = sourceCode.getFirstToken(body[i]);
  132. const { curLast, nextFirst } = getBoundaryTokens(body[i], body[i + 1]);
  133. const isMulti = !astUtils.isTokenOnSameLine(curFirst, curLast);
  134. const skip = !isMulti && options[1].exceptAfterSingleLine;
  135. const beforePadding = findLastConsecutiveTokenAfter(curLast, nextFirst, 1);
  136. const afterPadding = findFirstConsecutiveTokenBefore(nextFirst, curLast, 1);
  137. const isPadded = afterPadding.loc.start.line - beforePadding.loc.end.line > 1;
  138. const hasTokenInPadding = hasTokenOrCommentBetween(beforePadding, afterPadding);
  139. const curLineLastToken = findLastConsecutiveTokenAfter(curLast, nextFirst, 0);
  140. if ((options[0] === "always" && !skip && !isPadded) ||
  141. (options[0] === "never" && isPadded)) {
  142. context.report({
  143. node: body[i + 1],
  144. messageId: isPadded ? "never" : "always",
  145. fix(fixer) {
  146. if (hasTokenInPadding) {
  147. return null;
  148. }
  149. return isPadded
  150. ? fixer.replaceTextRange([beforePadding.range[1], afterPadding.range[0]], "\n")
  151. : fixer.insertTextAfter(curLineLastToken, "\n");
  152. }
  153. });
  154. }
  155. }
  156. }
  157. };
  158. }
  159. };