package unit; import java.util.ArrayList; import java.util.HashSet; import java.util.List; /** * ScalaCheckableTest — CWE-407 unit test for scala-0001 * * Models the O(n²) nested scan in Checkable.propagateKnownTypes: * * from.baseClasses foreach { bc => if (to.baseClasses.contains(bc)) { ... } } * * Both 'from.baseClasses' and 'to.baseClasses' are List[Symbol]. * The outer foreach × inner contains = O(M×N) element comparisons. * * slow(): simulates the original — outer list.forEach × inner list.contains. * fast(): simulates the fix — pre-build Set from to.baseClasses, then O(1) lookup. * * We use SIZE=200 base classes (realistic for trait-heavy Scala frameworks). * We assert slow() uses >= 5x more comparisons than fast(). */ public class ScalaCheckableTest { static final int SIZE = 200; // number of base classes (e.g. deep trait hierarchy) static final int N = 5; // minimum speedup factor required /** * Simulates a Symbol as an integer identifier. * In Scala, Symbol equality is reference identity (object identity). * Here we use Integer.equals for equivalence; both slow and fast use * the same equality semantics. */ /** * slow(): mirrors: from.baseClasses foreach { bc => if (to.baseClasses.contains(bc)) } * All elements of from.baseClasses are also in to.baseClasses (worst case — * every contains() call walks the full list before finding the element at end). * Returns total element comparisons. */ static long slow() { // from.baseClasses: SIZE symbols, ids 0..SIZE-1 List fromBases = new ArrayList<>(); for (int i = 0; i < SIZE; i++) fromBases.add(i); // to.baseClasses: same SIZE symbols, but in reverse order // so every contains() call scans to the end (worst case) List toBases = new ArrayList<>(); for (int i = SIZE - 1; i >= 0; i--) toBases.add(i); long ops = 0; for (Integer bc : fromBases) { // contains() on List — O(n) scan for (int j = 0; j < toBases.size(); j++) { ops++; if (toBases.get(j).equals(bc)) break; } } return ops; } /** * fast(): mirrors fix — val toBaseSet = to.baseClasses.toSet * then: from.baseClasses foreach { bc => if (toBaseSet.contains(bc)) } * One-time O(n) build + O(1) per lookup. */ static long fast() { List fromBases = new ArrayList<>(); for (int i = 0; i < SIZE; i++) fromBases.add(i); List toBases = new ArrayList<>(); for (int i = SIZE - 1; i >= 0; i--) toBases.add(i); // One-time set build — O(n) HashSet toBaseSet = new HashSet<>(toBases); long ops = toBases.size(); // count the build cost // O(1) per lookup for (Integer bc : fromBases) { ops++; // one hash probe per lookup toBaseSet.contains(bc); } return ops; } public static void main(String[] args) { long sOps = slow(); long fOps = fast(); System.out.println("slow ops: " + sOps); System.out.println("fast ops: " + fOps); System.out.println("ratio: " + sOps + "/" + fOps + " = " + (sOps / fOps) + "x"); if (sOps < fOps * N) { System.out.println("1/1 FAIL — expected slowOps >= " + N + "x fastOps, got ratio=" + (sOps / fOps)); System.exit(1); } System.out.println("1/1 PASS"); } }