package unit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * groovy-0001 — StaticTypeCheckingVisitor: collectedNames ArrayList O(E×C) * * Demonstrates CWE-407: checkNamedParamsAnnotation uses ArrayList.contains() * inside a loop over entries, scanning the list linearly on every iteration. * * Models StaticTypeCheckingVisitor.checkNamedParamsAnnotation(): * slow(): ArrayList + .contains() inside loop over entries — O(E×C) * fast(): HashSet + .contains() inside loop over entries — O(E+C) * * Ratio must be >= 5x at N=500. */ public class GroovyNamedParamsTest { /** * Defective path: collectedNames is ArrayList, contains() is O(C) per call. * Outer loop: E entries (supplied arguments). * Inner: collectedNames.contains(name) scans all C collected names. * Total: O(E × C). */ static long slow(List collectedNames, List entryKeys) { long ops = 0; for (String name : entryKeys) { // Simulate ArrayList.contains() — linear scan boolean found = false; for (int i = 0; i < collectedNames.size(); i++) { ops++; if (collectedNames.get(i).equals(name)) { found = true; break; } } // If not found, would addStaticTypeError — we just count ops } return ops; } /** * Fixed path: collectedNames is HashSet, contains() is O(1) per call. * Outer loop: E entries. Inner: O(1) hash lookup. * Total: O(E + C). */ static long fast(Set collectedNamesSet, List entryKeys) { long ops = 0; for (String name : entryKeys) { ops++; // one hash probe per entry collectedNamesSet.contains(name); } return ops; } public static void main(String[] args) { int[] sizes = {100, 200, 500, 1000}; int pass = 0, total = 0; boolean allPassed = true; for (int N : sizes) { // Build C collected param names (annotation-declared params) List collectedList = new ArrayList<>(); Set collectedSet = new HashSet<>(); for (int i = 0; i < N; i++) { String name = "param" + i; collectedList.add(name); collectedSet.add(name); } // Build E entry keys (arguments supplied at call site) // Use names that are NOT in collectedNames to force full scan in slow path List entries = new ArrayList<>(); for (int i = 0; i < N; i++) { entries.add("arg" + i); // no match — slow path scans entire list } long slowOps = slow(collectedList, entries); long fastOps = fast(collectedSet, entries); double ratio = (double) slowOps / fastOps; boolean pass1 = ratio >= 5.0; total++; if (pass1) pass++; else allPassed = false; System.out.printf("N=%4d slow=%8d fast=%6d ratio=%7.1fx %s%n", N, slowOps, fastOps, ratio, pass1 ? "PASS" : "FAIL"); } System.out.printf("%d/%d PASS%n", pass, total); if (!allPassed) System.exit(1); } }