ConstPlugin.js 16 KB

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