Skip to content
Permalink
Newer
Older
100644 423 lines (359 sloc) 14.7 KB
Ignoring revisions in .git-blame-ignore-revs.
September 14, 2020 10:42
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":
July 27, 2021 16:54
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
September 14, 2020 10:42
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
January 18, 2023 20:50
119
/** @type {import('../shared/types').Rule} */
September 14, 2020 10:42
120
module.exports = {
121
meta: {
122
type: "problem",
123
124
docs: {
January 18, 2023 20:50
125
description: "Require `super()` calls in constructors",
September 14, 2020 10:42
126
recommended: true,
July 13, 2023 09:09
127
url: "https://eslint.org/docs/latest/rules/constructor-super"
September 14, 2020 10:42
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
};