java-topology/defects/composer/patch/composer-0003-installed-repo-needles-inarray.md
russell@unturf.com ac82cff865 php-cluster: doctrine-orm-0001 + composer-0003 CWE-407 defects; count 668→670
doctrine-orm-0001: ClassMetadata::addSubClass in_array O(S²) subclasses list HIGH
  - src/Mapping/ClassMetadata.php addSubClass() scans plain $subClasses list
  - addSubClasses($parent->subClasses) called in doLoadMetadata: O(S²) per class
  - Fix: parallel $subClassesSet hash for O(1) membership; 500x at S=1000

composer-0003: InstalledRepository::getDependents in_array O(P²) needles list MEDIUM
  - needles array grows during foreach($packages) loop, in_array scan 3 sites
  - $packagesFoundSet already exists for cycle-detection but needles is separate
  - Fix: add $needlesSet = array_fill_keys($needles, true), mirror growth; 1000x at P=1000

cayley: CLEAN (confirmed -- map-based seen/pathMap throughout)
tinkerpop: CLEAN (confirmed -- existing CLEAN.md valid, sort ArrayList is query-plan-time only)
2026-03-29 19:51:31 -04:00

2.7 KiB
Raw Blame History

UNDF: UNDF-2026-000000045

UNDF: (pending)

composer-0003: InstalledRepository::getDependents — O(P²) in_array on growing needles list

CWE-407 — Algorithmic Complexity

Field Value
ID composer-0003
Severity MEDIUM
Ecosystem composer
Package composer/composer
File src/Composer/Repository/InstalledRepository.php
Lines 177, 190, 201
Complexity O(P·N) → O(P²) where P = installed packages, N = needles (grows during loop)
Hot path composer why / composer why-not — invoked for every dependency resolution query

Defect

// BEFORE — O(P²): in_array($x, $needles) is O(N) inside foreach($this->getPackages()) O(P).
// $needles starts as the query needle set but GROWS during the loop (line 144):
//   $needles[] = $link->getTarget();
// So at iteration i, len($needles) can be up to i, making total cost O(P²).

// Note: $packagesFoundSet already provides O(1) cycle-detection, but $needles is a
// separate plain PHP array with no hash index.

foreach ($this->getPackages() as $package) {
    // ...
    if ($invert && in_array($package->getName(), $needles, true)) {    // O(N) scan, line 177
        // ...
    }
    foreach ($package->getConflicts() as $link) {
        if (in_array($link->getTarget(), $needles, true)) {            // O(N) scan, line 190
            // ...
        }
    }
    if ($invert && $constraint && in_array($package->getName(), $needles, true) ...) { // O(N) scan, line 201
        // ...
    }
}

Fix

// AFTER — O(1) lookup: build a parallel $needlesSet hash alongside $needles.
// Update $needlesSet whenever $needles grows.

$needles = array_map('strtolower', (array) $needle);
$needlesSet = array_fill_keys($needles, true);   // O(1) lookup companion

// ...
// Where needles grows (line 144), mirror the addition:
$needles[] = $link->getTarget();
$needlesSet[$link->getTarget()] = true;          // O(1) insert

// Replace all three in_array() calls:
if ($invert && isset($needlesSet[$package->getName()])) {          // O(1) — was line 177
if (isset($needlesSet[$link->getTarget()])) {                      // O(1) — was line 190
if ($invert && $constraint && isset($needlesSet[$package->getName()])) { // O(1) — was line 201

Speedup

P (packages) Before (ops) After (ops) Speedup
50 2,500 50 50×
100 10,000 100 100×
500 250,000 500 500×
1,000 1,000,000 1,000 1,000×

A large monorepo with 500 installed packages incurs 250,000 membership scans on composer why (invert mode) instead of 500. This command is run frequently during debugging and CI dependency audits.