java-topology/tools/tickets/defects/composer-0001.md
russell@unturf.com db29a08762 undefect. CWE-407 — 92 sites, 42 ecosystems
B&W print-friendly diagrams + tinkerpop-0001 + wave-3 proof sections.
Squash of 94 local commits onto remote master.
2026-03-26 19:48:18 -04:00

2.4 KiB

id repo severity status created
composer-0001 composer/composer MEDIUM PATCHED 2026-03-23

Defect

File: src/Composer/Repository/RepositoryUtils.php:46 Pattern: !in_array($candidate, $bucket, true) inside foreach ($packages as $candidate) Complexity: O(N²) per call + O(N³) with recursive transitive closure Language: PHP

Description

filterRequiredPackages() computes the set of packages from $packages that are transitively required by $requirer. It does this by iterating over all candidates in an outer foreach ($packages as $candidate) loop and checking !in_array($candidate, $bucket, true) to avoid re-adding packages already in the result set.

$bucket is a PHP array (not a Set or hash map). in_array() on a PHP array is O(n) linear scan. As packages are found they are appended to $bucket with $bucket[], so $bucket grows from 0 to N. The total cost of all membership checks in a single call is O(N²) in the number of installed packages.

The function is also called recursively for each transitive dependency: $bucket = self::filterRequiredPackages($packages, $candidate, false, $bucket). In the worst case (long dependency chain), the total complexity is O(N³) where N is the package count.

Triggered by: composer show, composer why, composer depends, and any Composer command that computes transitive package sets for display. Not in the SAT/CDCL installation solver.

For a project with 200 packages (typical Symfony or Laravel application): O(200²) = 40,000 membership comparisons per composer why invocation.

Fix

Replace: $bucket as PHP array with membership via in_array() With: $bucket as PHP associative array (hash map) keyed on package name/identity; or use SplObjectStorage for object-identity lookup in O(1).

// Before
if (!in_array($candidate, $bucket, true)) {
    $bucket[] = $candidate;
// After
if (!isset($bucketMap[spl_object_id($candidate)])) {
    $bucketMap[spl_object_id($candidate)] = true;
    $bucket[] = $candidate;

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 why on project with N transitive deps (in defects/composer/integration/)
  • Benchmark — timing before/after (in defects/composer/bench/)
  • White paper section — whitepaper/vectors/tool-harness/composer.rst