ConstPlugin.js 17 KB

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