1 | /**
|
2 | * @fileoverview A rule to verify `super()` callings in constructor.
|
3 | * @author Toru Nagashima
|
4 | * @copyright 2015 Toru Nagashima. All rights reserved.
|
5 | */
|
6 |
|
7 | ;
|
8 |
|
9 | //------------------------------------------------------------------------------
|
10 | // Requirements
|
11 | //------------------------------------------------------------------------------
|
12 |
|
13 | var astUtils = require("../ast-utils");
|
14 |
|
15 | //------------------------------------------------------------------------------
|
16 | // Helpers
|
17 | //------------------------------------------------------------------------------
|
18 |
|
19 | /**
|
20 | * Checks whether or not a given node is a constructor.
|
21 | * @param {ASTNode} node - A node to check. This node type is one of
|
22 | * `Program`, `FunctionDeclaration`, `FunctionExpression`, and
|
23 | * `ArrowFunctionExpression`.
|
24 | * @returns {boolean} `true` if the node is a constructor.
|
25 | */
|
26 | function isConstructorFunction(node) {
|
27 | return (
|
28 | node.type === "FunctionExpression" &&
|
29 | node.parent.type === "MethodDefinition" &&
|
30 | node.parent.kind === "constructor"
|
31 | );
|
32 | }
|
33 |
|
34 | //------------------------------------------------------------------------------
|
35 | // Rule Definition
|
36 | //------------------------------------------------------------------------------
|
37 |
|
38 | module.exports = function(context) {
|
39 | // {{hasExtends: boolean, scope: Scope, codePath: CodePath}[]}
|
40 | // Information for each constructor.
|
41 | // - upper: Information of the upper constructor.
|
42 | // - hasExtends: A flag which shows whether own class has a valid `extends`
|
43 | // part.
|
44 | // - scope: The scope of own class.
|
45 | // - codePath: The code path object of the constructor.
|
46 | var funcInfo = null;
|
47 |
|
48 | // {Map<string, {calledInSomePaths: boolean, calledInEveryPaths: boolean}>}
|
49 | // Information for each code path segment.
|
50 | // - calledInSomePaths: A flag of be called `super()` in some code paths.
|
51 | // - calledInEveryPaths: A flag of be called `super()` in all code paths.
|
52 | var segInfoMap = Object.create(null);
|
53 |
|
54 | /**
|
55 | * Gets the flag which shows `super()` is called in some paths.
|
56 | * @param {CodePathSegment} segment - A code path segment to get.
|
57 | * @returns {boolean} The flag which shows `super()` is called in some paths
|
58 | */
|
59 | function isCalledInSomePath(segment) {
|
60 | return segInfoMap[segment.id].calledInSomePaths;
|
61 | }
|
62 |
|
63 | /**
|
64 | * Gets the flag which shows `super()` is called in all paths.
|
65 | * @param {CodePathSegment} segment - A code path segment to get.
|
66 | * @returns {boolean} The flag which shows `super()` is called in all paths.
|
67 | */
|
68 | function isCalledInEveryPath(segment) {
|
69 | return segInfoMap[segment.id].calledInEveryPaths;
|
70 | }
|
71 |
|
72 | return {
|
73 | /**
|
74 | * Stacks a constructor information.
|
75 | * @param {CodePath} codePath - A code path which was started.
|
76 | * @param {ASTNode} node - The current node.
|
77 | * @returns {void}
|
78 | */
|
79 | "onCodePathStart": function(codePath, node) {
|
80 | if (!isConstructorFunction(node)) {
|
81 | return;
|
82 | }
|
83 |
|
84 | // Class > ClassBody > MethodDefinition > FunctionExpression
|
85 | var classNode = node.parent.parent.parent;
|
86 | funcInfo = {
|
87 | upper: funcInfo,
|
88 | hasExtends: Boolean(
|
89 | classNode.superClass &&
|
90 | !astUtils.isNullOrUndefined(classNode.superClass)
|
91 | ),
|
92 | scope: context.getScope(),
|
93 | codePath: codePath
|
94 | };
|
95 | },
|
96 |
|
97 | /**
|
98 | * Pops a constructor information.
|
99 | * And reports if `super()` lacked.
|
100 | * @param {CodePath} codePath - A code path which was ended.
|
101 | * @param {ASTNode} node - The current node.
|
102 | * @returns {void}
|
103 | */
|
104 | "onCodePathEnd": function(codePath, node) {
|
105 | if (!isConstructorFunction(node)) {
|
106 | return;
|
107 | }
|
108 |
|
109 | // Skip if own class which has a valid `extends` part.
|
110 | var hasExtends = funcInfo.hasExtends;
|
111 | funcInfo = funcInfo.upper;
|
112 | if (!hasExtends) {
|
113 | return;
|
114 | }
|
115 |
|
116 | // Reports if `super()` lacked.
|
117 | var segments = codePath.returnedSegments;
|
118 | var calledInEveryPaths = segments.every(isCalledInEveryPath);
|
119 | var calledInSomePaths = segments.some(isCalledInSomePath);
|
120 | if (!calledInEveryPaths) {
|
121 | context.report({
|
122 | message: calledInSomePaths ?
|
123 | "Lacked a call of 'super()' in some code paths." :
|
124 | "Expected to call 'super()'.",
|
125 | node: node.parent
|
126 | });
|
127 | }
|
128 | },
|
129 |
|
130 | /**
|
131 | * Initialize information of a given code path segment.
|
132 | * @param {CodePathSegment} segment - A code path segment to initialize.
|
133 | * @returns {void}
|
134 | */
|
135 | "onCodePathSegmentStart": function(segment) {
|
136 | // Skip if this is not in a constructor of a class which has a valid
|
137 | // `extends` part.
|
138 | if (!(
|
139 | funcInfo &&
|
140 | funcInfo.hasExtends &&
|
141 | funcInfo.scope === context.getScope().variableScope
|
142 | )) {
|
143 | return;
|
144 | }
|
145 |
|
146 | // Initialize info.
|
147 | var info = segInfoMap[segment.id] = {
|
148 | calledInSomePaths: false,
|
149 | calledInEveryPaths: false
|
150 | };
|
151 |
|
152 | // When there are previous segments, aggregates these.
|
153 | var prevSegments = segment.prevSegments;
|
154 | if (prevSegments.length > 0) {
|
155 | info.calledInSomePaths = prevSegments.some(isCalledInSomePath);
|
156 | info.calledInEveryPaths = prevSegments.every(isCalledInEveryPath);
|
157 | }
|
158 | },
|
159 |
|
160 | /**
|
161 | * Checks for a call of `super()`.
|
162 | * @param {ASTNode} node - A CallExpression node to check.
|
163 | * @returns {void}
|
164 | */
|
165 | "CallExpression:exit": function(node) {
|
166 | // Skip if the node is not `super()`.
|
167 | if (node.callee.type !== "Super") {
|
168 | return;
|
169 | }
|
170 |
|
171 | // Skip if this is not in a constructor.
|
172 | if (!(funcInfo && funcInfo.scope === context.getScope().variableScope)) {
|
173 | return;
|
174 | }
|
175 |
|
176 | // Reports if needed.
|
177 | if (funcInfo.hasExtends) {
|
178 | // This class has a valid `extends` part.
|
179 | // Checks duplicate `super()`;
|
180 | var segments = funcInfo.codePath.currentSegments;
|
181 | var duplicate = false;
|
182 | for (var i = 0; i < segments.length; ++i) {
|
183 | var info = segInfoMap[segments[i].id];
|
184 |
|
185 | duplicate = duplicate || info.calledInSomePaths;
|
186 | info.calledInSomePaths = info.calledInEveryPaths = true;
|
187 | }
|
188 |
|
189 | if (duplicate) {
|
190 | context.report({
|
191 | message: "Unexpected duplicate 'super()'.",
|
192 | node: node
|
193 | });
|
194 | }
|
195 | } else {
|
196 | // This class does not have a valid `extends` part.
|
197 | // Disallow `super()`.
|
198 | context.report({
|
199 | message: "Unexpected 'super()'.",
|
200 | node: node
|
201 | });
|
202 | }
|
203 | },
|
204 |
|
205 | /**
|
206 | * Resets state.
|
207 | * @returns {void}
|
208 | */
|
209 | "Program:exit": function() {
|
210 | segInfoMap = Object.create(null);
|
211 | }
|
212 | };
|
213 | };
|
214 |
|
215 | module.exports.schema = [];
|