package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; /** * ScalaRefChecksIntersectionTest — CWE-407 unit test for scala-0002 * * Models the O(D²) defect in RefChecks.checkAllOverrides: * * def intersectionIsEmpty(syms1: List[Symbol], syms2: List[Symbol]) = * !syms1.exists(syms2.contains) * * syms1 = member.extendedOverriddenSymbols (O(D) for hierarchy depth D) * syms2 = other.extendedOverriddenSymbols (O(D) for hierarchy depth D) * syms2.contains = List[Symbol].contains (O(D) linear scan) * * So per invocation: O(D) × O(D) = O(D²). * With M overriding pairs per class: O(M × D²) total per compilation unit. * * Fix: val s2 = syms2.toSet; !syms1.exists(s2.contains) * Build cost O(D), lookup cost O(1) → O(D) per invocation. * * slow(): simulates original — nested list scan. * fast(): simulates fix — build HashSet once, then O(1) membership. * * Uses D=200 (deep trait hierarchy) to show measurable ratio. * We assert slow() uses >= 10x more element operations than fast(). */ public class ScalaRefChecksIntersectionTest { static final int D = 200; // hierarchy depth — length of overriddenSymbols lists static final int M = 50; // number of overriding method pairs (typical complex class) static final int N = 10; // minimum speedup factor required /** * Simulates: !syms1.exists(syms2.contains) * where syms1 and syms2 are List[Symbol] of length D. * Counts total element comparisons across M pairs. */ static long slow() { long ops = 0; for (int pair = 0; pair < M; pair++) { // syms1: symbols 0..D-1 List syms1 = new ArrayList<>(); for (int i = 0; i < D; i++) syms1.add(i); // syms2: same symbols in reverse order (worst case: match at end of each scan) List syms2 = new ArrayList<>(); for (int i = D - 1; i >= 0; i--) syms2.add(i); // syms1.exists(syms2.contains) — for each sym in syms1, do O(D) scan of syms2 outer: for (Integer sym : syms1) { for (int j = 0; j < syms2.size(); j++) { ops++; if (syms2.get(j).equals(sym)) { // found — but exists() keeps going (checking all of syms1) // Actually exists() short-circuits on first true match. // Here intersection is NON-empty (all symbols match), // so first sym in syms1 will match syms2 at position D-1. break; // simulate the inner scan stopping at match } } } } return ops; } /** * Simulates: val s2 = syms2.toSet; !syms1.exists(s2.contains) * Build s2 once per invocation (O(D)), then O(1) per lookup. */ static long fast() { long ops = 0; for (int pair = 0; pair < M; pair++) { List syms1 = new ArrayList<>(); for (int i = 0; i < D; i++) syms1.add(i); List syms2 = new ArrayList<>(); for (int i = D - 1; i >= 0; i--) syms2.add(i); // Build set — O(D) cost Set s2 = new HashSet<>(syms2); ops += D; // count build cost // O(1) per lookup for (Integer sym : syms1) { ops++; if (s2.contains(sym)) break; // same short-circuit as exists() } } return ops; } public static void main(String[] args) { long sOps = slow(); long fOps = fast(); long ratio = fOps > 0 ? sOps / fOps : sOps; System.out.println("slow ops: " + sOps); System.out.println("fast ops: " + fOps); System.out.println("ratio: " + sOps + "/" + fOps + " = " + ratio + "x"); if (ratio < N) { System.out.println("1/1 FAIL — expected slowOps >= " + N + "x fastOps, got ratio=" + ratio); System.exit(1); } System.out.println("1/1 PASS"); } }