B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections. Squash of 94 local commits onto remote master.
2.7 KiB
| id | repo | severity | status | created |
|---|---|---|---|---|
| composer-0002 | composer/composer | MEDIUM | PATCHED | 2026-03-23 |
Defect
File: src/Composer/Repository/InstalledRepository.php:128,154,167,180
Pattern: in_array($link->getTarget(), $packagesInTree) / in_array($link->getSource(), $packagesInTree) inside nested foreach loops
Complexity: O(|packages| × |links| × |packagesInTree|) per getDependents() call
Language: PHP
Description
InstalledRepository::getDependents() traverses the installed package dependency
graph to find all packages that (directly or transitively) depend on a given package.
It maintains $packagesInTree — an array that accumulates visited package names —
and checks membership with in_array() (O(n) linear scan) before recursing.
The traversal has the following structure:
foreach ($this->getPackages() as $package) { // O(N) packages
foreach ($links as $link) { // O(L) links per package
foreach ($needles as $needle) { // O(K) needles
if (in_array($source, $packagesInTree)) { // O(|tree|) — O(N) worst case
In the worst case (deep dependency graph), this is O(N² × L × K) where N is installed package count, L is average links per package, K is the needle count.
$packagesInTree grows as packages are added: $packagesInTree[] = $link->getSource().
By the time N packages have been visited, each in_array call scans up to N entries.
This fires on composer show --tree, composer why, composer depends --tree, and
the BSP server's dependency tree protocol response. For typical medium-size projects
(100-200 packages), this produces 10,000–40,000 string comparisons per invocation.
Fix
Replace: in_array($pkg, $packagesInTree) array scan
With: $packagesInTreeSet[$pkg] = true associative array; check isset($packagesInTreeSet[$pkg])
All four call sites (lines 128, 154, 167, 180) use the same $packagesInTree variable
and can be fixed together in a single patch. The fix requires threading a $packagesInTreeSet
hash alongside the existing $packagesInTree array (or replacing the array entirely with
a keyed hash, using array_values() at the return boundary if caller expects a list).
Work required
- Patch in
defects/composer/patch/ - Unit test — asserts operation counts before/after at N=50,100,200 packages (in
defects/composer/unit/) - Integration test —
composer show --treeon project with N transitive deps (indefects/composer/integration/) - Benchmark — timing before/after (in
defects/composer/bench/) - White paper section —
whitepaper/vectors/tool-harness/composer.rst