constructor-super.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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('../shared/types').Rule} */
  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: "Unexpected 'super()' because 'super' is not a constructor."
  127. }
  128. },
  129. create(context) {
  130. /*
  131. * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
  132. * Information for each constructor.
  133. * - upper: Information of the upper constructor.
  134. * - hasExtends: A flag which shows whether own class has a valid `extends`
  135. * part.
  136. * - scope: The scope of own class.
  137. * - codePath: The code path object of the constructor.
  138. */
  139. let funcInfo = null;
  140. /**
  141. * @type {Record<string, SegmentInfo>}
  142. */
  143. const segInfoMap = Object.create(null);
  144. /**
  145. * Gets the flag which shows `super()` is called in some paths.
  146. * @param {CodePathSegment} segment A code path segment to get.
  147. * @returns {boolean} The flag which shows `super()` is called in some paths
  148. */
  149. function isCalledInSomePath(segment) {
  150. return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
  151. }
  152. /**
  153. * Determines if a segment has been seen in the traversal.
  154. * @param {CodePathSegment} segment A code path segment to check.
  155. * @returns {boolean} `true` if the segment has been seen.
  156. */
  157. function hasSegmentBeenSeen(segment) {
  158. return !!segInfoMap[segment.id];
  159. }
  160. /**
  161. * Gets the flag which shows `super()` is called in all paths.
  162. * @param {CodePathSegment} segment A code path segment to get.
  163. * @returns {boolean} The flag which shows `super()` is called in all paths.
  164. */
  165. function isCalledInEveryPath(segment) {
  166. return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
  167. }
  168. return {
  169. /**
  170. * Stacks a constructor information.
  171. * @param {CodePath} codePath A code path which was started.
  172. * @param {ASTNode} node The current node.
  173. * @returns {void}
  174. */
  175. onCodePathStart(codePath, node) {
  176. if (isConstructorFunction(node)) {
  177. // Class > ClassBody > MethodDefinition > FunctionExpression
  178. const classNode = node.parent.parent.parent;
  179. const superClass = classNode.superClass;
  180. funcInfo = {
  181. upper: funcInfo,
  182. isConstructor: true,
  183. hasExtends: Boolean(superClass),
  184. superIsConstructor: isPossibleConstructor(superClass),
  185. codePath,
  186. currentSegments: new Set()
  187. };
  188. } else {
  189. funcInfo = {
  190. upper: funcInfo,
  191. isConstructor: false,
  192. hasExtends: false,
  193. superIsConstructor: false,
  194. codePath,
  195. currentSegments: new Set()
  196. };
  197. }
  198. },
  199. /**
  200. * Pops a constructor information.
  201. * And reports if `super()` lacked.
  202. * @param {CodePath} codePath A code path which was ended.
  203. * @param {ASTNode} node The current node.
  204. * @returns {void}
  205. */
  206. onCodePathEnd(codePath, node) {
  207. const hasExtends = funcInfo.hasExtends;
  208. // Pop.
  209. funcInfo = funcInfo.upper;
  210. if (!hasExtends) {
  211. return;
  212. }
  213. // Reports if `super()` lacked.
  214. const returnedSegments = codePath.returnedSegments;
  215. const calledInEveryPaths = returnedSegments.every(isCalledInEveryPath);
  216. const calledInSomePaths = returnedSegments.some(isCalledInSomePath);
  217. if (!calledInEveryPaths) {
  218. context.report({
  219. messageId: calledInSomePaths
  220. ? "missingSome"
  221. : "missingAll",
  222. node: node.parent
  223. });
  224. }
  225. },
  226. /**
  227. * Initialize information of a given code path segment.
  228. * @param {CodePathSegment} segment A code path segment to initialize.
  229. * @param {CodePathSegment} node Node that starts the segment.
  230. * @returns {void}
  231. */
  232. onCodePathSegmentStart(segment, node) {
  233. funcInfo.currentSegments.add(segment);
  234. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  235. return;
  236. }
  237. // Initialize info.
  238. const info = segInfoMap[segment.id] = new SegmentInfo();
  239. const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
  240. // When there are previous segments, aggregates these.
  241. if (seenPrevSegments.length > 0) {
  242. info.calledInSomePaths = seenPrevSegments.some(isCalledInSomePath);
  243. info.calledInEveryPaths = seenPrevSegments.every(isCalledInEveryPath);
  244. }
  245. /*
  246. * ForStatement > *.update segments are a special case as they are created in advance,
  247. * without seen previous segments. Since they logically don't affect `calledInEveryPaths`
  248. * calculations, and they can never be a lone previous segment of another one, we'll set
  249. * their `calledInEveryPaths` to `true` to effectively ignore them in those calculations.
  250. * .
  251. */
  252. if (node.parent && node.parent.type === "ForStatement" && node.parent.update === node) {
  253. info.calledInEveryPaths = true;
  254. }
  255. },
  256. onUnreachableCodePathSegmentStart(segment) {
  257. funcInfo.currentSegments.add(segment);
  258. },
  259. onUnreachableCodePathSegmentEnd(segment) {
  260. funcInfo.currentSegments.delete(segment);
  261. },
  262. onCodePathSegmentEnd(segment) {
  263. funcInfo.currentSegments.delete(segment);
  264. },
  265. /**
  266. * Update information of the code path segment when a code path was
  267. * looped.
  268. * @param {CodePathSegment} fromSegment The code path segment of the
  269. * end of a loop.
  270. * @param {CodePathSegment} toSegment A code path segment of the head
  271. * of a loop.
  272. * @returns {void}
  273. */
  274. onCodePathSegmentLoop(fromSegment, toSegment) {
  275. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  276. return;
  277. }
  278. funcInfo.codePath.traverseSegments(
  279. { first: toSegment, last: fromSegment },
  280. (segment, controller) => {
  281. const info = segInfoMap[segment.id];
  282. // skip segments after the loop
  283. if (!info) {
  284. controller.skip();
  285. return;
  286. }
  287. const seenPrevSegments = segment.prevSegments.filter(hasSegmentBeenSeen);
  288. const calledInSomePreviousPaths = seenPrevSegments.some(isCalledInSomePath);
  289. const calledInEveryPreviousPaths = seenPrevSegments.every(isCalledInEveryPath);
  290. info.calledInSomePaths ||= calledInSomePreviousPaths;
  291. info.calledInEveryPaths ||= calledInEveryPreviousPaths;
  292. // If flags become true anew, reports the valid nodes.
  293. if (calledInSomePreviousPaths) {
  294. const nodes = info.validNodes;
  295. info.validNodes = [];
  296. for (let i = 0; i < nodes.length; ++i) {
  297. const node = nodes[i];
  298. context.report({
  299. messageId: "duplicate",
  300. node
  301. });
  302. }
  303. }
  304. }
  305. );
  306. },
  307. /**
  308. * Checks for a call of `super()`.
  309. * @param {ASTNode} node A CallExpression node to check.
  310. * @returns {void}
  311. */
  312. "CallExpression:exit"(node) {
  313. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  314. return;
  315. }
  316. // Skips except `super()`.
  317. if (node.callee.type !== "Super") {
  318. return;
  319. }
  320. // Reports if needed.
  321. const segments = funcInfo.currentSegments;
  322. let duplicate = false;
  323. let info = null;
  324. for (const segment of segments) {
  325. if (segment.reachable) {
  326. info = segInfoMap[segment.id];
  327. duplicate = duplicate || info.calledInSomePaths;
  328. info.calledInSomePaths = info.calledInEveryPaths = true;
  329. }
  330. }
  331. if (info) {
  332. if (duplicate) {
  333. context.report({
  334. messageId: "duplicate",
  335. node
  336. });
  337. } else if (!funcInfo.superIsConstructor) {
  338. context.report({
  339. messageId: "badSuper",
  340. node
  341. });
  342. } else {
  343. info.validNodes.push(node);
  344. }
  345. }
  346. },
  347. /**
  348. * Set the mark to the returned path as `super()` was called.
  349. * @param {ASTNode} node A ReturnStatement node to check.
  350. * @returns {void}
  351. */
  352. ReturnStatement(node) {
  353. if (!(funcInfo.isConstructor && funcInfo.hasExtends)) {
  354. return;
  355. }
  356. // Skips if no argument.
  357. if (!node.argument) {
  358. return;
  359. }
  360. // Returning argument is a substitute of 'super()'.
  361. const segments = funcInfo.currentSegments;
  362. for (const segment of segments) {
  363. if (segment.reachable) {
  364. const info = segInfoMap[segment.id];
  365. info.calledInSomePaths = info.calledInEveryPaths = true;
  366. }
  367. }
  368. }
  369. };
  370. }
  371. };