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.
497 lines
14 KiB
497 lines
14 KiB
/* |
|
MIT License http://www.opensource.org/licenses/mit-license.php |
|
Author Tobias Koppers @sokra |
|
*/ |
|
|
|
"use strict"; |
|
|
|
const CachedConstDependency = require("./dependencies/CachedConstDependency"); |
|
const ConstDependency = require("./dependencies/ConstDependency"); |
|
const { evaluateToString } = require("./javascript/JavascriptParserHelpers"); |
|
const { parseResource } = require("./util/identifier"); |
|
|
|
/** @typedef {import("estree").Expression} ExpressionNode */ |
|
/** @typedef {import("estree").Super} SuperNode */ |
|
/** @typedef {import("./Compiler")} Compiler */ |
|
|
|
const collectDeclaration = (declarations, pattern) => { |
|
const stack = [pattern]; |
|
while (stack.length > 0) { |
|
const node = stack.pop(); |
|
switch (node.type) { |
|
case "Identifier": |
|
declarations.add(node.name); |
|
break; |
|
case "ArrayPattern": |
|
for (const element of node.elements) { |
|
if (element) { |
|
stack.push(element); |
|
} |
|
} |
|
break; |
|
case "AssignmentPattern": |
|
stack.push(node.left); |
|
break; |
|
case "ObjectPattern": |
|
for (const property of node.properties) { |
|
stack.push(property.value); |
|
} |
|
break; |
|
case "RestElement": |
|
stack.push(node.argument); |
|
break; |
|
} |
|
} |
|
}; |
|
|
|
const getHoistedDeclarations = (branch, includeFunctionDeclarations) => { |
|
const declarations = new Set(); |
|
const stack = [branch]; |
|
while (stack.length > 0) { |
|
const node = stack.pop(); |
|
// Some node could be `null` or `undefined`. |
|
if (!node) continue; |
|
switch (node.type) { |
|
// Walk through control statements to look for hoisted declarations. |
|
// Some branches are skipped since they do not allow declarations. |
|
case "BlockStatement": |
|
for (const stmt of node.body) { |
|
stack.push(stmt); |
|
} |
|
break; |
|
case "IfStatement": |
|
stack.push(node.consequent); |
|
stack.push(node.alternate); |
|
break; |
|
case "ForStatement": |
|
stack.push(node.init); |
|
stack.push(node.body); |
|
break; |
|
case "ForInStatement": |
|
case "ForOfStatement": |
|
stack.push(node.left); |
|
stack.push(node.body); |
|
break; |
|
case "DoWhileStatement": |
|
case "WhileStatement": |
|
case "LabeledStatement": |
|
stack.push(node.body); |
|
break; |
|
case "SwitchStatement": |
|
for (const cs of node.cases) { |
|
for (const consequent of cs.consequent) { |
|
stack.push(consequent); |
|
} |
|
} |
|
break; |
|
case "TryStatement": |
|
stack.push(node.block); |
|
if (node.handler) { |
|
stack.push(node.handler.body); |
|
} |
|
stack.push(node.finalizer); |
|
break; |
|
case "FunctionDeclaration": |
|
if (includeFunctionDeclarations) { |
|
collectDeclaration(declarations, node.id); |
|
} |
|
break; |
|
case "VariableDeclaration": |
|
if (node.kind === "var") { |
|
for (const decl of node.declarations) { |
|
collectDeclaration(declarations, decl.id); |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
return Array.from(declarations); |
|
}; |
|
|
|
class ConstPlugin { |
|
/** |
|
* Apply the plugin |
|
* @param {Compiler} compiler the compiler instance |
|
* @returns {void} |
|
*/ |
|
apply(compiler) { |
|
const cachedParseResource = parseResource.bindCache(compiler.root); |
|
compiler.hooks.compilation.tap( |
|
"ConstPlugin", |
|
(compilation, { normalModuleFactory }) => { |
|
compilation.dependencyTemplates.set( |
|
ConstDependency, |
|
new ConstDependency.Template() |
|
); |
|
|
|
compilation.dependencyTemplates.set( |
|
CachedConstDependency, |
|
new CachedConstDependency.Template() |
|
); |
|
|
|
const handler = parser => { |
|
parser.hooks.statementIf.tap("ConstPlugin", statement => { |
|
if (parser.scope.isAsmJs) return; |
|
const param = parser.evaluateExpression(statement.test); |
|
const bool = param.asBool(); |
|
if (typeof bool === "boolean") { |
|
if (!param.couldHaveSideEffects()) { |
|
const dep = new ConstDependency(`${bool}`, param.range); |
|
dep.loc = statement.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} else { |
|
parser.walkExpression(statement.test); |
|
} |
|
const branchToRemove = bool |
|
? statement.alternate |
|
: statement.consequent; |
|
if (branchToRemove) { |
|
// Before removing the dead branch, the hoisted declarations |
|
// must be collected. |
|
// |
|
// Given the following code: |
|
// |
|
// if (true) f() else g() |
|
// if (false) { |
|
// function f() {} |
|
// const g = function g() {} |
|
// if (someTest) { |
|
// let a = 1 |
|
// var x, {y, z} = obj |
|
// } |
|
// } else { |
|
// … |
|
// } |
|
// |
|
// the generated code is: |
|
// |
|
// if (true) f() else {} |
|
// if (false) { |
|
// var f, x, y, z; (in loose mode) |
|
// var x, y, z; (in strict mode) |
|
// } else { |
|
// … |
|
// } |
|
// |
|
// NOTE: When code runs in strict mode, `var` declarations |
|
// are hoisted but `function` declarations don't. |
|
// |
|
let declarations; |
|
if (parser.scope.isStrict) { |
|
// If the code runs in strict mode, variable declarations |
|
// using `var` must be hoisted. |
|
declarations = getHoistedDeclarations(branchToRemove, false); |
|
} else { |
|
// Otherwise, collect all hoisted declaration. |
|
declarations = getHoistedDeclarations(branchToRemove, true); |
|
} |
|
let replacement; |
|
if (declarations.length > 0) { |
|
replacement = `{ var ${declarations.join(", ")}; }`; |
|
} else { |
|
replacement = "{}"; |
|
} |
|
const dep = new ConstDependency( |
|
replacement, |
|
branchToRemove.range |
|
); |
|
dep.loc = branchToRemove.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} |
|
return bool; |
|
} |
|
}); |
|
parser.hooks.expressionConditionalOperator.tap( |
|
"ConstPlugin", |
|
expression => { |
|
if (parser.scope.isAsmJs) return; |
|
const param = parser.evaluateExpression(expression.test); |
|
const bool = param.asBool(); |
|
if (typeof bool === "boolean") { |
|
if (!param.couldHaveSideEffects()) { |
|
const dep = new ConstDependency(` ${bool}`, param.range); |
|
dep.loc = expression.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} else { |
|
parser.walkExpression(expression.test); |
|
} |
|
// Expressions do not hoist. |
|
// It is safe to remove the dead branch. |
|
// |
|
// Given the following code: |
|
// |
|
// false ? someExpression() : otherExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// false ? 0 : otherExpression(); |
|
// |
|
const branchToRemove = bool |
|
? expression.alternate |
|
: expression.consequent; |
|
const dep = new ConstDependency("0", branchToRemove.range); |
|
dep.loc = branchToRemove.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
return bool; |
|
} |
|
} |
|
); |
|
parser.hooks.expressionLogicalOperator.tap( |
|
"ConstPlugin", |
|
expression => { |
|
if (parser.scope.isAsmJs) return; |
|
if ( |
|
expression.operator === "&&" || |
|
expression.operator === "||" |
|
) { |
|
const param = parser.evaluateExpression(expression.left); |
|
const bool = param.asBool(); |
|
if (typeof bool === "boolean") { |
|
// Expressions do not hoist. |
|
// It is safe to remove the dead branch. |
|
// |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// falsyExpression() && someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// falsyExpression() && false; |
|
// |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// truthyExpression() && someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// true && someExpression(); |
|
// |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// truthyExpression() || someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// truthyExpression() || false; |
|
// |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// falsyExpression() || someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// false && someExpression(); |
|
// |
|
const keepRight = |
|
(expression.operator === "&&" && bool) || |
|
(expression.operator === "||" && !bool); |
|
|
|
if ( |
|
!param.couldHaveSideEffects() && |
|
(param.isBoolean() || keepRight) |
|
) { |
|
// for case like |
|
// |
|
// return'development'===process.env.NODE_ENV&&'foo' |
|
// |
|
// we need a space before the bool to prevent result like |
|
// |
|
// returnfalse&&'foo' |
|
// |
|
const dep = new ConstDependency(` ${bool}`, param.range); |
|
dep.loc = expression.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} else { |
|
parser.walkExpression(expression.left); |
|
} |
|
if (!keepRight) { |
|
const dep = new ConstDependency( |
|
"0", |
|
expression.right.range |
|
); |
|
dep.loc = expression.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} |
|
return keepRight; |
|
} |
|
} else if (expression.operator === "??") { |
|
const param = parser.evaluateExpression(expression.left); |
|
const keepRight = param.asNullish(); |
|
if (typeof keepRight === "boolean") { |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// nonNullish ?? someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// nonNullish ?? 0; |
|
// |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// nullish ?? someExpression(); |
|
// |
|
// the generated code is: |
|
// |
|
// null ?? someExpression(); |
|
// |
|
if (!param.couldHaveSideEffects() && keepRight) { |
|
// cspell:word returnnull |
|
// for case like |
|
// |
|
// return('development'===process.env.NODE_ENV&&null)??'foo' |
|
// |
|
// we need a space before the bool to prevent result like |
|
// |
|
// returnnull??'foo' |
|
// |
|
const dep = new ConstDependency(" null", param.range); |
|
dep.loc = expression.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
} else { |
|
const dep = new ConstDependency( |
|
"0", |
|
expression.right.range |
|
); |
|
dep.loc = expression.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
parser.walkExpression(expression.left); |
|
} |
|
|
|
return keepRight; |
|
} |
|
} |
|
} |
|
); |
|
parser.hooks.optionalChaining.tap("ConstPlugin", expr => { |
|
/** @type {ExpressionNode[]} */ |
|
const optionalExpressionsStack = []; |
|
/** @type {ExpressionNode|SuperNode} */ |
|
let next = expr.expression; |
|
|
|
while ( |
|
next.type === "MemberExpression" || |
|
next.type === "CallExpression" |
|
) { |
|
if (next.type === "MemberExpression") { |
|
if (next.optional) { |
|
// SuperNode can not be optional |
|
optionalExpressionsStack.push( |
|
/** @type {ExpressionNode} */ (next.object) |
|
); |
|
} |
|
next = next.object; |
|
} else { |
|
if (next.optional) { |
|
// SuperNode can not be optional |
|
optionalExpressionsStack.push( |
|
/** @type {ExpressionNode} */ (next.callee) |
|
); |
|
} |
|
next = next.callee; |
|
} |
|
} |
|
|
|
while (optionalExpressionsStack.length) { |
|
const expression = optionalExpressionsStack.pop(); |
|
const evaluated = parser.evaluateExpression(expression); |
|
|
|
if (evaluated.asNullish()) { |
|
// ------------------------------------------ |
|
// |
|
// Given the following code: |
|
// |
|
// nullishMemberChain?.a.b(); |
|
// |
|
// the generated code is: |
|
// |
|
// undefined; |
|
// |
|
// ------------------------------------------ |
|
// |
|
const dep = new ConstDependency(" undefined", expr.range); |
|
dep.loc = expr.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
return true; |
|
} |
|
} |
|
}); |
|
parser.hooks.evaluateIdentifier |
|
.for("__resourceQuery") |
|
.tap("ConstPlugin", expr => { |
|
if (parser.scope.isAsmJs) return; |
|
if (!parser.state.module) return; |
|
return evaluateToString( |
|
cachedParseResource(parser.state.module.resource).query |
|
)(expr); |
|
}); |
|
parser.hooks.expression |
|
.for("__resourceQuery") |
|
.tap("ConstPlugin", expr => { |
|
if (parser.scope.isAsmJs) return; |
|
if (!parser.state.module) return; |
|
const dep = new CachedConstDependency( |
|
JSON.stringify( |
|
cachedParseResource(parser.state.module.resource).query |
|
), |
|
expr.range, |
|
"__resourceQuery" |
|
); |
|
dep.loc = expr.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
return true; |
|
}); |
|
|
|
parser.hooks.evaluateIdentifier |
|
.for("__resourceFragment") |
|
.tap("ConstPlugin", expr => { |
|
if (parser.scope.isAsmJs) return; |
|
if (!parser.state.module) return; |
|
return evaluateToString( |
|
cachedParseResource(parser.state.module.resource).fragment |
|
)(expr); |
|
}); |
|
parser.hooks.expression |
|
.for("__resourceFragment") |
|
.tap("ConstPlugin", expr => { |
|
if (parser.scope.isAsmJs) return; |
|
if (!parser.state.module) return; |
|
const dep = new CachedConstDependency( |
|
JSON.stringify( |
|
cachedParseResource(parser.state.module.resource).fragment |
|
), |
|
expr.range, |
|
"__resourceFragment" |
|
); |
|
dep.loc = expr.loc; |
|
parser.state.module.addPresentationalDependency(dep); |
|
return true; |
|
}); |
|
}; |
|
|
|
normalModuleFactory.hooks.parser |
|
.for("javascript/auto") |
|
.tap("ConstPlugin", handler); |
|
normalModuleFactory.hooks.parser |
|
.for("javascript/dynamic") |
|
.tap("ConstPlugin", handler); |
|
normalModuleFactory.hooks.parser |
|
.for("javascript/esm") |
|
.tap("ConstPlugin", handler); |
|
} |
|
); |
|
} |
|
} |
|
|
|
module.exports = ConstPlugin;
|
|
|