ConstPlugin.js 17 KB

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