ConstPlugin.js 17 KB

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