ConstPlugin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  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. // Before removing the dead branch, the hoisted declarations
  174. // must be collected.
  175. //
  176. // Given the following code:
  177. //
  178. // if (true) f() else g()
  179. // if (false) {
  180. // function f() {}
  181. // const g = function g() {}
  182. // if (someTest) {
  183. // let a = 1
  184. // var x, {y, z} = obj
  185. // }
  186. // } else {
  187. // …
  188. // }
  189. //
  190. // the generated code is:
  191. //
  192. // if (true) f() else {}
  193. // if (false) {
  194. // var f, x, y, z; (in loose mode)
  195. // var x, y, z; (in strict mode)
  196. // } else {
  197. // …
  198. // }
  199. //
  200. // NOTE: When code runs in strict mode, `var` declarations
  201. // are hoisted but `function` declarations don't.
  202. //
  203. const declarations = parser.scope.isStrict
  204. ? getHoistedDeclarations(branchToRemove, false)
  205. : getHoistedDeclarations(branchToRemove, true);
  206. const replacement =
  207. declarations.length > 0
  208. ? `{ var ${declarations.join(", ")}; }`
  209. : "{}";
  210. const dep = new ConstDependency(
  211. replacement,
  212. /** @type {Range} */ (branchToRemove.range)
  213. );
  214. dep.loc = /** @type {SourceLocation} */ (branchToRemove.loc);
  215. parser.state.module.addPresentationalDependency(dep);
  216. }
  217. return bool;
  218. }
  219. });
  220. parser.hooks.expressionConditionalOperator.tap(
  221. PLUGIN_NAME,
  222. expression => {
  223. if (parser.scope.isAsmJs) return;
  224. const param = parser.evaluateExpression(expression.test);
  225. const bool = param.asBool();
  226. if (typeof bool === "boolean") {
  227. if (!param.couldHaveSideEffects()) {
  228. const dep = new ConstDependency(
  229. ` ${bool}`,
  230. /** @type {Range} */ (param.range)
  231. );
  232. dep.loc = /** @type {SourceLocation} */ (expression.loc);
  233. parser.state.module.addPresentationalDependency(dep);
  234. } else {
  235. parser.walkExpression(expression.test);
  236. }
  237. // Expressions do not hoist.
  238. // It is safe to remove the dead branch.
  239. //
  240. // Given the following code:
  241. //
  242. // false ? someExpression() : otherExpression();
  243. //
  244. // the generated code is:
  245. //
  246. // false ? 0 : otherExpression();
  247. //
  248. const branchToRemove = bool
  249. ? expression.alternate
  250. : expression.consequent;
  251. const dep = new ConstDependency(
  252. "0",
  253. /** @type {Range} */ (branchToRemove.range)
  254. );
  255. dep.loc = /** @type {SourceLocation} */ (branchToRemove.loc);
  256. parser.state.module.addPresentationalDependency(dep);
  257. return bool;
  258. }
  259. }
  260. );
  261. parser.hooks.expressionLogicalOperator.tap(
  262. PLUGIN_NAME,
  263. expression => {
  264. if (parser.scope.isAsmJs) return;
  265. if (
  266. expression.operator === "&&" ||
  267. expression.operator === "||"
  268. ) {
  269. const param = parser.evaluateExpression(expression.left);
  270. const bool = param.asBool();
  271. if (typeof bool === "boolean") {
  272. // Expressions do not hoist.
  273. // It is safe to remove the dead branch.
  274. //
  275. // ------------------------------------------
  276. //
  277. // Given the following code:
  278. //
  279. // falsyExpression() && someExpression();
  280. //
  281. // the generated code is:
  282. //
  283. // falsyExpression() && false;
  284. //
  285. // ------------------------------------------
  286. //
  287. // Given the following code:
  288. //
  289. // truthyExpression() && someExpression();
  290. //
  291. // the generated code is:
  292. //
  293. // true && someExpression();
  294. //
  295. // ------------------------------------------
  296. //
  297. // Given the following code:
  298. //
  299. // truthyExpression() || someExpression();
  300. //
  301. // the generated code is:
  302. //
  303. // truthyExpression() || false;
  304. //
  305. // ------------------------------------------
  306. //
  307. // Given the following code:
  308. //
  309. // falsyExpression() || someExpression();
  310. //
  311. // the generated code is:
  312. //
  313. // false && someExpression();
  314. //
  315. const keepRight =
  316. (expression.operator === "&&" && bool) ||
  317. (expression.operator === "||" && !bool);
  318. if (
  319. !param.couldHaveSideEffects() &&
  320. (param.isBoolean() || keepRight)
  321. ) {
  322. // for case like
  323. //
  324. // return'development'===process.env.NODE_ENV&&'foo'
  325. //
  326. // we need a space before the bool to prevent result like
  327. //
  328. // returnfalse&&'foo'
  329. //
  330. const dep = new ConstDependency(
  331. ` ${bool}`,
  332. /** @type {Range} */ (param.range)
  333. );
  334. dep.loc = /** @type {SourceLocation} */ (expression.loc);
  335. parser.state.module.addPresentationalDependency(dep);
  336. } else {
  337. parser.walkExpression(expression.left);
  338. }
  339. if (!keepRight) {
  340. const dep = new ConstDependency(
  341. "0",
  342. /** @type {Range} */ (expression.right.range)
  343. );
  344. dep.loc = /** @type {SourceLocation} */ (expression.loc);
  345. parser.state.module.addPresentationalDependency(dep);
  346. }
  347. return keepRight;
  348. }
  349. } else if (expression.operator === "??") {
  350. const param = parser.evaluateExpression(expression.left);
  351. const keepRight = param.asNullish();
  352. if (typeof keepRight === "boolean") {
  353. // ------------------------------------------
  354. //
  355. // Given the following code:
  356. //
  357. // nonNullish ?? someExpression();
  358. //
  359. // the generated code is:
  360. //
  361. // nonNullish ?? 0;
  362. //
  363. // ------------------------------------------
  364. //
  365. // Given the following code:
  366. //
  367. // nullish ?? someExpression();
  368. //
  369. // the generated code is:
  370. //
  371. // null ?? someExpression();
  372. //
  373. if (!param.couldHaveSideEffects() && keepRight) {
  374. // cspell:word returnnull
  375. // for case like
  376. //
  377. // return('development'===process.env.NODE_ENV&&null)??'foo'
  378. //
  379. // we need a space before the bool to prevent result like
  380. //
  381. // returnnull??'foo'
  382. //
  383. const dep = new ConstDependency(
  384. " null",
  385. /** @type {Range} */ (param.range)
  386. );
  387. dep.loc = /** @type {SourceLocation} */ (expression.loc);
  388. parser.state.module.addPresentationalDependency(dep);
  389. } else {
  390. const dep = new ConstDependency(
  391. "0",
  392. /** @type {Range} */ (expression.right.range)
  393. );
  394. dep.loc = /** @type {SourceLocation} */ (expression.loc);
  395. parser.state.module.addPresentationalDependency(dep);
  396. parser.walkExpression(expression.left);
  397. }
  398. return keepRight;
  399. }
  400. }
  401. }
  402. );
  403. parser.hooks.optionalChaining.tap(PLUGIN_NAME, expr => {
  404. /** @type {Expression[]} */
  405. const optionalExpressionsStack = [];
  406. /** @type {Expression | Super} */
  407. let next = expr.expression;
  408. while (
  409. next.type === "MemberExpression" ||
  410. next.type === "CallExpression"
  411. ) {
  412. if (next.type === "MemberExpression") {
  413. if (next.optional) {
  414. // SuperNode can not be optional
  415. optionalExpressionsStack.push(
  416. /** @type {Expression} */ (next.object)
  417. );
  418. }
  419. next = next.object;
  420. } else {
  421. if (next.optional) {
  422. // SuperNode can not be optional
  423. optionalExpressionsStack.push(
  424. /** @type {Expression} */ (next.callee)
  425. );
  426. }
  427. next = next.callee;
  428. }
  429. }
  430. while (optionalExpressionsStack.length) {
  431. const expression = optionalExpressionsStack.pop();
  432. const evaluated = parser.evaluateExpression(
  433. /** @type {Expression} */ (expression)
  434. );
  435. if (evaluated.asNullish()) {
  436. // ------------------------------------------
  437. //
  438. // Given the following code:
  439. //
  440. // nullishMemberChain?.a.b();
  441. //
  442. // the generated code is:
  443. //
  444. // undefined;
  445. //
  446. // ------------------------------------------
  447. //
  448. const dep = new ConstDependency(
  449. " undefined",
  450. /** @type {Range} */ (expr.range)
  451. );
  452. dep.loc = /** @type {SourceLocation} */ (expr.loc);
  453. parser.state.module.addPresentationalDependency(dep);
  454. return true;
  455. }
  456. }
  457. });
  458. parser.hooks.evaluateIdentifier
  459. .for("__resourceQuery")
  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).query
  465. )(expr);
  466. });
  467. parser.hooks.expression
  468. .for("__resourceQuery")
  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).query
  475. ),
  476. /** @type {Range} */ (expr.range),
  477. "__resourceQuery"
  478. );
  479. dep.loc = /** @type {SourceLocation} */ (expr.loc);
  480. parser.state.module.addPresentationalDependency(dep);
  481. return true;
  482. });
  483. parser.hooks.evaluateIdentifier
  484. .for("__resourceFragment")
  485. .tap(PLUGIN_NAME, expr => {
  486. if (parser.scope.isAsmJs) return;
  487. if (!parser.state.module) return;
  488. return evaluateToString(
  489. cachedParseResource(parser.state.module.resource).fragment
  490. )(expr);
  491. });
  492. parser.hooks.expression
  493. .for("__resourceFragment")
  494. .tap(PLUGIN_NAME, expr => {
  495. if (parser.scope.isAsmJs) return;
  496. if (!parser.state.module) return;
  497. const dep = new CachedConstDependency(
  498. JSON.stringify(
  499. cachedParseResource(parser.state.module.resource).fragment
  500. ),
  501. /** @type {Range} */ (expr.range),
  502. "__resourceFragment"
  503. );
  504. dep.loc = /** @type {SourceLocation} */ (expr.loc);
  505. parser.state.module.addPresentationalDependency(dep);
  506. return true;
  507. });
  508. };
  509. normalModuleFactory.hooks.parser
  510. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  511. .tap(PLUGIN_NAME, handler);
  512. normalModuleFactory.hooks.parser
  513. .for(JAVASCRIPT_MODULE_TYPE_DYNAMIC)
  514. .tap(PLUGIN_NAME, handler);
  515. normalModuleFactory.hooks.parser
  516. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  517. .tap(PLUGIN_NAME, handler);
  518. }
  519. );
  520. }
  521. }
  522. module.exports = ConstPlugin;