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