InnerGraphPlugin.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  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_ESM
  9. } = require("../ModuleTypeConstants");
  10. const RuntimeGlobals = require("../RuntimeGlobals");
  11. const {
  12. harmonySpecifierTag
  13. } = require("../dependencies/HarmonyImportDependencyParserPlugin");
  14. const HarmonyImportSideEffectDependency = require("../dependencies/HarmonyImportSideEffectDependency");
  15. const PureExpressionDependency = require("../dependencies/PureExpressionDependency");
  16. const { getInnerGraphUtils, topLevelSymbolTag } = require("./InnerGraph");
  17. /**
  18. * @import {
  19. * CallExpression,
  20. * ClassDeclaration,
  21. * ClassExpression,
  22. * Expression,
  23. * MaybeNamedClassDeclaration,
  24. * MaybeNamedFunctionDeclaration,
  25. * Node,
  26. * VariableDeclarator
  27. * } from "estree"
  28. */
  29. /**
  30. * @import {
  31. * JavascriptParserOptions
  32. * } from "../../declarations/WebpackOptions"
  33. */
  34. /** @import Compilation from "../Compilation" */
  35. /** @import Compiler from "../Compiler" */
  36. /** @import Dependency from "../Dependency" */
  37. /** @import Module from "../Module" */
  38. /** @import JavascriptParser, { Range } from "../javascript/JavascriptParser" */
  39. /** @import { TopLevelSymbol } from "./InnerGraph" */
  40. const PLUGIN_NAME = "InnerGraphPlugin";
  41. const impureVariableDeclarationKinds = new Set(["using", "await using"]);
  42. class InnerGraphPlugin {
  43. /**
  44. * Applies the plugin by registering its hooks on the compiler.
  45. * @param {Compiler} compiler the compiler instance
  46. * @returns {void}
  47. */
  48. apply(compiler) {
  49. compiler.hooks.compilation.tap(
  50. PLUGIN_NAME,
  51. (compilation, { normalModuleFactory }) => {
  52. const logger = compilation.getLogger("webpack.InnerGraphPlugin");
  53. const innerGraph = getInnerGraphUtils(compilation);
  54. compilation.dependencyTemplates.set(
  55. PureExpressionDependency,
  56. new PureExpressionDependency.Template()
  57. );
  58. /**
  59. * Adds pure dependency added after parsing, with parents set so it survives persistent caching.
  60. * @param {Module} module module
  61. * @param {Dependency} dep pure dependency
  62. * @returns {void}
  63. */
  64. const addPureDependency = (module, dep) => {
  65. compilation.moduleGraph.setParents(dep, module, module, -1);
  66. module.addDependency(dep);
  67. };
  68. /**
  69. * Handles the hook callback for this code path.
  70. * @param {JavascriptParser} parser the parser
  71. * @param {JavascriptParserOptions} parserOptions options
  72. * @returns {void}
  73. */
  74. const handler = (parser, parserOptions) => {
  75. /**
  76. * Processes the provided sup.
  77. * @param {Expression} sup sup
  78. */
  79. const onUsageSuper = (sup) => {
  80. // the callback fires after parsing, when the parser can no
  81. // longer map offsets — take the location now
  82. const loc = parser.getLocation(sup);
  83. innerGraph.onUsage(parser.state, (usedByExports, module) => {
  84. switch (usedByExports) {
  85. case undefined:
  86. case true:
  87. return;
  88. default: {
  89. const dep = new PureExpressionDependency(
  90. /** @type {Range} */
  91. (sup.range)
  92. );
  93. dep.loc = loc;
  94. dep.usedByExports = usedByExports;
  95. addPureDependency(module, dep);
  96. break;
  97. }
  98. }
  99. });
  100. };
  101. parser.hooks.program.tap(PLUGIN_NAME, () => {
  102. innerGraph.enable(parser.state);
  103. statementWithTopLevelSymbol = new WeakMap();
  104. statementPurePart = new WeakMap();
  105. classWithTopLevelSymbol = new WeakMap();
  106. declWithTopLevelSymbol = new WeakMap();
  107. pureDeclarators = new WeakSet();
  108. pureConditionByCallExpr = new WeakMap();
  109. });
  110. // During prewalking the following datastructures are filled with
  111. // nodes that have a TopLevelSymbol assigned and
  112. // variables are tagged with the assigned TopLevelSymbol
  113. // We differ 3 types of nodes:
  114. // 1. full statements (export default, function declaration)
  115. // 2. classes (class declaration, class expression)
  116. // 3. variable declarators (const x = ...)
  117. /** @type {WeakMap<Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, TopLevelSymbol>} */
  118. let statementWithTopLevelSymbol = new WeakMap();
  119. /** @type {WeakMap<Node | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration, Node>} */
  120. let statementPurePart = new WeakMap();
  121. /** @type {WeakMap<ClassExpression | ClassDeclaration | MaybeNamedClassDeclaration, TopLevelSymbol>} */
  122. let classWithTopLevelSymbol = new WeakMap();
  123. /** @type {WeakMap<VariableDeclarator, TopLevelSymbol>} */
  124. let declWithTopLevelSymbol = new WeakMap();
  125. /** @type {WeakSet<VariableDeclarator>} */
  126. let pureDeclarators = new WeakSet();
  127. /** @type {WeakMap<CallExpression, (compilation: Compilation, module: Module) => boolean>} */
  128. let pureConditionByCallExpr = new WeakMap();
  129. parser.hooks.isPure.for("CallExpression").tap(
  130. {
  131. name: PLUGIN_NAME,
  132. stage: -10
  133. },
  134. (expression) => {
  135. const expr = /** @type {CallExpression} */ (expression);
  136. const callee = expr.callee;
  137. /** @type {Node} */
  138. let root;
  139. /** @type {string[]} */
  140. let chainMembers;
  141. if (callee.type === "Identifier") {
  142. root = callee;
  143. chainMembers = [];
  144. } else if (callee.type === "MemberExpression") {
  145. const chain = parser.extractMemberExpressionChain(callee);
  146. // optional chaining short-circuits and breaks straight purity
  147. if (chain.membersOptionals.some(Boolean)) return;
  148. root = /** @type {Node} */ (chain.object);
  149. // extractMemberExpressionChain returns members in reverse
  150. chainMembers = [...chain.members].reverse();
  151. } else {
  152. return;
  153. }
  154. if (root.type !== "Identifier") return;
  155. const harmonySettings =
  156. /** @type {{ source: string, ids: string[] } | undefined} */ (
  157. parser.getTagData(root.name, harmonySpecifierTag)
  158. );
  159. if (!harmonySettings) return;
  160. const ids = [...harmonySettings.ids, ...chainMembers];
  161. if (ids.length === 0) return;
  162. let pos = /** @type {Range} */ (callee.range)[1];
  163. for (const arg of expr.arguments) {
  164. if (arg.type === "SpreadElement") return;
  165. if (!parser.isPure(arg, pos)) return;
  166. pos = /** @type {Range} */ (arg.range)[1];
  167. }
  168. const source = harmonySettings.source;
  169. pureConditionByCallExpr.set(expr, (compilation, module) => {
  170. const moduleGraph = compilation.moduleGraph;
  171. for (const dep of module.dependencies) {
  172. if (
  173. dep instanceof HarmonyImportSideEffectDependency &&
  174. dep.request === source
  175. ) {
  176. const m = moduleGraph.getModule(dep);
  177. if (!m) return false;
  178. const exportInfo = moduleGraph
  179. .getExportsInfo(m)
  180. .getReadOnlyExportInfoRecursive(ids);
  181. if (!exportInfo) return false;
  182. const target = exportInfo.getTarget(moduleGraph);
  183. const final =
  184. target && target.export
  185. ? moduleGraph
  186. .getExportsInfo(target.module)
  187. .getReadOnlyExportInfoRecursive(target.export)
  188. : exportInfo;
  189. return final !== undefined && final.pureProvide === true;
  190. }
  191. }
  192. return false;
  193. });
  194. }
  195. );
  196. // The following hooks are used during prewalking:
  197. parser.hooks.preStatementByType
  198. .for("FunctionDeclaration")
  199. .tap(PLUGIN_NAME, (statement) => {
  200. if (!innerGraph.isEnabled(parser.state)) return;
  201. if (
  202. parser.scope.topLevelScope === true &&
  203. statement.type === "FunctionDeclaration"
  204. ) {
  205. const name = statement.id ? statement.id.name : "*default*";
  206. const symbol = /** @type {TopLevelSymbol} */ (
  207. innerGraph.tagTopLevelSymbol(
  208. parser,
  209. name,
  210. parser.isPure(
  211. statement,
  212. /** @type {Range} */ (statement.range)[0]
  213. )
  214. )
  215. );
  216. statementWithTopLevelSymbol.set(statement, symbol);
  217. return true;
  218. }
  219. });
  220. /**
  221. * @param {import("estree").Statement | import("estree").ModuleDeclaration | import("estree").MaybeNamedFunctionDeclaration | import("estree").MaybeNamedClassDeclaration} statement statement
  222. * @returns {boolean | void} true when handled
  223. */
  224. const blockPreStatementHandler = (statement) => {
  225. if (!innerGraph.isEnabled(parser.state)) return;
  226. if (parser.scope.topLevelScope === true) {
  227. if (statement.type === "ClassDeclaration") {
  228. const name = statement.id ? statement.id.name : "*default*";
  229. const pure = parser.isPure(
  230. statement,
  231. /** @type {Range} */ (statement.range)[0]
  232. );
  233. const symbol = /** @type {TopLevelSymbol} */ (
  234. innerGraph.tagTopLevelSymbol(parser, name, pure)
  235. );
  236. classWithTopLevelSymbol.set(statement, symbol);
  237. return true;
  238. }
  239. if (statement.type === "ExportDefaultDeclaration") {
  240. const name = "*default*";
  241. const decl = statement.declaration;
  242. /** @type {boolean | ((compilation: Compilation, module: Module) => boolean)} */
  243. let pure = parser.isPure(
  244. decl,
  245. /** @type {Range} */ (statement.range)[0]
  246. );
  247. if (!pure && decl.type === "CallExpression") {
  248. const deferred = pureConditionByCallExpr.get(decl);
  249. if (deferred) pure = deferred;
  250. }
  251. const symbol =
  252. /** @type {TopLevelSymbol} */
  253. (innerGraph.tagTopLevelSymbol(parser, name, pure));
  254. if (
  255. decl.type === "ClassExpression" ||
  256. decl.type === "ClassDeclaration"
  257. ) {
  258. classWithTopLevelSymbol.set(
  259. /** @type {ClassExpression | ClassDeclaration} */
  260. (decl),
  261. symbol
  262. );
  263. } else {
  264. statementWithTopLevelSymbol.set(statement, symbol);
  265. if (
  266. pure &&
  267. // body deferred to call-time, no eager read
  268. !decl.type.endsWith("FunctionExpression") &&
  269. // only FunctionDeclaration here (classes routed above), body deferred
  270. !decl.type.endsWith("Declaration") &&
  271. // literal references nothing
  272. decl.type !== "Literal"
  273. ) {
  274. statementPurePart.set(
  275. statement,
  276. /** @type {Expression} */
  277. (decl)
  278. );
  279. }
  280. }
  281. }
  282. }
  283. };
  284. parser.hooks.blockPreStatementByType
  285. .for("ClassDeclaration")
  286. .tap(PLUGIN_NAME, blockPreStatementHandler);
  287. parser.hooks.blockPreStatementByType
  288. .for("ExportDefaultDeclaration")
  289. .tap(PLUGIN_NAME, blockPreStatementHandler);
  290. parser.hooks.preDeclarator.tap(PLUGIN_NAME, (decl, statement) => {
  291. if (!innerGraph.isEnabled(parser.state)) return;
  292. if (impureVariableDeclarationKinds.has(statement.kind)) return;
  293. if (
  294. parser.scope.topLevelScope === true &&
  295. decl.init &&
  296. decl.id.type === "Identifier"
  297. ) {
  298. const name = decl.id.name;
  299. // Skip webpack runtime variables handled by CompatibilityPlugin
  300. if (
  301. name === RuntimeGlobals.require ||
  302. name === RuntimeGlobals.exports
  303. ) {
  304. return;
  305. }
  306. /** @type {boolean | ((compilation: Compilation, module: Module) => boolean)} */
  307. let pure = parser.isPure(
  308. decl.init,
  309. /** @type {Range} */ (decl.id.range)[1]
  310. );
  311. if (!pure && decl.init.type === "CallExpression") {
  312. const deferred = pureConditionByCallExpr.get(decl.init);
  313. if (deferred) pure = deferred;
  314. }
  315. if (decl.init.type === "ClassExpression") {
  316. const symbol =
  317. /** @type {TopLevelSymbol} */
  318. (innerGraph.tagTopLevelSymbol(parser, name, pure));
  319. classWithTopLevelSymbol.set(decl.init, symbol);
  320. } else {
  321. const symbol =
  322. /** @type {TopLevelSymbol} */
  323. (innerGraph.tagTopLevelSymbol(parser, name, pure));
  324. declWithTopLevelSymbol.set(decl, symbol);
  325. if (
  326. pure &&
  327. !decl.init.type.endsWith("FunctionExpression") &&
  328. decl.init.type !== "Literal"
  329. ) {
  330. pureDeclarators.add(decl);
  331. }
  332. }
  333. }
  334. });
  335. // During real walking we set the TopLevelSymbol state to the assigned
  336. // TopLevelSymbol by using the fill datastructures.
  337. // In addition to tracking TopLevelSymbols, we sometimes need to
  338. // add a PureExpressionDependency. This is needed to skip execution
  339. // of pure expressions, even when they are not dropped due to
  340. // minimizing. Otherwise symbols used there might not exist anymore
  341. // as they are removed as unused by this optimization
  342. // When we find a reference to a TopLevelSymbol, we register a
  343. // TopLevelSymbol dependency from TopLevelSymbol in state to the
  344. // referenced TopLevelSymbol. This way we get a graph of all
  345. // TopLevelSymbols.
  346. // The following hooks are called during walking:
  347. parser.hooks.statement.tap(PLUGIN_NAME, (statement) => {
  348. if (!innerGraph.isEnabled(parser.state)) return;
  349. if (parser.scope.topLevelScope === true) {
  350. innerGraph.setTopLevelSymbol(parser.state, undefined);
  351. const symbol = statementWithTopLevelSymbol.get(statement);
  352. if (symbol) {
  353. innerGraph.setTopLevelSymbol(parser.state, symbol);
  354. const purePart = statementPurePart.get(statement);
  355. if (purePart) {
  356. // the callback fires after parsing, when the parser can no
  357. // longer map offsets — take the location now
  358. const loc = parser.getLocation(statement);
  359. innerGraph.onUsage(parser.state, (usedByExports, module) => {
  360. switch (usedByExports) {
  361. case undefined:
  362. case true:
  363. return;
  364. default: {
  365. const dep = new PureExpressionDependency(
  366. /** @type {Range} */ (purePart.range)
  367. );
  368. dep.loc = loc;
  369. dep.usedByExports = usedByExports;
  370. addPureDependency(module, dep);
  371. break;
  372. }
  373. }
  374. });
  375. }
  376. }
  377. }
  378. });
  379. parser.hooks.classExtendsExpression.tap(
  380. PLUGIN_NAME,
  381. (expr, statement) => {
  382. if (!innerGraph.isEnabled(parser.state)) return;
  383. if (parser.scope.topLevelScope === true) {
  384. const symbol = classWithTopLevelSymbol.get(statement);
  385. if (
  386. symbol &&
  387. parser.isPure(
  388. expr,
  389. statement.id
  390. ? /** @type {Range} */ (statement.id.range)[1]
  391. : /** @type {Range} */ (statement.range)[0]
  392. )
  393. ) {
  394. innerGraph.setTopLevelSymbol(parser.state, symbol);
  395. onUsageSuper(expr);
  396. }
  397. }
  398. }
  399. );
  400. parser.hooks.classBodyElement.tap(
  401. PLUGIN_NAME,
  402. (element, classDefinition) => {
  403. if (!innerGraph.isEnabled(parser.state)) return;
  404. if (parser.scope.topLevelScope === true) {
  405. const symbol = classWithTopLevelSymbol.get(classDefinition);
  406. if (symbol) {
  407. innerGraph.setTopLevelSymbol(parser.state, undefined);
  408. }
  409. }
  410. }
  411. );
  412. parser.hooks.classBodyValue.tap(
  413. PLUGIN_NAME,
  414. (expression, element, classDefinition) => {
  415. if (!innerGraph.isEnabled(parser.state)) return;
  416. if (parser.scope.topLevelScope === true) {
  417. const symbol = classWithTopLevelSymbol.get(classDefinition);
  418. if (symbol) {
  419. if (
  420. !element.static ||
  421. parser.isPure(
  422. expression,
  423. element.key
  424. ? /** @type {Range} */ (element.key.range)[1]
  425. : /** @type {Range} */ (element.range)[0]
  426. )
  427. ) {
  428. innerGraph.setTopLevelSymbol(parser.state, symbol);
  429. if (element.type !== "MethodDefinition" && element.static) {
  430. // the callback fires after parsing, when the parser can
  431. // no longer map offsets — take the location now
  432. const loc = parser.getLocation(expression);
  433. innerGraph.onUsage(
  434. parser.state,
  435. (usedByExports, module) => {
  436. switch (usedByExports) {
  437. case undefined:
  438. case true:
  439. return;
  440. default: {
  441. const dep = new PureExpressionDependency(
  442. /** @type {Range} */ (expression.range)
  443. );
  444. dep.loc = loc;
  445. dep.usedByExports = usedByExports;
  446. addPureDependency(module, dep);
  447. break;
  448. }
  449. }
  450. }
  451. );
  452. }
  453. } else {
  454. innerGraph.setTopLevelSymbol(parser.state, undefined);
  455. }
  456. }
  457. }
  458. }
  459. );
  460. parser.hooks.declarator.tap(PLUGIN_NAME, (decl, _statement) => {
  461. if (!innerGraph.isEnabled(parser.state)) return;
  462. const symbol = declWithTopLevelSymbol.get(decl);
  463. if (symbol) {
  464. innerGraph.setTopLevelSymbol(parser.state, symbol);
  465. if (pureDeclarators.has(decl)) {
  466. if (
  467. /** @type {ClassExpression} */
  468. (decl.init).type === "ClassExpression"
  469. ) {
  470. if (decl.init.superClass) {
  471. onUsageSuper(decl.init.superClass);
  472. }
  473. } else {
  474. // the callback fires after parsing, when the parser can no
  475. // longer map offsets — take the location now
  476. const loc = parser.getLocation(decl);
  477. innerGraph.onUsage(parser.state, (usedByExports, module) => {
  478. switch (usedByExports) {
  479. case undefined:
  480. case true:
  481. return;
  482. default: {
  483. const dep = new PureExpressionDependency(
  484. /** @type {Range} */ (
  485. /** @type {ClassExpression} */
  486. (decl.init).range
  487. )
  488. );
  489. dep.loc = loc;
  490. dep.usedByExports = usedByExports;
  491. addPureDependency(module, dep);
  492. break;
  493. }
  494. }
  495. });
  496. }
  497. }
  498. parser.walkExpression(
  499. /** @type {NonNullable<VariableDeclarator["init"]>} */ (
  500. decl.init
  501. )
  502. );
  503. innerGraph.setTopLevelSymbol(parser.state, undefined);
  504. return true;
  505. } else if (
  506. decl.id.type === "Identifier" &&
  507. decl.init &&
  508. decl.init.type === "ClassExpression" &&
  509. classWithTopLevelSymbol.has(decl.init)
  510. ) {
  511. parser.walkExpression(decl.init);
  512. innerGraph.setTopLevelSymbol(parser.state, undefined);
  513. return true;
  514. }
  515. });
  516. parser.hooks.expression
  517. .for(topLevelSymbolTag)
  518. .tap(PLUGIN_NAME, () => {
  519. const topLevelSymbol = /** @type {TopLevelSymbol} */ (
  520. parser.currentTagData
  521. );
  522. const currentTopLevelSymbol = innerGraph.getTopLevelSymbol(
  523. parser.state
  524. );
  525. innerGraph.addUsage(
  526. parser.state,
  527. topLevelSymbol,
  528. currentTopLevelSymbol || true
  529. );
  530. });
  531. parser.hooks.assign
  532. .for(topLevelSymbolTag)
  533. .tap(PLUGIN_NAME, (expr) => {
  534. if (!innerGraph.isEnabled(parser.state)) return;
  535. if (expr.operator === "=") return true;
  536. });
  537. };
  538. normalModuleFactory.hooks.parser
  539. .for(JAVASCRIPT_MODULE_TYPE_AUTO)
  540. .tap(PLUGIN_NAME, handler);
  541. normalModuleFactory.hooks.parser
  542. .for(JAVASCRIPT_MODULE_TYPE_ESM)
  543. .tap(PLUGIN_NAME, handler);
  544. compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => {
  545. logger.time("infer dependency usage");
  546. for (const module of modules) {
  547. innerGraph.inferDependencyUsage(module);
  548. // state is dead after inference; release it to not retain ASTs via callbacks
  549. innerGraph.release(module);
  550. }
  551. logger.timeEnd("infer dependency usage");
  552. });
  553. }
  554. );
  555. }
  556. }
  557. module.exports = InnerGraphPlugin;