UNPKG

14.8 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const CachedConstDependency = require("./dependencies/CachedConstDependency");
9const ConstDependency = require("./dependencies/ConstDependency");
10const { evaluateToString } = require("./javascript/JavascriptParserHelpers");
11const { parseResource } = require("./util/identifier");
12
13/** @typedef {import("estree").Expression} ExpressionNode */
14/** @typedef {import("estree").Super} SuperNode */
15/** @typedef {import("./Compiler")} Compiler */
16
17const collectDeclaration = (declarations, pattern) => {
18 const stack = [pattern];
19 while (stack.length > 0) {
20 const node = stack.pop();
21 switch (node.type) {
22 case "Identifier":
23 declarations.add(node.name);
24 break;
25 case "ArrayPattern":
26 for (const element of node.elements) {
27 if (element) {
28 stack.push(element);
29 }
30 }
31 break;
32 case "AssignmentPattern":
33 stack.push(node.left);
34 break;
35 case "ObjectPattern":
36 for (const property of node.properties) {
37 stack.push(property.value);
38 }
39 break;
40 case "RestElement":
41 stack.push(node.argument);
42 break;
43 }
44 }
45};
46
47const getHoistedDeclarations = (branch, includeFunctionDeclarations) => {
48 const declarations = new Set();
49 const stack = [branch];
50 while (stack.length > 0) {
51 const node = stack.pop();
52 // Some node could be `null` or `undefined`.
53 if (!node) continue;
54 switch (node.type) {
55 // Walk through control statements to look for hoisted declarations.
56 // Some branches are skipped since they do not allow declarations.
57 case "BlockStatement":
58 for (const stmt of node.body) {
59 stack.push(stmt);
60 }
61 break;
62 case "IfStatement":
63 stack.push(node.consequent);
64 stack.push(node.alternate);
65 break;
66 case "ForStatement":
67 stack.push(node.init);
68 stack.push(node.body);
69 break;
70 case "ForInStatement":
71 case "ForOfStatement":
72 stack.push(node.left);
73 stack.push(node.body);
74 break;
75 case "DoWhileStatement":
76 case "WhileStatement":
77 case "LabeledStatement":
78 stack.push(node.body);
79 break;
80 case "SwitchStatement":
81 for (const cs of node.cases) {
82 for (const consequent of cs.consequent) {
83 stack.push(consequent);
84 }
85 }
86 break;
87 case "TryStatement":
88 stack.push(node.block);
89 if (node.handler) {
90 stack.push(node.handler.body);
91 }
92 stack.push(node.finalizer);
93 break;
94 case "FunctionDeclaration":
95 if (includeFunctionDeclarations) {
96 collectDeclaration(declarations, node.id);
97 }
98 break;
99 case "VariableDeclaration":
100 if (node.kind === "var") {
101 for (const decl of node.declarations) {
102 collectDeclaration(declarations, decl.id);
103 }
104 }
105 break;
106 }
107 }
108 return Array.from(declarations);
109};
110
111class ConstPlugin {
112 /**
113 * Apply the plugin
114 * @param {Compiler} compiler the compiler instance
115 * @returns {void}
116 */
117 apply(compiler) {
118 const cachedParseResource = parseResource.bindCache(compiler.root);
119 compiler.hooks.compilation.tap(
120 "ConstPlugin",
121 (compilation, { normalModuleFactory }) => {
122 compilation.dependencyTemplates.set(
123 ConstDependency,
124 new ConstDependency.Template()
125 );
126
127 compilation.dependencyTemplates.set(
128 CachedConstDependency,
129 new CachedConstDependency.Template()
130 );
131
132 const handler = parser => {
133 parser.hooks.statementIf.tap("ConstPlugin", statement => {
134 if (parser.scope.isAsmJs) return;
135 const param = parser.evaluateExpression(statement.test);
136 const bool = param.asBool();
137 if (typeof bool === "boolean") {
138 if (!param.couldHaveSideEffects()) {
139 const dep = new ConstDependency(`${bool}`, param.range);
140 dep.loc = statement.loc;
141 parser.state.module.addPresentationalDependency(dep);
142 } else {
143 parser.walkExpression(statement.test);
144 }
145 const branchToRemove = bool
146 ? statement.alternate
147 : statement.consequent;
148 if (branchToRemove) {
149 // Before removing the dead branch, the hoisted declarations
150 // must be collected.
151 //
152 // Given the following code:
153 //
154 // if (true) f() else g()
155 // if (false) {
156 // function f() {}
157 // const g = function g() {}
158 // if (someTest) {
159 // let a = 1
160 // var x, {y, z} = obj
161 // }
162 // } else {
163 // …
164 // }
165 //
166 // the generated code is:
167 //
168 // if (true) f() else {}
169 // if (false) {
170 // var f, x, y, z; (in loose mode)
171 // var x, y, z; (in strict mode)
172 // } else {
173 // …
174 // }
175 //
176 // NOTE: When code runs in strict mode, `var` declarations
177 // are hoisted but `function` declarations don't.
178 //
179 let declarations;
180 if (parser.scope.isStrict) {
181 // If the code runs in strict mode, variable declarations
182 // using `var` must be hoisted.
183 declarations = getHoistedDeclarations(branchToRemove, false);
184 } else {
185 // Otherwise, collect all hoisted declaration.
186 declarations = getHoistedDeclarations(branchToRemove, true);
187 }
188 let replacement;
189 if (declarations.length > 0) {
190 replacement = `{ var ${declarations.join(", ")}; }`;
191 } else {
192 replacement = "{}";
193 }
194 const dep = new ConstDependency(
195 replacement,
196 branchToRemove.range
197 );
198 dep.loc = branchToRemove.loc;
199 parser.state.module.addPresentationalDependency(dep);
200 }
201 return bool;
202 }
203 });
204 parser.hooks.expressionConditionalOperator.tap(
205 "ConstPlugin",
206 expression => {
207 if (parser.scope.isAsmJs) return;
208 const param = parser.evaluateExpression(expression.test);
209 const bool = param.asBool();
210 if (typeof bool === "boolean") {
211 if (!param.couldHaveSideEffects()) {
212 const dep = new ConstDependency(` ${bool}`, param.range);
213 dep.loc = expression.loc;
214 parser.state.module.addPresentationalDependency(dep);
215 } else {
216 parser.walkExpression(expression.test);
217 }
218 // Expressions do not hoist.
219 // It is safe to remove the dead branch.
220 //
221 // Given the following code:
222 //
223 // false ? someExpression() : otherExpression();
224 //
225 // the generated code is:
226 //
227 // false ? 0 : otherExpression();
228 //
229 const branchToRemove = bool
230 ? expression.alternate
231 : expression.consequent;
232 const dep = new ConstDependency("0", branchToRemove.range);
233 dep.loc = branchToRemove.loc;
234 parser.state.module.addPresentationalDependency(dep);
235 return bool;
236 }
237 }
238 );
239 parser.hooks.expressionLogicalOperator.tap(
240 "ConstPlugin",
241 expression => {
242 if (parser.scope.isAsmJs) return;
243 if (
244 expression.operator === "&&" ||
245 expression.operator === "||"
246 ) {
247 const param = parser.evaluateExpression(expression.left);
248 const bool = param.asBool();
249 if (typeof bool === "boolean") {
250 // Expressions do not hoist.
251 // It is safe to remove the dead branch.
252 //
253 // ------------------------------------------
254 //
255 // Given the following code:
256 //
257 // falsyExpression() && someExpression();
258 //
259 // the generated code is:
260 //
261 // falsyExpression() && false;
262 //
263 // ------------------------------------------
264 //
265 // Given the following code:
266 //
267 // truthyExpression() && someExpression();
268 //
269 // the generated code is:
270 //
271 // true && someExpression();
272 //
273 // ------------------------------------------
274 //
275 // Given the following code:
276 //
277 // truthyExpression() || someExpression();
278 //
279 // the generated code is:
280 //
281 // truthyExpression() || false;
282 //
283 // ------------------------------------------
284 //
285 // Given the following code:
286 //
287 // falsyExpression() || someExpression();
288 //
289 // the generated code is:
290 //
291 // false && someExpression();
292 //
293 const keepRight =
294 (expression.operator === "&&" && bool) ||
295 (expression.operator === "||" && !bool);
296
297 if (
298 !param.couldHaveSideEffects() &&
299 (param.isBoolean() || keepRight)
300 ) {
301 // for case like
302 //
303 // return'development'===process.env.NODE_ENV&&'foo'
304 //
305 // we need a space before the bool to prevent result like
306 //
307 // returnfalse&&'foo'
308 //
309 const dep = new ConstDependency(` ${bool}`, param.range);
310 dep.loc = expression.loc;
311 parser.state.module.addPresentationalDependency(dep);
312 } else {
313 parser.walkExpression(expression.left);
314 }
315 if (!keepRight) {
316 const dep = new ConstDependency(
317 "0",
318 expression.right.range
319 );
320 dep.loc = expression.loc;
321 parser.state.module.addPresentationalDependency(dep);
322 }
323 return keepRight;
324 }
325 } else if (expression.operator === "??") {
326 const param = parser.evaluateExpression(expression.left);
327 const keepRight = param && param.asNullish();
328 if (typeof keepRight === "boolean") {
329 // ------------------------------------------
330 //
331 // Given the following code:
332 //
333 // nonNullish ?? someExpression();
334 //
335 // the generated code is:
336 //
337 // nonNullish ?? 0;
338 //
339 // ------------------------------------------
340 //
341 // Given the following code:
342 //
343 // nullish ?? someExpression();
344 //
345 // the generated code is:
346 //
347 // null ?? someExpression();
348 //
349 if (!param.couldHaveSideEffects() && keepRight) {
350 // cspell:word returnnull
351 // for case like
352 //
353 // return('development'===process.env.NODE_ENV&&null)??'foo'
354 //
355 // we need a space before the bool to prevent result like
356 //
357 // returnnull??'foo'
358 //
359 const dep = new ConstDependency(" null", param.range);
360 dep.loc = expression.loc;
361 parser.state.module.addPresentationalDependency(dep);
362 } else {
363 const dep = new ConstDependency(
364 "0",
365 expression.right.range
366 );
367 dep.loc = expression.loc;
368 parser.state.module.addPresentationalDependency(dep);
369 parser.walkExpression(expression.left);
370 }
371
372 return keepRight;
373 }
374 }
375 }
376 );
377 parser.hooks.optionalChaining.tap("ConstPlugin", expr => {
378 /** @type {ExpressionNode[]} */
379 const optionalExpressionsStack = [];
380 /** @type {ExpressionNode|SuperNode} */
381 let next = expr.expression;
382
383 while (
384 next.type === "MemberExpression" ||
385 next.type === "CallExpression"
386 ) {
387 if (next.type === "MemberExpression") {
388 if (next.optional) {
389 // SuperNode can not be optional
390 optionalExpressionsStack.push(
391 /** @type {ExpressionNode} */ (next.object)
392 );
393 }
394 next = next.object;
395 } else {
396 if (next.optional) {
397 // SuperNode can not be optional
398 optionalExpressionsStack.push(
399 /** @type {ExpressionNode} */ (next.callee)
400 );
401 }
402 next = next.callee;
403 }
404 }
405
406 while (optionalExpressionsStack.length) {
407 const expression = optionalExpressionsStack.pop();
408 const evaluated = parser.evaluateExpression(expression);
409
410 if (evaluated && evaluated.asNullish()) {
411 // ------------------------------------------
412 //
413 // Given the following code:
414 //
415 // nullishMemberChain?.a.b();
416 //
417 // the generated code is:
418 //
419 // undefined;
420 //
421 // ------------------------------------------
422 //
423 const dep = new ConstDependency(" undefined", expr.range);
424 dep.loc = expr.loc;
425 parser.state.module.addPresentationalDependency(dep);
426 return true;
427 }
428 }
429 });
430 parser.hooks.evaluateIdentifier
431 .for("__resourceQuery")
432 .tap("ConstPlugin", expr => {
433 if (parser.scope.isAsmJs) return;
434 if (!parser.state.module) return;
435 return evaluateToString(
436 cachedParseResource(parser.state.module.resource).query
437 )(expr);
438 });
439 parser.hooks.expression
440 .for("__resourceQuery")
441 .tap("ConstPlugin", expr => {
442 if (parser.scope.isAsmJs) return;
443 if (!parser.state.module) return;
444 const dep = new CachedConstDependency(
445 JSON.stringify(
446 cachedParseResource(parser.state.module.resource).query
447 ),
448 expr.range,
449 "__resourceQuery"
450 );
451 dep.loc = expr.loc;
452 parser.state.module.addPresentationalDependency(dep);
453 return true;
454 });
455
456 parser.hooks.evaluateIdentifier
457 .for("__resourceFragment")
458 .tap("ConstPlugin", expr => {
459 if (parser.scope.isAsmJs) return;
460 if (!parser.state.module) return;
461 return evaluateToString(
462 cachedParseResource(parser.state.module.resource).fragment
463 )(expr);
464 });
465 parser.hooks.expression
466 .for("__resourceFragment")
467 .tap("ConstPlugin", expr => {
468 if (parser.scope.isAsmJs) return;
469 if (!parser.state.module) return;
470 const dep = new CachedConstDependency(
471 JSON.stringify(
472 cachedParseResource(parser.state.module.resource).fragment
473 ),
474 expr.range,
475 "__resourceFragment"
476 );
477 dep.loc = expr.loc;
478 parser.state.module.addPresentationalDependency(dep);
479 return true;
480 });
481 };
482
483 normalModuleFactory.hooks.parser
484 .for("javascript/auto")
485 .tap("ConstPlugin", handler);
486 normalModuleFactory.hooks.parser
487 .for("javascript/dynamic")
488 .tap("ConstPlugin", handler);
489 normalModuleFactory.hooks.parser
490 .for("javascript/esm")
491 .tap("ConstPlugin", handler);
492 }
493 );
494 }
495}
496
497module.exports = ConstPlugin;