CircularModulesPlugin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Natsu @xiaoxiaojx
  4. */
  5. "use strict";
  6. const CircularDependenciesWarning = require("./errors/CircularDependenciesWarning");
  7. const { compareModulesByIdentifier } = require("./util/comparators");
  8. /** @import { PerformanceOptions } from "../declarations/WebpackOptions" */
  9. /** @import Compiler from "./Compiler" */
  10. /** @import ModuleGraph from "./ModuleGraph" */
  11. /** @import ModuleGraphConnection from "./ModuleGraphConnection" */
  12. /** @import Module, { BuildInfo } from "./Module" */
  13. /** @import RequestShortener from "./RequestShortener" */
  14. const PLUGIN_NAME = "CircularModulesPlugin";
  15. // Enough to name the worst tangles without printing the module graph.
  16. const MAX_REPORTED_CYCLES = 5;
  17. /**
  18. * The module a connection makes its origin evaluate before it can run, or
  19. * `null` when it does not: a weak reference never loads its target, and an
  20. * async edge lives in an `AsyncDependenciesBlock`, so a synchronous
  21. * dependency's parent block is the module itself. A self-reference is
  22. * returned — how CommonJS reads its own `module.exports` — and is the
  23. * caller's to interpret.
  24. * @param {ModuleGraphConnection} connection an outgoing connection
  25. * @param {Module} module the module the connection starts at
  26. * @param {ModuleGraph} moduleGraph the module graph
  27. * @returns {Module | null} the module evaluated first, or `null`
  28. */
  29. const getSynchronousTarget = (connection, module, moduleGraph) => {
  30. const dependency = connection.dependency;
  31. if (!dependency) return null;
  32. const target = connection.module;
  33. if (!target || connection.weak) return null;
  34. if (moduleGraph.getParentBlock(dependency) !== module) return null;
  35. return target;
  36. };
  37. /**
  38. * The member a group is reported from. Traversal order is not stable across
  39. * runs, so the lowest identifier is picked rather than the one found first.
  40. * @param {Module[]} members one group of modules that can all reach each other
  41. * @returns {Module} the module the report starts at
  42. */
  43. const getCanonicalMember = (members) => {
  44. let canonical = members[0];
  45. for (const member of members) {
  46. if (compareModulesByIdentifier(member, canonical) < 0) {
  47. canonical = member;
  48. }
  49. }
  50. return canonical;
  51. };
  52. /**
  53. * The shortest cycle through the canonical module of a group, as a readable
  54. * path ending back where it started.
  55. * @param {Module[]} members one group of modules that can all reach each other
  56. * @param {ModuleGraph} moduleGraph the module graph
  57. * @param {RequestShortener} requestShortener the request shortener
  58. * @returns {string} the cycle path
  59. */
  60. const formatShortestCycle = (members, moduleGraph, requestShortener) => {
  61. const group = new Set(members);
  62. const start = getCanonicalMember(members);
  63. /** @type {Map<Module, Module>} */
  64. const previous = new Map();
  65. const queue = [start];
  66. /** @type {Module | undefined} */
  67. let last;
  68. for (let i = 0; i < queue.length && last === undefined; i++) {
  69. const module = queue[i];
  70. for (const connection of moduleGraph.getOutgoingConnections(module)) {
  71. const target = getSynchronousTarget(connection, module, moduleGraph);
  72. // A module reading its own exports would otherwise collapse the path.
  73. if (target === null || target === module || !group.has(target)) continue;
  74. if (target === start) {
  75. last = module;
  76. break;
  77. }
  78. if (previous.has(target)) continue;
  79. previous.set(target, module);
  80. queue.push(target);
  81. }
  82. }
  83. const path = [start];
  84. for (
  85. let module = last;
  86. module !== undefined && module !== start;
  87. module = previous.get(module)
  88. ) {
  89. path.push(module);
  90. }
  91. path.push(start);
  92. return path
  93. .reverse()
  94. .map((module) => module.readableIdentifier(requestShortener))
  95. .join(" -> ");
  96. };
  97. /**
  98. * Detects circular dependencies among synchronous module imports.
  99. *
  100. * Builds an adjacency layout from each module's synchronous outgoing
  101. * connections (skipping weak and async-block edges), then runs an iterative
  102. * SCC algorithm to find circular modules.
  103. *
  104. * Use the static `build()` method to create an instance. All intermediate data
  105. * (adjacency layout, index mappings) is local to `build()` and released on
  106. * return. The instance only holds the result.
  107. */
  108. class CycleGraph {
  109. /**
  110. * @param {Set<Module>} circularModules modules in a multi-module SCC or with a self-loop (for isCircular)
  111. * @param {Module[][]} circularGroups multi-module SCC groups only (for reporting; self-loops omitted)
  112. */
  113. constructor(circularModules, circularGroups) {
  114. /** @type {Set<Module>} */
  115. this.circularModules = circularModules;
  116. /** @type {Module[][]} */
  117. this.circularGroups = circularGroups;
  118. }
  119. /**
  120. * Builds a CycleGraph by constructing the synchronous outgoing-connection
  121. * adjacency list and running iterative SCC to detect circular modules.
  122. * @param {Iterable<Module>} modules the set of modules
  123. * @param {ModuleGraph} moduleGraph the module graph
  124. * @param {boolean=} collectGroups also group the modules of each cycle, which
  125. * only the hint reads — marking `isCircular` needs the set alone
  126. * @returns {CycleGraph} the result
  127. */
  128. static build(modules, moduleGraph, collectGroups = false) {
  129. /** @type {Module[]} */
  130. const moduleList = [];
  131. /** @type {Map<Module, number>} */
  132. const moduleToIndex = new Map();
  133. for (const module of modules) {
  134. moduleToIndex.set(module, moduleList.length);
  135. moduleList.push(module);
  136. }
  137. const size = moduleList.length;
  138. if (size === 0) return new CycleGraph(new Set(), []);
  139. /** @type {number[][]} */
  140. const edges = Array.from({ length: size });
  141. /** @type {boolean[]} */
  142. const selfLoops = Array.from({ length: size }, () => false);
  143. for (let i = 0; i < size; i++) {
  144. const module = moduleList[i];
  145. /** @type {number[]} */
  146. const deps = [];
  147. for (const connection of moduleGraph.getOutgoingConnections(module)) {
  148. const target = getSynchronousTarget(connection, module, moduleGraph);
  149. if (target === null) continue;
  150. if (target === module) {
  151. selfLoops[i] = true;
  152. continue;
  153. }
  154. const targetIdx = moduleToIndex.get(target);
  155. if (targetIdx !== undefined) {
  156. deps.push(targetIdx);
  157. }
  158. }
  159. edges[i] = deps;
  160. }
  161. // Iterative SCC algorithm
  162. /** @type {Set<Module>} */
  163. const circularModules = new Set();
  164. /** @type {Module[][]} */
  165. const circularGroups = [];
  166. let nextIndex = 0;
  167. const nodeIndex = new Int32Array(size).fill(-1);
  168. const nodeLowLink = new Int32Array(size);
  169. const nodeOnStack = new Uint8Array(size);
  170. /** @type {number[]} */
  171. const sccStack = [];
  172. /**
  173. * @typedef {object} Frame
  174. * @property {number} node
  175. * @property {number} edgeIdx
  176. * @property {number} parent
  177. */
  178. for (let root = 0; root < size; root++) {
  179. if (nodeIndex[root] !== -1) continue;
  180. nodeIndex[root] = nextIndex;
  181. nodeLowLink[root] = nextIndex;
  182. nextIndex++;
  183. nodeOnStack[root] = 1;
  184. sccStack.push(root);
  185. /** @type {Frame[]} */
  186. const callStack = [{ node: root, edgeIdx: 0, parent: -1 }];
  187. while (callStack.length > 0) {
  188. const frame = /** @type {Frame} */ (callStack[callStack.length - 1]);
  189. const v = frame.node;
  190. const vEdges = edges[v];
  191. if (frame.edgeIdx < vEdges.length) {
  192. const w = vEdges[frame.edgeIdx++];
  193. if (nodeIndex[w] === -1) {
  194. nodeIndex[w] = nextIndex;
  195. nodeLowLink[w] = nextIndex;
  196. nextIndex++;
  197. nodeOnStack[w] = 1;
  198. sccStack.push(w);
  199. callStack.push({ node: w, edgeIdx: 0, parent: v });
  200. } else if (nodeOnStack[w] && nodeIndex[w] < nodeLowLink[v]) {
  201. nodeLowLink[v] = nodeIndex[w];
  202. }
  203. } else {
  204. if (nodeLowLink[v] === nodeIndex[v]) {
  205. /** @type {number[]} */
  206. const group = [];
  207. let w;
  208. do {
  209. w = /** @type {number} */ (sccStack.pop());
  210. nodeOnStack[w] = 0;
  211. group.push(w);
  212. } while (w !== v);
  213. if (group.length > 1 || selfLoops[v]) {
  214. for (const idx of group) {
  215. circularModules.add(moduleList[idx]);
  216. }
  217. }
  218. // A module that only references itself is no cycle, so it is
  219. // circular for inlining but never a group to report.
  220. if (collectGroups && group.length > 1) {
  221. circularGroups.push(group.map((idx) => moduleList[idx]));
  222. }
  223. }
  224. callStack.pop();
  225. if (
  226. frame.parent !== -1 &&
  227. nodeLowLink[v] < nodeLowLink[frame.parent]
  228. ) {
  229. nodeLowLink[frame.parent] = nodeLowLink[v];
  230. }
  231. }
  232. }
  233. }
  234. return new CycleGraph(circularModules, circularGroups);
  235. }
  236. }
  237. /**
  238. * @typedef {object} CircularModulesPluginOptions
  239. * @property {PerformanceOptions["hints"]=} hints when set, report SCC groups
  240. */
  241. /**
  242. * One SCC scan: marks isCircular on every module, and reports groups when hints are set.
  243. */
  244. class CircularModulesPlugin {
  245. /**
  246. * @param {CircularModulesPluginOptions=} options options
  247. */
  248. constructor(options) {
  249. /** @type {PerformanceOptions["hints"] | undefined} */
  250. this.hints = options && options.hints;
  251. }
  252. /**
  253. * @param {Compiler} compiler the compiler instance
  254. * @returns {void}
  255. */
  256. apply(compiler) {
  257. const hints = this.hints;
  258. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  259. /** @type {CircularDependenciesWarning | undefined} */
  260. let warning;
  261. compilation.hooks.optimizeModules.tap(PLUGIN_NAME, (modules) => {
  262. const { circularModules, circularGroups } = CycleGraph.build(
  263. modules,
  264. compilation.moduleGraph,
  265. Boolean(hints)
  266. );
  267. // Must be an explicit boolean: ConstExportsPlugin checks `=== false`.
  268. for (const m of modules) {
  269. /** @type {BuildInfo} */
  270. (m.buildInfo).isCircular = circularModules.has(m);
  271. }
  272. if (!hints) return;
  273. if (circularGroups.length === 0) return;
  274. const { moduleGraph, requestShortener } = compilation;
  275. // Largest tangle first; ties break by name, as the order groups are
  276. // discovered in is not stable.
  277. circularGroups.sort(
  278. (a, b) =>
  279. b.length - a.length ||
  280. compareModulesByIdentifier(
  281. getCanonicalMember(a),
  282. getCanonicalMember(b)
  283. )
  284. );
  285. // The shortest cycle of a large group names two of its modules, so the
  286. // size travels with it — otherwise the group reads as a pair.
  287. const groups = circularGroups
  288. .slice(0, MAX_REPORTED_CYCLES)
  289. .map((members) => ({
  290. size: members.length,
  291. cycle: formatShortestCycle(members, moduleGraph, requestShortener)
  292. }));
  293. warning = new CircularDependenciesWarning(
  294. groups,
  295. circularGroups.length
  296. );
  297. });
  298. // Reported past the hash: `createHash` folds every message into it, so
  299. // a hint pushed earlier would change the build's identity.
  300. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  301. if (warning === undefined) return;
  302. if (hints === "error") {
  303. compilation.errors.push(warning);
  304. } else if (hints === "stats") {
  305. compilation.hints.push(warning);
  306. } else {
  307. compilation.warnings.push(warning);
  308. }
  309. });
  310. });
  311. }
  312. }
  313. module.exports = CircularModulesPlugin;