ConstPlugin.js 17 KB

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