constructor-super.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. /**
  2. * @fileoverview A rule to verify `super()` callings in constructor.
  3. * @author Toru Nagashima
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Helpers
  8. //------------------------------------------------------------------------------
  9. /**
  10. * Checks whether or not a given node is a constructor.
  11. * @param {ASTNode} node A node to check. This node type is one of
  12. * `Program`, `FunctionDeclaration`, `FunctionExpression`, and
  13. * `ArrowFunctionExpression`.
  14. * @returns {boolean} `true` if the node is a constructor.
  15. */
  16. function isConstructorFunction(node) {
  17. return (
  18. node.type === "FunctionExpression" &&
  19. node.parent.type === "MethodDefinition" &&
  20. node.parent.kind === "constructor"
  21. );
  22. }
  23. /**
  24. * Checks whether a given node can be a constructor or not.
  25. * @param {ASTNode} node A node to check.
  26. * @returns {boolean} `true` if the node can be a constructor.
  27. */
  28. function isPossibleConstructor(node) {
  29. if (!node) {
  30. return false;
  31. }
  32. switch (node.type) {
  33. case "ClassExpression":
  34. case "FunctionExpression":
  35. case "ThisExpression":
  36. case "MemberExpression":
  37. case "CallExpression":
  38. case "NewExpression":
  39. case "ChainExpression":
  40. case "YieldExpression":
  41. case "TaggedTemplateExpression":
  42. case "MetaProperty":
  43. return true;
  44. case "Identifier":
  45. return node.name !== "undefined";
  46. case "AssignmentExpression":
  47. if (["=", "&&="].includes(node.operator)) {
  48. return isPossibleConstructor(node.right);
  49. }
  50. if (["||=", "??="].includes(node.operator)) {
  51. return (
  52. isPossibleConstructor(node.left) ||
  53. isPossibleConstructor(node.right)
  54. );
  55. }
  56. /**
  57. * All other assignment operators are mathematical assignment operators (arithmetic or bitwise).
  58. * An assignment expression with a mathematical operator can either evaluate to a primitive value,
  59. * or throw, depending on the operands. Thus, it cannot evaluate to a constructor function.
  60. */
  61. return false;
  62. case "LogicalExpression":
  63. /*
  64. * If the && operator short-circuits, the left side was falsy and therefore not a constructor, and if
  65. * it doesn't short-circuit, it takes the value from the right side, so the right side must always be a
  66. * possible constructor. A future improvement could verify that the left side could be truthy by
  67. * excluding falsy literals.
  68. */
  69. if (node.operator === "&&") {
  70. return isPossibleConstructor(node.right);
  71. }
  72. return (
  73. isPossibleConstructor(node.left) ||
  74. isPossibleConstructor(node.right)
  75. );
  76. case "ConditionalExpression":
  77. return (
  78. isPossibleConstructor(node.alternate) ||
  79. isPossibleConstructor(node.consequent)
  80. );
  81. case "SequenceExpression": {
  82. const lastExpression = node.expressions.at(-1);
  83. return isPossibleConstructor(lastExpression);
  84. }
  85. default:
  86. return false;
  87. }
  88. }
  89. /**
  90. * A class to store information about a code path segment.
  91. */
  92. class SegmentInfo {
  93. /**
  94. * Indicates if super() is called in all code paths.
  95. * @type {boolean}
  96. */
  97. calledInEveryPaths = false;
  98. /**
  99. * Indicates if super() is called in any code paths.
  100. * @type {boolean}
  101. */
  102. calledInSomePaths = false;
  103. /**
  104. * The nodes which have been validated and don't need to be reconsidered.
  105. * @type {ASTNode[]}
  106. */
  107. validNodes = [];
  108. }
  109. //------------------------------------------------------------------------------
  110. // Rule Definition
  111. //------------------------------------------------------------------------------
  112. /** @type {import('../types').Rule.RuleModule} */
  113. module.exports = {
  114. meta: {
  115. type: "problem",
  116. docs: {
  117. description: "Require `super()` calls in constructors",
  118. recommended: true,
  119. url: "https://eslint.org/docs/latest/rules/constructor-super",
  120. },
  121. schema: [],
  122. messages: {
  123. missingSome: "Lacked a call of 'super()' in some code paths.",
  124. missingAll: "Expected to call 'super()'.",
  125. duplicate: "Unexpected duplicate 'super()'.",
  126. badSuper:
  127. "Unexpected 'super()' because 'super' is not a constructor.",
  128. },
  129. },
  130. create(context) {
  131. /*
  132. * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
  133. * Information for each constructor.
  134. * - upper: Information of the upper constructor.
  135. * - hasExtends: A flag which shows whether own class has a valid `extends`
  136. * part.
  137. * - scope: The scope of own class.
  138. * - codePath: The code path object of the constructor.
  139. */
  140. let funcInfo = null;
  141. /**
  142. * @type {Record<string, SegmentInfo>}
  143. */
  144. const segInfoMap = Object.create(null);
  145. /**
  146. * Gets the flag which shows `super()` is called in some paths.
  147. * @param {CodePathSegment} segment A code path segment to get.
  148. * @returns {boolean} The flag which shows `super()` is called in some paths
  149. */
  150. function isCalledInSomePath(segment) {
  151. return (
  152. segment.reachable && segInfoMap[segment.id].calledInSomePaths
  153. );
  154. }
  155. /**
  156. * Determines if a segment has been seen in the traversal.
  157. * @param {CodePathSegment} segment A code path segment to check.
  158. * @returns {boolean} `true` if the segment has been seen.
  159. */
  160. function hasSegmentBeenSeen(segment) {
  161. return !!segInfoMap[segment.id];
  162. }
  163. /**
  164. * Gets the flag which shows `super()` is called in all paths.
  165. * @param {CodePathSegment} segment A code path segment to get.
  166. * @returns {boolean} The flag which shows `super()` is called in all paths.
  167. */
  168. function isCalledInEveryPath(segment) {
  169. return (
  170. segment.reachable && segInfoMap[segment.id].calledInEveryPaths
  171. );
  172. }
  173. return {
  174. /**
  175. * Stacks a constructor information.
  176. * @param {CodePath} codePath A code path which was started.
  177. * @param {ASTNode} node The current node.
  178. * @returns {void}
  179. */
  180. onCodePathStart(codePath, node) {
  181. if (isConstructorFunction(node)) {
  182. // Class > ClassBody > MethodDefinition > FunctionExpression
  183. const classNode = node.parent.parent.parent;
  184. const superClass = classNode.superClass;
  185. funcInfo = {
  186. upper: funcInfo,
  187. isConstructor: true,
  188. hasExtends: Boolean(superClass),
  189. superIsConstructor: isPossibleConstructor(superClass),
  190. codePath,
  191. currentSegments: new Set(),
  192. };
  193. } else {
  194. funcInfo = {
  195. upper: funcInfo,
  196. isConstructor: false,
  197. hasExtends: false,
  198. superIsConstructor: false,
  199. codePath,
  200. currentSegments: new Set(),
  201. };
  202. }
  203. },
  204. /**
  205. * Pops a constructor information.
  206. * And reports if `super()` lacked.
  207. * @param {CodePath} codePath A code path which was ended.
  208. * @param {ASTNode} node The current node.
  209. * @returns {void}
  210. */
  211. onCodePathEnd(codePath, node) {
  212. const hasExtends = funcInfo.hasExtends;
  213. // Pop.
  214. funcInfo = funcInfo.upper;
  215. if (!hasExtends) {
  216. return;
  217. }
  218. // Reports if `super()` lacked.
  219. const returnedSegments = codePath.returnedSegments;
  220. const calledInEveryPaths =
  221. returnedSegments.every(isCalledInEveryPath);
  222. const calledInSomePaths =
  223. returnedSegments.some(isCalledInSomePath);
  224. if (!calledInEveryPaths) {
  225. context.report({
  226. messageId: calledInSomePaths
  227. ? "missingSome"
  228. : "missingAll",
  229. node: node.parent,
  230. });
  231. }
  232. },
  233. /**
  234. * Initialize information of a given code path segment.
  235. * @param {CodePathSegment} segment A code path segment to initialize.
  236. * @param {CodePathSegment} node Node that starts the segment.
  237. * @returns {void}
  238. */
  239. onCodePathSegmentStart(segment, node) {
  240. funcInfo.currentSegments.add(segment);
  241. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  242. return;
  243. }
  244. // Initialize info.
  245. const info = (segInfoMap[segment.id] = new SegmentInfo());
  246. const seenPrevSegments =
  247. segment.prevSegments.filter(hasSegmentBeenSeen);
  248. // When there are previous segments, aggregates these.
  249. if (seenPrevSegments.length > 0) {
  250. info.calledInSomePaths =
  251. seenPrevSegments.some(isCalledInSomePath);
  252. info.calledInEveryPaths =
  253. seenPrevSegments.every(isCalledInEveryPath);
  254. }
  255. /*
  256. * ForStatement > *.update segments are a special case as they are created in advance,
  257. * without seen previous segments. Since they logically don't affect `calledInEveryPaths`
  258. * calculations, and they can never be a lone previous segment of another one, we'll set
  259. * their `calledInEveryPaths` to `true` to effectively ignore them in those calculations.
  260. * .
  261. */
  262. if (
  263. node.parent &&
  264. node.parent.type === "ForStatement" &&
  265. node.parent.update === node
  266. ) {
  267. info.calledInEveryPaths = true;
  268. }
  269. },
  270. onUnreachableCodePathSegmentStart(segment) {
  271. funcInfo.currentSegments.add(segment);
  272. },
  273. onUnreachableCodePathSegmentEnd(segment) {
  274. funcInfo.currentSegments.delete(segment);
  275. },
  276. onCodePathSegmentEnd(segment) {
  277. funcInfo.currentSegments.delete(segment);
  278. },
  279. /**
  280. * Update information of the code path segment when a code path was
  281. * looped.
  282. * @param {CodePathSegment} fromSegment The code path segment of the
  283. * end of a loop.
  284. * @param {CodePathSegment} toSegment A code path segment of the head
  285. * of a loop.
  286. * @returns {void}
  287. */
  288. onCodePathSegmentLoop(fromSegment, toSegment) {
  289. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  290. return;
  291. }
  292. funcInfo.codePath.traverseSegments(
  293. { first: toSegment, last: fromSegment },
  294. (segment, controller) => {
  295. const info = segInfoMap[segment.id];
  296. // skip segments after the loop
  297. if (!info) {
  298. controller.skip();
  299. return;
  300. }
  301. const seenPrevSegments =
  302. segment.prevSegments.filter(hasSegmentBeenSeen);
  303. const calledInSomePreviousPaths =
  304. seenPrevSegments.some(isCalledInSomePath);
  305. const calledInEveryPreviousPaths =
  306. seenPrevSegments.every(isCalledInEveryPath);
  307. info.calledInSomePaths ||= calledInSomePreviousPaths;
  308. info.calledInEveryPaths ||= calledInEveryPreviousPaths;
  309. // If flags become true anew, reports the valid nodes.
  310. if (calledInSomePreviousPaths) {
  311. const nodes = info.validNodes;
  312. info.validNodes = [];
  313. for (let i = 0; i < nodes.length; ++i) {
  314. const node = nodes[i];
  315. context.report({
  316. messageId: "duplicate",
  317. node,
  318. });
  319. }
  320. }
  321. },
  322. );
  323. },
  324. /**
  325. * Checks for a call of `super()`.
  326. * @param {ASTNode} node A CallExpression node to check.
  327. * @returns {void}
  328. */
  329. "CallExpression:exit"(node) {
  330. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  331. return;
  332. }
  333. // Skips except `super()`.
  334. if (node.callee.type !== "Super") {
  335. return;
  336. }
  337. // Reports if needed.
  338. const segments = funcInfo.currentSegments;
  339. let duplicate = false;
  340. let info = null;
  341. for (const segment of segments) {
  342. if (segment.reachable) {
  343. info = segInfoMap[segment.id];
  344. duplicate = duplicate || info.calledInSomePaths;
  345. info.calledInSomePaths = info.calledInEveryPaths = true;
  346. }
  347. }
  348. if (info) {
  349. if (duplicate) {
  350. context.report({
  351. messageId: "duplicate",
  352. node,
  353. });
  354. } else if (!funcInfo.superIsConstructor) {
  355. context.report({
  356. messageId: "badSuper",
  357. node,
  358. });
  359. } else {
  360. info.validNodes.push(node);
  361. }
  362. }
  363. },
  364. /**
  365. * Set the mark to the returned path as `super()` was called.
  366. * @param {ASTNode} node A ReturnStatement node to check.
  367. * @returns {void}
  368. */
  369. ReturnStatement(node) {
  370. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  371. return;
  372. }
  373. // Skips if no argument.
  374. if (!node.argument) {
  375. return;
  376. }
  377. // Returning argument is a substitute of 'super()'.
  378. const segments = funcInfo.currentSegments;
  379. for (const segment of segments) {
  380. if (segment.reachable) {
  381. const info = segInfoMap[segment.id];
  382. info.calledInSomePaths = info.calledInEveryPaths = true;
  383. }
  384. }
  385. },
  386. };
  387. },
  388. };