package unit; import java.util.*; /** * Unit test for webpack-0001/0002/0003: CWE-407 in webpack HMR runtime. * * webpack-0001 (MEDIUM-HIGH): * File: lib/hmr/JavascriptHotModuleReplacement.runtime.js:74 * Symbol: getAffectedModuleEffects — `outdatedModules.indexOf(parentId)` * Defect: BFS traversal over a module dependency graph uses an Array for the * outdatedModules visited set. For each queued module, for each parent, * indexOf scans the entire array: O(M) per check, O(M²) total. * Fix: Shadow with a Set; check becomes Set.has(), O(1). * * webpack-0002 (MEDIUM): * File: lib/hmr/JavascriptHotModuleReplacement.runtime.js:101 * Symbol: addAllToSet — `a.indexOf(item)` * Defect: Result-merging helper deduplicates by scanning the accumulator * array on every insertion: O(N) per item, O(N²) total for N items. * Fix: Maintain a companion Set on the accumulator array; check is O(1). * * webpack-0003 (MEDIUM): * File: lib/hmr/HotModuleReplacement.runtime.js:60,67 * Symbol: createRequire — `parents.indexOf(moduleId)`, `me.children.indexOf(request)` * Defect: Every require() call in the HMR hot path deduplicates parents and * children using indexOf on plain arrays: O(P) and O(C) per call. * Fix: Attach Set companions (_parentsSet, _childrenSet); O(1) per check. * * Modeled here in Java: * JS Array + indexOf ≡ List + contains/indexOf (defective) * JS Set + has ≡ java.util.Set + contains (fixed) * Comparison counts tracked at the membership-test site. * * Expected at M=200: * defective BFS comparisons ≈ M² = 40 000 * fixed BFS comparisons ≈ M = 200 * ratio > 10× */ public class WebpackHmrTest { // ========================================================================= // webpack-0001 model: BFS with outdatedModules as Array vs Set // ========================================================================= /** * Defective BFS: outdatedModules is a plain List. * For each (module, parent) pair we call list.contains(), which is O(size). * comparisons counts every element examined during indexOf scans. */ static class DefectiveBfs { long comparisons = 0; /** * Simulate getAffectedModuleEffects: process a chain of M modules where * each module has one parent, and the parent is always "not yet in * outdatedModules" (worst-case: no early-exit, full scan every time). * * Graph: 0 → 1 → 2 → ... → M-1 (child → parent direction). * BFS starts from module 0, propagates up. */ List run(int M) { List outdatedModules = new ArrayList<>(); outdatedModules.add(0); // Queue: (moduleId). We process each and check its single parent. Queue queue = new ArrayDeque<>(); queue.add(0); while (!queue.isEmpty()) { int moduleId = queue.poll(); int parentId = moduleId + 1; // parent in linear chain if (parentId >= M) continue; // no parent beyond chain end // outdatedModules.indexOf(parentId) — O(current size) scan boolean found = false; for (int x : outdatedModules) { comparisons++; if (x == parentId) { found = true; break; } } if (found) continue; outdatedModules.add(parentId); queue.add(parentId); } return outdatedModules; } } /** * Fixed BFS: shadow outdatedModules with a Set for O(1) membership. * comparisons counts one hash-probe per Set.contains() call. */ static class FixedBfs { long comparisons = 0; List run(int M) { List outdatedModules = new ArrayList<>(); Set outdatedSet = new HashSet<>(); outdatedModules.add(0); outdatedSet.add(0); Queue queue = new ArrayDeque<>(); queue.add(0); while (!queue.isEmpty()) { int moduleId = queue.poll(); int parentId = moduleId + 1; if (parentId >= M) continue; // Set.contains(parentId) — O(1) comparisons++; if (outdatedSet.contains(parentId)) continue; outdatedModules.add(parentId); outdatedSet.add(parentId); queue.add(parentId); } return outdatedModules; } } // ========================================================================= // webpack-0002 model: addAllToSet with Array indexOf vs Set companion // ========================================================================= /** * Defective addAllToSet: accumulator is a plain List; every insertion * scans the list to deduplicate. * comparisons counts every element examined during indexOf scans. */ static long addAllToSetDefective(List a, List b, long[] comparisons) { for (int item : b) { // a.indexOf(item) — O(a.size()) scan boolean found = false; for (int x : a) { comparisons[0]++; if (x == item) { found = true; break; } } if (!found) a.add(item); } return comparisons[0]; } /** * Fixed addAllToSet: companion Set maintained alongside the List. * comparisons counts one hash-probe per Set.contains() call. */ static long addAllToSetFixed(List a, Set aSet, List b, long[] comparisons) { for (int item : b) { comparisons[0]++; // O(1) Set.contains if (!aSet.contains(item)) { a.add(item); aSet.add(item); } } return comparisons[0]; } // ========================================================================= // webpack-0003 model: parents/children dedup in require() hot path // ========================================================================= /** * Defective require(): parents and children deduped via indexOf on plain arrays. * Simulates N require() calls where each call checks both parents and children. * comparisons counts every element examined during indexOf scans. */ static long simulateRequireDefective(int N) { long comparisons = 0; List parents = new ArrayList<>(); List children = new ArrayList<>(); for (int moduleId = 0; moduleId < N; moduleId++) { int request = moduleId + 1000; // distinct child module IDs // parents.indexOf(moduleId) — O(parents.size()) boolean foundParent = false; for (int x : parents) { comparisons++; if (x == moduleId) { foundParent = true; break; } } if (!foundParent) parents.add(moduleId); // me.children.indexOf(request) — O(children.size()) boolean foundChild = false; for (int x : children) { comparisons++; if (x == request) { foundChild = true; break; } } if (!foundChild) children.add(request); } return comparisons; } /** * Fixed require(): Set companions for O(1) dedup. * comparisons counts one hash-probe per check. */ static long simulateRequireFixed(int N) { long comparisons = 0; List parents = new ArrayList<>(); Set parentsSet = new HashSet<>(); List children = new ArrayList<>(); Set childrenSet = new HashSet<>(); for (int moduleId = 0; moduleId < N; moduleId++) { int request = moduleId + 1000; // parentsSet.contains(moduleId) — O(1) comparisons++; if (!parentsSet.contains(moduleId)) { parents.add(moduleId); parentsSet.add(moduleId); } // childrenSet.contains(request) — O(1) comparisons++; if (!childrenSet.contains(request)) { children.add(request); childrenSet.add(request); } } return comparisons; } // ========================================================================= // Tests // ========================================================================= /** * Test 1 — Correctness: defective and fixed BFS produce identical module sets. */ static void testBfsCorrectnessMatch() { int M = 50; DefectiveBfs def = new DefectiveBfs(); FixedBfs fix = new FixedBfs(); List defResult = def.run(M); List fixResult = fix.run(M); Set defSet = new HashSet<>(defResult); Set fixSet = new HashSet<>(fixResult); assert defSet.equals(fixSet) : "BFS output mismatch: defective=" + defSet + " fixed=" + fixSet; assert defResult.size() == M : "expected " + M + " outdated modules; got " + defResult.size(); System.out.println("PASS testBfsCorrectnessMatch"); } /** * Test 2 — webpack-0001: ratio of defective vs fixed BFS comparisons > 10x at M=200. * * Defective: each of M modules triggers a scan of the growing outdatedModules * list (size 0..M-1), summing to M*(M-1)/2 comparisons ~ O(M²). * Fixed: each module triggers exactly 1 Set probe, summing to M ~ O(M). */ static void testBfsRatioAtScale() { int M = 200; DefectiveBfs def = new DefectiveBfs(); FixedBfs fix = new FixedBfs(); def.run(M); fix.run(M); long defComp = def.comparisons; long fixComp = fix.comparisons; double ratio = (double) defComp / fixComp; // Defective: sum 0+1+...+(M-1) = M*(M-1)/2 long expectedDef = (long) M * (M - 1) / 2; assert defComp == expectedDef : "defective BFS comparisons should be M*(M-1)/2=" + expectedDef + "; got " + defComp; // Fixed: M-1 probes (one per module that has a parent to check; // the last module in the chain has no parent, so no probe is issued) assert fixComp == M - 1 : "fixed BFS comparisons should be M-1=" + (M - 1) + "; got " + fixComp; assert ratio > 10.0 : "ratio should be >10x at M=200; got " + ratio; System.out.printf( "PASS testBfsRatioAtScale (defective=%d, fixed=%d, ratio=%.1fx)%n", defComp, fixComp, ratio); } /** * Test 3 — webpack-0002: addAllToSet defective is O(N²), fixed is O(N). * * Merge N unique items into an accumulator. Defective scans the accumulator * before each insert (size 0..N-1), costing N*(N-1)/2. Fixed costs N probes. */ static void testAddAllToSetRatio() { int N = 200; // Build source list of N unique items List source = new ArrayList<>(); for (int i = 0; i < N; i++) source.add(i); // Defective long[] defComp = {0}; List defAcc = new ArrayList<>(); addAllToSetDefective(defAcc, source, defComp); // Fixed long[] fixComp = {0}; List fixAcc = new ArrayList<>(); Set fixSet = new HashSet<>(); addAllToSetFixed(fixAcc, fixSet, source, fixComp); assert defAcc.size() == N && fixAcc.size() == N : "both accumulators should have " + N + " items"; assert new HashSet<>(defAcc).equals(new HashSet<>(fixAcc)) : "accumulator contents differ"; double ratio = (double) defComp[0] / fixComp[0]; assert ratio > 10.0 : "addAllToSet ratio should be >10x at N=200; got " + ratio; System.out.printf( "PASS testAddAllToSetRatio (defective=%d, fixed=%d, ratio=%.1fx)%n", defComp[0], fixComp[0], ratio); } /** * Test 4 — webpack-0003: require() hot path defective is O(N²), fixed is O(N). * * N require() calls, each adding a unique moduleId to parents and a unique * request to children. Defective: scans grow 0..N-1 for each; fixed: O(1) probes. */ static void testRequireHotPathRatio() { int N = 200; long defComp = simulateRequireDefective(N); long fixComp = simulateRequireFixed(N); double ratio = (double) defComp / fixComp; // Defective: two indexOf scans per call, sizes grow 0..N-1 // Sum for parents: 0+1+...+(N-1) = N*(N-1)/2; same for children. long expectedDef = (long) N * (N - 1); // two arrays: 2 × N*(N-1)/2 assert defComp == expectedDef : "defective require comparisons should be N*(N-1)=" + expectedDef + "; got " + defComp; // Fixed: exactly 2 probes per call (one per Set) long expectedFix = 2L * N; assert fixComp == expectedFix : "fixed require comparisons should be 2*N=" + expectedFix + "; got " + fixComp; assert ratio > 10.0 : "require() ratio should be >10x at N=200; got " + ratio; System.out.printf( "PASS testRequireHotPathRatio (defective=%d, fixed=%d, ratio=%.1fx)%n", defComp, fixComp, ratio); } // ========================================================================= public static void main(String[] args) { testBfsCorrectnessMatch(); testBfsRatioAtScale(); testAddAllToSetRatio(); testRequireHotPathRatio(); System.out.println("All webpack HMR tests passed."); } }