lines-between-class-members.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. /**
  2. * @fileoverview Rule to check empty newline between class members
  3. * @author 薛定谔的猫<hh_2013@foxmail.com>
  4. * @deprecated in ESLint v8.53.0
  5. */
  6. "use strict";
  7. //------------------------------------------------------------------------------
  8. // Requirements
  9. //------------------------------------------------------------------------------
  10. const astUtils = require("./utils/ast-utils");
  11. //------------------------------------------------------------------------------
  12. // Helpers
  13. //------------------------------------------------------------------------------
  14. /**
  15. * Types of class members.
  16. * Those have `test` method to check it matches to the given class member.
  17. * @private
  18. */
  19. const ClassMemberTypes = {
  20. "*": { test: () => true },
  21. field: { test: node => node.type === "PropertyDefinition" },
  22. method: { test: node => node.type === "MethodDefinition" },
  23. };
  24. //------------------------------------------------------------------------------
  25. // Rule Definition
  26. //------------------------------------------------------------------------------
  27. /** @type {import('../types').Rule.RuleModule} */
  28. module.exports = {
  29. meta: {
  30. deprecated: {
  31. message: "Formatting rules are being moved out of ESLint core.",
  32. url: "https://eslint.org/blog/2023/10/deprecating-formatting-rules/",
  33. deprecatedSince: "8.53.0",
  34. availableUntil: "11.0.0",
  35. replacedBy: [
  36. {
  37. message:
  38. "ESLint Stylistic now maintains deprecated stylistic core rules.",
  39. url: "https://eslint.style/guide/migration",
  40. plugin: {
  41. name: "@stylistic/eslint-plugin",
  42. url: "https://eslint.style",
  43. },
  44. rule: {
  45. name: "lines-between-class-members",
  46. url: "https://eslint.style/rules/lines-between-class-members",
  47. },
  48. },
  49. ],
  50. },
  51. type: "layout",
  52. docs: {
  53. description:
  54. "Require or disallow an empty line between class members",
  55. recommended: false,
  56. url: "https://eslint.org/docs/latest/rules/lines-between-class-members",
  57. },
  58. fixable: "whitespace",
  59. schema: [
  60. {
  61. anyOf: [
  62. {
  63. type: "object",
  64. properties: {
  65. enforce: {
  66. type: "array",
  67. items: {
  68. type: "object",
  69. properties: {
  70. blankLine: {
  71. enum: ["always", "never"],
  72. },
  73. prev: {
  74. enum: ["method", "field", "*"],
  75. },
  76. next: {
  77. enum: ["method", "field", "*"],
  78. },
  79. },
  80. additionalProperties: false,
  81. required: ["blankLine", "prev", "next"],
  82. },
  83. minItems: 1,
  84. },
  85. },
  86. additionalProperties: false,
  87. required: ["enforce"],
  88. },
  89. {
  90. enum: ["always", "never"],
  91. },
  92. ],
  93. },
  94. {
  95. type: "object",
  96. properties: {
  97. exceptAfterSingleLine: {
  98. type: "boolean",
  99. default: false,
  100. },
  101. },
  102. additionalProperties: false,
  103. },
  104. ],
  105. messages: {
  106. never: "Unexpected blank line between class members.",
  107. always: "Expected blank line between class members.",
  108. },
  109. },
  110. create(context) {
  111. const options = [];
  112. options[0] = context.options[0] || "always";
  113. options[1] = context.options[1] || { exceptAfterSingleLine: false };
  114. const configureList =
  115. typeof options[0] === "object"
  116. ? options[0].enforce
  117. : [{ blankLine: options[0], prev: "*", next: "*" }];
  118. const sourceCode = context.sourceCode;
  119. /**
  120. * Gets a pair of tokens that should be used to check lines between two class member nodes.
  121. *
  122. * In most cases, this returns the very last token of the current node and
  123. * the very first token of the next node.
  124. * For example:
  125. *
  126. * class C {
  127. * x = 1; // curLast: `;` nextFirst: `in`
  128. * in = 2
  129. * }
  130. *
  131. * There is only one exception. If the given node ends with a semicolon, and it looks like
  132. * a semicolon-less style's semicolon - one that is not on the same line as the preceding
  133. * token, but is on the line where the next class member starts - this returns the preceding
  134. * token and the semicolon as boundary tokens.
  135. * For example:
  136. *
  137. * class C {
  138. * x = 1 // curLast: `1` nextFirst: `;`
  139. * ;in = 2
  140. * }
  141. * When determining the desired layout of the code, we should treat this semicolon as
  142. * a part of the next class member node instead of the one it technically belongs to.
  143. * @param {ASTNode} curNode Current class member node.
  144. * @param {ASTNode} nextNode Next class member node.
  145. * @returns {Token} The actual last token of `node`.
  146. * @private
  147. */
  148. function getBoundaryTokens(curNode, nextNode) {
  149. const lastToken = sourceCode.getLastToken(curNode);
  150. const prevToken = sourceCode.getTokenBefore(lastToken);
  151. const nextToken = sourceCode.getFirstToken(nextNode); // skip possible lone `;` between nodes
  152. const isSemicolonLessStyle =
  153. astUtils.isSemicolonToken(lastToken) &&
  154. !astUtils.isTokenOnSameLine(prevToken, lastToken) &&
  155. astUtils.isTokenOnSameLine(lastToken, nextToken);
  156. return isSemicolonLessStyle
  157. ? { curLast: prevToken, nextFirst: lastToken }
  158. : { curLast: lastToken, nextFirst: nextToken };
  159. }
  160. /**
  161. * 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.
  162. * @param {Token} prevLastToken The last token in the previous member node.
  163. * @param {Token} nextFirstToken The first token in the next member node.
  164. * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
  165. * @returns {Token} The last token among the consecutive tokens.
  166. */
  167. function findLastConsecutiveTokenAfter(
  168. prevLastToken,
  169. nextFirstToken,
  170. maxLine,
  171. ) {
  172. const after = sourceCode.getTokenAfter(prevLastToken, {
  173. includeComments: true,
  174. });
  175. if (
  176. after !== nextFirstToken &&
  177. after.loc.start.line - prevLastToken.loc.end.line <= maxLine
  178. ) {
  179. return findLastConsecutiveTokenAfter(
  180. after,
  181. nextFirstToken,
  182. maxLine,
  183. );
  184. }
  185. return prevLastToken;
  186. }
  187. /**
  188. * 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.
  189. * @param {Token} nextFirstToken The first token in the next member node.
  190. * @param {Token} prevLastToken The last token in the previous member node.
  191. * @param {number} maxLine The maximum number of allowed line difference between consecutive tokens.
  192. * @returns {Token} The first token among the consecutive tokens.
  193. */
  194. function findFirstConsecutiveTokenBefore(
  195. nextFirstToken,
  196. prevLastToken,
  197. maxLine,
  198. ) {
  199. const before = sourceCode.getTokenBefore(nextFirstToken, {
  200. includeComments: true,
  201. });
  202. if (
  203. before !== prevLastToken &&
  204. nextFirstToken.loc.start.line - before.loc.end.line <= maxLine
  205. ) {
  206. return findFirstConsecutiveTokenBefore(
  207. before,
  208. prevLastToken,
  209. maxLine,
  210. );
  211. }
  212. return nextFirstToken;
  213. }
  214. /**
  215. * Checks if there is a token or comment between two tokens.
  216. * @param {Token} before The token before.
  217. * @param {Token} after The token after.
  218. * @returns {boolean} True if there is a token or comment between two tokens.
  219. */
  220. function hasTokenOrCommentBetween(before, after) {
  221. return (
  222. sourceCode.getTokensBetween(before, after, {
  223. includeComments: true,
  224. }).length !== 0
  225. );
  226. }
  227. /**
  228. * Checks whether the given node matches the given type.
  229. * @param {ASTNode} node The class member node to check.
  230. * @param {string} type The class member type to check.
  231. * @returns {boolean} `true` if the class member node matched the type.
  232. * @private
  233. */
  234. function match(node, type) {
  235. return ClassMemberTypes[type].test(node);
  236. }
  237. /**
  238. * Finds the last matched configuration from the configureList.
  239. * @param {ASTNode} prevNode The previous node to match.
  240. * @param {ASTNode} nextNode The current node to match.
  241. * @returns {string|null} Padding type or `null` if no matches were found.
  242. * @private
  243. */
  244. function getPaddingType(prevNode, nextNode) {
  245. for (let i = configureList.length - 1; i >= 0; --i) {
  246. const configure = configureList[i];
  247. const matched =
  248. match(prevNode, configure.prev) &&
  249. match(nextNode, configure.next);
  250. if (matched) {
  251. return configure.blankLine;
  252. }
  253. }
  254. return null;
  255. }
  256. return {
  257. ClassBody(node) {
  258. const body = node.body;
  259. for (let i = 0; i < body.length - 1; i++) {
  260. const curFirst = sourceCode.getFirstToken(body[i]);
  261. const { curLast, nextFirst } = getBoundaryTokens(
  262. body[i],
  263. body[i + 1],
  264. );
  265. const isMulti = !astUtils.isTokenOnSameLine(
  266. curFirst,
  267. curLast,
  268. );
  269. const skip = !isMulti && options[1].exceptAfterSingleLine;
  270. const beforePadding = findLastConsecutiveTokenAfter(
  271. curLast,
  272. nextFirst,
  273. 1,
  274. );
  275. const afterPadding = findFirstConsecutiveTokenBefore(
  276. nextFirst,
  277. curLast,
  278. 1,
  279. );
  280. const isPadded =
  281. afterPadding.loc.start.line -
  282. beforePadding.loc.end.line >
  283. 1;
  284. const hasTokenInPadding = hasTokenOrCommentBetween(
  285. beforePadding,
  286. afterPadding,
  287. );
  288. const curLineLastToken = findLastConsecutiveTokenAfter(
  289. curLast,
  290. nextFirst,
  291. 0,
  292. );
  293. const paddingType = getPaddingType(body[i], body[i + 1]);
  294. if (paddingType === "never" && isPadded) {
  295. context.report({
  296. node: body[i + 1],
  297. messageId: "never",
  298. fix(fixer) {
  299. if (hasTokenInPadding) {
  300. return null;
  301. }
  302. return fixer.replaceTextRange(
  303. [
  304. beforePadding.range[1],
  305. afterPadding.range[0],
  306. ],
  307. "\n",
  308. );
  309. },
  310. });
  311. } else if (paddingType === "always" && !skip && !isPadded) {
  312. context.report({
  313. node: body[i + 1],
  314. messageId: "always",
  315. fix(fixer) {
  316. if (hasTokenInPadding) {
  317. return null;
  318. }
  319. return fixer.insertTextAfter(
  320. curLineLastToken,
  321. "\n",
  322. );
  323. },
  324. });
  325. }
  326. }
  327. },
  328. };
  329. },
  330. };