You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
14 KiB

4 years ago
  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 a given code path segment is reachable or not.
  11. * @param {CodePathSegment} segment A code path segment to check.
  12. * @returns {boolean} `true` if the segment is reachable.
  13. */
  14. function isReachable(segment) {
  15. return segment.reachable;
  16. }
  17. /**
  18. * Checks whether or not a given node is a constructor.
  19. * @param {ASTNode} node A node to check. This node type is one of
  20. * `Program`, `FunctionDeclaration`, `FunctionExpression`, and
  21. * `ArrowFunctionExpression`.
  22. * @returns {boolean} `true` if the node is a constructor.
  23. */
  24. function isConstructorFunction(node) {
  25. return (
  26. node.type === "FunctionExpression" &&
  27. node.parent.type === "MethodDefinition" &&
  28. node.parent.kind === "constructor"
  29. );
  30. }
  31. /**
  32. * Checks whether a given node can be a constructor or not.
  33. * @param {ASTNode} node A node to check.
  34. * @returns {boolean} `true` if the node can be a constructor.
  35. */
  36. function isPossibleConstructor(node) {
  37. if (!node) {
  38. return false;
  39. }
  40. switch (node.type) {
  41. case "ClassExpression":
  42. case "FunctionExpression":
  43. case "ThisExpression":
  44. case "MemberExpression":
  45. case "CallExpression":
  46. case "NewExpression":
  47. case "ChainExpression":
  48. case "YieldExpression":
  49. case "TaggedTemplateExpression":
  50. case "MetaProperty":
  51. return true;
  52. case "Identifier":
  53. return node.name !== "undefined";
  54. case "AssignmentExpression":
  55. return isPossibleConstructor(node.right);
  56. case "LogicalExpression":
  57. return (
  58. isPossibleConstructor(node.left) ||
  59. isPossibleConstructor(node.right)
  60. );
  61. case "ConditionalExpression":
  62. return (
  63. isPossibleConstructor(node.alternate) ||
  64. isPossibleConstructor(node.consequent)
  65. );
  66. case "SequenceExpression": {
  67. const lastExpression = node.expressions[node.expressions.length - 1];
  68. return isPossibleConstructor(lastExpression);
  69. }
  70. default:
  71. return false;
  72. }
  73. }
  74. //------------------------------------------------------------------------------
  75. // Rule Definition
  76. //------------------------------------------------------------------------------
  77. module.exports = {
  78. meta: {
  79. type: "problem",
  80. docs: {
  81. description: "require `super()` calls in constructors",
  82. category: "ECMAScript 6",
  83. recommended: true,
  84. url: "https://eslint.org/docs/rules/constructor-super"
  85. },
  86. schema: [],
  87. messages: {
  88. missingSome: "Lacked a call of 'super()' in some code paths.",
  89. missingAll: "Expected to call 'super()'.",
  90. duplicate: "Unexpected duplicate 'super()'.",
  91. badSuper: "Unexpected 'super()' because 'super' is not a constructor.",
  92. unexpected: "Unexpected 'super()'."
  93. }
  94. },
  95. create(context) {
  96. /*
  97. * {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
  98. * Information for each constructor.
  99. * - upper: Information of the upper constructor.
  100. * - hasExtends: A flag which shows whether own class has a valid `extends`
  101. * part.
  102. * - scope: The scope of own class.
  103. * - codePath: The code path object of the constructor.
  104. */
  105. let funcInfo = null;
  106. /*
  107. * {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
  108. * Information for each code path segment.
  109. * - calledInSomePaths: A flag of be called `super()` in some code paths.
  110. * - calledInEveryPaths: A flag of be called `super()` in all code paths.
  111. * - validNodes:
  112. */
  113. let segInfoMap = Object.create(null);
  114. /**
  115. * Gets the flag which shows `super()` is called in some paths.
  116. * @param {CodePathSegment} segment A code path segment to get.
  117. * @returns {boolean} The flag which shows `super()` is called in some paths
  118. */
  119. function isCalledInSomePath(segment) {
  120. return segment.reachable && segInfoMap[segment.id].calledInSomePaths;
  121. }
  122. /**
  123. * Gets the flag which shows `super()` is called in all paths.
  124. * @param {CodePathSegment} segment A code path segment to get.
  125. * @returns {boolean} The flag which shows `super()` is called in all paths.
  126. */
  127. function isCalledInEveryPath(segment) {
  128. /*
  129. * If specific segment is the looped segment of the current segment,
  130. * skip the segment.
  131. * If not skipped, this never becomes true after a loop.
  132. */
  133. if (segment.nextSegments.length === 1 &&
  134. segment.nextSegments[0].isLoopedPrevSegment(segment)
  135. ) {
  136. return true;
  137. }
  138. return segment.reachable && segInfoMap[segment.id].calledInEveryPaths;
  139. }
  140. return {
  141. /**
  142. * Stacks a constructor information.
  143. * @param {CodePath} codePath A code path which was started.
  144. * @param {ASTNode} node The current node.
  145. * @returns {void}
  146. */
  147. onCodePathStart(codePath, node) {
  148. if (isConstructorFunction(node)) {
  149. // Class > ClassBody > MethodDefinition > FunctionExpression
  150. const classNode = node.parent.parent.parent;
  151. const superClass = classNode.superClass;
  152. funcInfo = {
  153. upper: funcInfo,
  154. isConstructor: true,
  155. hasExtends: Boolean(superClass),
  156. superIsConstructor: isPossibleConstructor(superClass),
  157. codePath
  158. };
  159. } else {
  160. funcInfo = {
  161. upper: funcInfo,
  162. isConstructor: false,
  163. hasExtends: false,
  164. superIsConstructor: false,
  165. codePath
  166. };
  167. }
  168. },
  169. /**
  170. * Pops a constructor information.
  171. * And reports if `super()` lacked.
  172. * @param {CodePath} codePath A code path which was ended.
  173. * @param {ASTNode} node The current node.
  174. * @returns {void}
  175. */
  176. onCodePathEnd(codePath, node) {
  177. const hasExtends = funcInfo.hasExtends;
  178. // Pop.
  179. funcInfo = funcInfo.upper;
  180. if (!hasExtends) {
  181. return;
  182. }
  183. // Reports if `super()` lacked.
  184. const segments = codePath.returnedSegments;
  185. const calledInEveryPaths = segments.every(isCalledInEveryPath);
  186. const calledInSomePaths = segments.some(isCalledInSomePath);
  187. if (!calledInEveryPaths) {
  188. context.report({
  189. messageId: calledInSomePaths
  190. ? "missingSome"
  191. : "missingAll",
  192. node: node.parent
  193. });
  194. }
  195. },
  196. /**
  197. * Initialize information of a given code path segment.
  198. * @param {CodePathSegment} segment A code path segment to initialize.
  199. * @returns {void}
  200. */
  201. onCodePathSegmentStart(segment) {
  202. if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
  203. return;
  204. }
  205. // Initialize info.
  206. const info = segInfoMap[segment.id] = {
  207. calledInSomePaths: false,
  208. calledInEveryPaths: false,
  209. validNodes: []
  210. };
  211. // When there are previous segments, aggregates these.
  212. const prevSegments = segment.prevSegments;
  213. if (prevSegments.length > 0) {
  214. info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
  215. info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
  216. }
  217. },
  218. /**
  219. * Update information of the code path segment when a code path was
  220. * looped.
  221. * @param {CodePathSegment} fromSegment The code path segment of the
  222. * end of a loop.
  223. * @param {CodePathSegment} toSegment A code path segment of the head
  224. * of a loop.
  225. * @returns {void}
  226. */
  227. onCodePathSegmentLoop(fromSegment, toSegment) {
  228. if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
  229. return;
  230. }
  231. // Update information inside of the loop.
  232. const isRealLoop = toSegment.prevSegments.length >= 2;
  233. funcInfo.codePath.traverseSegments(
  234. { first: toSegment, last: fromSegment },
  235. segment => {
  236. const info = segInfoMap[segment.id];
  237. const prevSegments = segment.prevSegments;
  238. // Updates flags.
  239. info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
  240. info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
  241. // If flags become true anew, reports the valid nodes.
  242. if (info.calledInSomePaths || isRealLoop) {
  243. const nodes = info.validNodes;
  244. info.validNodes = [];
  245. for (let i = 0; i < nodes.length; ++i) {
  246. const node = nodes[i];
  247. context.report({
  248. messageId: "duplicate",
  249. node
  250. });
  251. }
  252. }
  253. }
  254. );
  255. },
  256. /**
  257. * Checks for a call of `super()`.
  258. * @param {ASTNode} node A CallExpression node to check.
  259. * @returns {void}
  260. */
  261. "CallExpression:exit"(node) {
  262. if (!(funcInfo && funcInfo.isConstructor)) {
  263. return;
  264. }
  265. // Skips except `super()`.
  266. if (node.callee.type !== "Super") {
  267. return;
  268. }
  269. // Reports if needed.
  270. if (funcInfo.hasExtends) {
  271. const segments = funcInfo.codePath.currentSegments;
  272. let duplicate = false;
  273. let info = null;
  274. for (let i = 0; i < segments.length; ++i) {
  275. const segment = segments[i];
  276. if (segment.reachable) {
  277. info = segInfoMap[segment.id];
  278. duplicate = duplicate || info.calledInSomePaths;
  279. info.calledInSomePaths = info.calledInEveryPaths = true;
  280. }
  281. }
  282. if (info) {
  283. if (duplicate) {
  284. context.report({
  285. messageId: "duplicate",
  286. node
  287. });
  288. } else if (!funcInfo.superIsConstructor) {
  289. context.report({
  290. messageId: "badSuper",
  291. node
  292. });
  293. } else {
  294. info.validNodes.push(node);
  295. }
  296. }
  297. } else if (funcInfo.codePath.currentSegments.some(isReachable)) {
  298. context.report({
  299. messageId: "unexpected",
  300. node
  301. });
  302. }
  303. },
  304. /**
  305. * Set the mark to the returned path as `super()` was called.
  306. * @param {ASTNode} node A ReturnStatement node to check.
  307. * @returns {void}
  308. */
  309. ReturnStatement(node) {
  310. if (!(funcInfo && funcInfo.isConstructor && funcInfo.hasExtends)) {
  311. return;
  312. }
  313. // Skips if no argument.
  314. if (!node.argument) {
  315. return;
  316. }
  317. // Returning argument is a substitute of 'super()'.
  318. const segments = funcInfo.codePath.currentSegments;
  319. for (let i = 0; i < segments.length; ++i) {
  320. const segment = segments[i];
  321. if (segment.reachable) {
  322. const info = segInfoMap[segment.id];
  323. info.calledInSomePaths = info.calledInEveryPaths = true;
  324. }
  325. }
  326. },
  327. /**
  328. * Resets state.
  329. * @returns {void}
  330. */
  331. "Program:exit"() {
  332. segInfoMap = Object.create(null);
  333. }
  334. };
  335. }
  336. };