SideEffectsFlagPlugin.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const glob2regexp = require("glob-to-regexp");
  7. const {
  8. JAVASCRIPT_MODULE_TYPE_AUTO,
  9. JAVASCRIPT_MODULE_TYPE_DYNAMIC,
  10. JAVASCRIPT_MODULE_TYPE_ESM
  11. } = require("../ModuleTypeConstants");
  12. const { STAGE_DEFAULT } = require("../OptimizationStages");
  13. const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
  14. const HarmonyImportSpecifierDependency = require("../dependencies/HarmonyImportSpecifierDependency");
  15. const formatLocation = require("../formatLocation");
  16. /** @typedef {import("estree").MaybeNamedClassDeclaration} MaybeNamedClassDeclaration */
  17. /** @typedef {import("estree").MaybeNamedFunctionDeclaration} MaybeNamedFunctionDeclaration */
  18. /** @typedef {import("estree").ModuleDeclaration} ModuleDeclaration */
  19. /** @typedef {import("estree").Statement} Statement */
  20. /** @typedef {import("../Compiler")} Compiler */
  21. /** @typedef {import("../Dependency").DependencyLocation} DependencyLocation */
  22. /** @typedef {import("../Module")} Module */
  23. /** @typedef {import("../Module").BuildMeta} BuildMeta */
  24. /** @typedef {import("../ModuleGraphConnection")} ModuleGraphConnection */
  25. /** @typedef {import("../NormalModuleFactory").ModuleSettings} ModuleSettings */
  26. /** @typedef {import("../javascript/JavascriptParser")} JavascriptParser */
  27. /** @typedef {import("../javascript/JavascriptParser").Range} Range */
  28. /**
  29. * @typedef {object} ExportInModule
  30. * @property {Module} module the module
  31. * @property {string} exportName the name of the export
  32. * @property {boolean} checked if the export is conditional
  33. */
  34. /** @typedef {string | boolean | string[] | undefined} SideEffectsFlagValue */
  35. /** @typedef {Map<string, RegExp>} CacheItem */
  36. /** @type {WeakMap<Compiler, CacheItem>} */
  37. const globToRegexpCache = new WeakMap();
  38. /**
  39. * @param {string} glob the pattern
  40. * @param {CacheItem} cache the glob to RegExp cache
  41. * @returns {RegExp} a regular expression
  42. */
  43. const globToRegexp = (glob, cache) => {
  44. const cacheEntry = cache.get(glob);
  45. if (cacheEntry !== undefined) return cacheEntry;
  46. if (!glob.includes("/")) {
  47. glob = `**/${glob}`;
  48. }
  49. const baseRegexp = glob2regexp(glob, { globstar: true, extended: true });
  50. const regexpSource = baseRegexp.source;
  51. const regexp = new RegExp(`^(\\./)?${regexpSource.slice(1)}`);
  52. cache.set(glob, regexp);
  53. return regexp;
  54. };
  55. const PLUGIN_NAME = "SideEffectsFlagPlugin";
  56. class SideEffectsFlagPlugin {
  57. /**
  58. * @param {boolean} analyseSource analyse source code for side effects
  59. */
  60. constructor(analyseSource = true) {
  61. /** @type {boolean} */
  62. this._analyseSource = analyseSource;
  63. }
  64. /**
  65. * Apply the plugin
  66. * @param {Compiler} compiler the compiler instance
  67. * @returns {void}
  68. */
  69. apply(compiler) {
  70. let cache = globToRegexpCache.get(compiler.root);
  71. if (cache === undefined) {
  72. cache = new Map();
  73. globToRegexpCache.set(compiler.root, cache);
  74. }
  75. compiler.hooks.compilation.tap(
  76. PLUGIN_NAME,
  77. (compilation, { normalModuleFactory }) => {
  78. const moduleGraph = compilation.moduleGraph;
  79. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  80. const resolveData = data.resourceResolveData;
  81. if (
  82. resolveData &&
  83. resolveData.descriptionFileData &&
  84. resolveData.relativePath
  85. ) {
  86. const sideEffects = resolveData.descriptionFileData.sideEffects;
  87. if (sideEffects !== undefined) {
  88. if (module.factoryMeta === undefined) {
  89. module.factoryMeta = {};
  90. }
  91. const hasSideEffects = SideEffectsFlagPlugin.moduleHasSideEffects(
  92. resolveData.relativePath,
  93. /** @type {SideEffectsFlagValue} */ (sideEffects),
  94. /** @type {CacheItem} */ (cache)
  95. );
  96. module.factoryMeta.sideEffectFree = !hasSideEffects;
  97. }
  98. }
  99. return module;
  100. });
  101. normalModuleFactory.hooks.module.tap(PLUGIN_NAME, (module, data) => {
  102. const settings = /** @type {ModuleSettings} */ (data.settings);
  103. if (typeof settings.sideEffects === "boolean") {
  104. if (module.factoryMeta === undefined) {
  105. module.factoryMeta = {};
  106. }
  107. module.factoryMeta.sideEffectFree = !settings.sideEffects;
  108. }
  109. return module;
  110. });
  111. if (this._analyseSource) {
  112. /**
  113. * @param {JavascriptParser} parser the parser
  114. * @returns {void}
  115. */
  116. const parserHandler = (parser) => {
  117. /** @type {undefined | Statement | ModuleDeclaration | MaybeNamedFunctionDeclaration | MaybeNamedClassDeclaration} */
  118. let sideEffectsStatement;
  119. parser.hooks.program.tap(PLUGIN_NAME, () => {
  120. sideEffectsStatement = undefined;
  121. });
  122. parser.hooks.statement.tap(
  123. { name: PLUGIN_NAME, stage: -100 },
  124. (statement) => {
  125. if (sideEffectsStatement) return;
  126. if (parser.scope.topLevelScope !== true) return;
  127. switch (statement.type) {
  128. case "ExpressionStatement":
  129. if (
  130. !parser.isPure(
  131. statement.expression,
  132. /** @type {Range} */
  133. (statement.range)[0]
  134. )
  135. ) {
  136. sideEffectsStatement = statement;
  137. }
  138. break;
  139. case "IfStatement":
  140. case "WhileStatement":
  141. case "DoWhileStatement":
  142. if (
  143. !parser.isPure(
  144. statement.test,
  145. /** @type {Range} */
  146. (statement.range)[0]
  147. )
  148. ) {
  149. sideEffectsStatement = statement;
  150. }
  151. // statement hook will be called for child statements too
  152. break;
  153. case "ForStatement":
  154. if (
  155. !parser.isPure(
  156. statement.init,
  157. /** @type {Range} */ (statement.range)[0]
  158. ) ||
  159. !parser.isPure(
  160. statement.test,
  161. statement.init
  162. ? /** @type {Range} */ (statement.init.range)[1]
  163. : /** @type {Range} */ (statement.range)[0]
  164. ) ||
  165. !parser.isPure(
  166. statement.update,
  167. statement.test
  168. ? /** @type {Range} */ (statement.test.range)[1]
  169. : statement.init
  170. ? /** @type {Range} */ (statement.init.range)[1]
  171. : /** @type {Range} */ (statement.range)[0]
  172. )
  173. ) {
  174. sideEffectsStatement = statement;
  175. }
  176. // statement hook will be called for child statements too
  177. break;
  178. case "SwitchStatement":
  179. if (
  180. !parser.isPure(
  181. statement.discriminant,
  182. /** @type {Range} */
  183. (statement.range)[0]
  184. )
  185. ) {
  186. sideEffectsStatement = statement;
  187. }
  188. // statement hook will be called for child statements too
  189. break;
  190. case "VariableDeclaration":
  191. case "ClassDeclaration":
  192. case "FunctionDeclaration":
  193. if (
  194. !parser.isPure(
  195. statement,
  196. /** @type {Range} */ (statement.range)[0]
  197. )
  198. ) {
  199. sideEffectsStatement = statement;
  200. }
  201. break;
  202. case "ExportNamedDeclaration":
  203. case "ExportDefaultDeclaration":
  204. if (
  205. !parser.isPure(
  206. statement.declaration,
  207. /** @type {Range} */
  208. (statement.range)[0]
  209. )
  210. ) {
  211. sideEffectsStatement = statement;
  212. }
  213. break;
  214. case "LabeledStatement":
  215. case "BlockStatement":
  216. // statement hook will be called for child statements too
  217. break;
  218. case "EmptyStatement":
  219. break;
  220. case "ExportAllDeclaration":
  221. case "ImportDeclaration":
  222. // imports will be handled by the dependencies
  223. break;
  224. default:
  225. sideEffectsStatement = statement;
  226. break;
  227. }
  228. }
  229. );
  230. parser.hooks.finish.tap(PLUGIN_NAME, () => {
  231. if (sideEffectsStatement === undefined) {
  232. /** @type {BuildMeta} */
  233. (parser.state.module.buildMeta).sideEffectFree = true;
  234. } else {
  235. const { loc, type } = sideEffectsStatement;
  236. moduleGraph
  237. .getOptimizationBailout(parser.state.module)
  238. .push(
  239. () =>
  240. `Statement (${type}) with side effects in source code at ${formatLocation(
  241. /** @type {DependencyLocation} */ (loc)
  242. )}`
  243. );
  244. }
  245. });
  246. };
  247. for (const key of [
  248. JAVASCRIPT_MODULE_TYPE_AUTO,
  249. JAVASCRIPT_MODULE_TYPE_ESM,
  250. JAVASCRIPT_MODULE_TYPE_DYNAMIC
  251. ]) {
  252. normalModuleFactory.hooks.parser
  253. .for(key)
  254. .tap(PLUGIN_NAME, parserHandler);
  255. }
  256. }
  257. compilation.hooks.optimizeDependencies.tap(
  258. {
  259. name: PLUGIN_NAME,
  260. stage: STAGE_DEFAULT
  261. },
  262. (modules) => {
  263. const logger = compilation.getLogger(
  264. "webpack.SideEffectsFlagPlugin"
  265. );
  266. logger.time("update dependencies");
  267. /** @type {Set<Module>} */
  268. const optimizedModules = new Set();
  269. /**
  270. * @param {Module} module module
  271. */
  272. const optimizeIncomingConnections = (module) => {
  273. if (optimizedModules.has(module)) return;
  274. optimizedModules.add(module);
  275. if (module.getSideEffectsConnectionState(moduleGraph) === false) {
  276. const exportsInfo = moduleGraph.getExportsInfo(module);
  277. for (const connection of moduleGraph.getIncomingConnections(
  278. module
  279. )) {
  280. const dep = connection.dependency;
  281. /** @type {boolean} */
  282. let isReexport;
  283. if (
  284. (isReexport =
  285. dep instanceof
  286. HarmonyExportImportedSpecifierDependency) ||
  287. (dep instanceof HarmonyImportSpecifierDependency &&
  288. !dep.namespaceObjectAsContext)
  289. ) {
  290. if (connection.originModule !== null) {
  291. optimizeIncomingConnections(connection.originModule);
  292. }
  293. // TODO improve for export *
  294. if (isReexport && dep.name) {
  295. const exportInfo = moduleGraph.getExportInfo(
  296. /** @type {Module} */ (connection.originModule),
  297. dep.name
  298. );
  299. exportInfo.moveTarget(
  300. moduleGraph,
  301. ({ module }) =>
  302. module.getSideEffectsConnectionState(moduleGraph) ===
  303. false,
  304. ({
  305. module: newModule,
  306. export: exportName,
  307. connection: targetConnection
  308. }) => {
  309. moduleGraph.updateModule(dep, newModule);
  310. moduleGraph.updateParent(
  311. dep,
  312. targetConnection,
  313. /** @type {Module} */ (connection.originModule)
  314. );
  315. moduleGraph.addExplanation(
  316. dep,
  317. "(skipped side-effect-free modules)"
  318. );
  319. const ids = dep.getIds(moduleGraph);
  320. dep.setIds(
  321. moduleGraph,
  322. exportName
  323. ? [...exportName, ...ids.slice(1)]
  324. : ids.slice(1)
  325. );
  326. return /** @type {ModuleGraphConnection} */ (
  327. moduleGraph.getConnection(dep)
  328. );
  329. }
  330. );
  331. continue;
  332. }
  333. // TODO improve for nested imports
  334. const ids = dep.getIds(moduleGraph);
  335. if (ids.length > 0) {
  336. const exportInfo = exportsInfo.getExportInfo(ids[0]);
  337. const target = exportInfo.getTarget(
  338. moduleGraph,
  339. ({ module }) =>
  340. module.getSideEffectsConnectionState(moduleGraph) ===
  341. false
  342. );
  343. if (!target) continue;
  344. moduleGraph.updateModule(dep, target.module);
  345. moduleGraph.updateParent(
  346. dep,
  347. /** @type {ModuleGraphConnection} */ (
  348. target.connection
  349. ),
  350. /** @type {Module} */ (connection.originModule)
  351. );
  352. moduleGraph.addExplanation(
  353. dep,
  354. "(skipped side-effect-free modules)"
  355. );
  356. dep.setIds(
  357. moduleGraph,
  358. target.export
  359. ? [...target.export, ...ids.slice(1)]
  360. : ids.slice(1)
  361. );
  362. }
  363. }
  364. }
  365. }
  366. };
  367. for (const module of modules) {
  368. optimizeIncomingConnections(module);
  369. }
  370. moduleGraph.finishUpdateParent();
  371. logger.timeEnd("update dependencies");
  372. }
  373. );
  374. }
  375. );
  376. }
  377. /**
  378. * @param {string} moduleName the module name
  379. * @param {SideEffectsFlagValue} flagValue the flag value
  380. * @param {CacheItem} cache cache for glob to regexp
  381. * @returns {boolean | undefined} true, when the module has side effects, undefined or false when not
  382. */
  383. static moduleHasSideEffects(moduleName, flagValue, cache) {
  384. switch (typeof flagValue) {
  385. case "undefined":
  386. return true;
  387. case "boolean":
  388. return flagValue;
  389. case "string":
  390. return globToRegexp(flagValue, cache).test(moduleName);
  391. case "object":
  392. return flagValue.some((glob) =>
  393. SideEffectsFlagPlugin.moduleHasSideEffects(moduleName, glob, cache)
  394. );
  395. }
  396. }
  397. }
  398. module.exports = SideEffectsFlagPlugin;