2 follow-up flagships: symfony-0001 + pyright-0001 (Wave 17 backlog cleared)
Both were flagged in Wave 17 survey as borderline real defects deferred for follow-up because the fix needed careful design beyond a single-line set hoist. Both shipped now with full bench + ticket + intel. UNDF-1310 symfony-0001 (HIGH) - PropertyAccessor::writeCollection. Doctrine entity collection diff: in_array($item, $collection, true) per item in $previousValue, then in_array($item, $previousValue, true) per item in $collection. O(P*C). Fix: dual lookup (SplObjectStorage for objects + serialize-keyed array for scalars, in_array fallback for resources). Bench: 5.2x at P=C=100, 88x at P=C=2000. UNDF-1311 pyright-0001 (HIGH) - CallHierarchyProvider outgoing/incoming call dedup. _outgoingCalls.find / _incomingCalls.find with composite key (uri, range) walks the list per call expression. O(C^2). Fix: parallel Map<string, entry> keyed by composite serialized form (uri|start.line|start.char|end.line|end.char). Bench: 2.7x at C=100, 22x at C=2000. Total session flagships: 11 (was 9) — 7 CWE-407 + 3 MOAD-0003 + 1 MOAD-0004. Wave 17 borderline backlog now empty.
This commit is contained in:
parent
2eea7d128c
commit
24052ec668
11 changed files with 758 additions and 0 deletions
|
|
@ -0,0 +1,92 @@
|
|||
# UNDF: UNDF-2026-000001310
|
||||
# CWE-407: Algorithmic Complexity — O(P*C) -> O(P+C) in
|
||||
# PropertyAccessor::writeCollection
|
||||
#
|
||||
# Defect: src/Symfony/Component/PropertyAccess/PropertyAccessor.php:580-595
|
||||
# The collection diff calls in_array($item, $collection, true) per item
|
||||
# in $previousValue, then in_array($item, $previousValue, true) per item
|
||||
# in $collection. PHP's in_array() with strict mode is O(N) per call.
|
||||
# Per write: O(P*C) where P = previous-collection size, C = new-collection
|
||||
# size. For Symfony Doctrine entities with deep OneToMany associations,
|
||||
# N=100..2000 is realistic.
|
||||
#
|
||||
# Fix: build dual lookup once before the diff:
|
||||
# - SplObjectStorage for object items (O(1) identity)
|
||||
# - Associative array indexed by serialize($item) for scalar/array items
|
||||
# (O(1) hashed lookup; serialize() canonicalizes equals)
|
||||
# Both pointer-equal and value-equal cases handled. Falls back to in_array
|
||||
# for resource items (rare; can't be hashed).
|
||||
#
|
||||
# Complexity gate (defects/symfony/bench/bench-symfony-0001.py):
|
||||
# P=2000 C=2000: defective ~180ms, fixed <5ms (>=30x speedup)
|
||||
# k-scaling 5x: time ratio must be <17.5x (O(N) ~5x not O(N^2) ~25x)
|
||||
--- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
|
||||
+++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
|
||||
@@ -575,18 +575,33 @@ class PropertyAccessor implements PropertyAccessorInterface
|
||||
if ($previousValue && \is_array($previousValue)) {
|
||||
if (\is_object($collection)) {
|
||||
$collection = iterator_to_array($collection);
|
||||
}
|
||||
+ // Build dual lookup for $collection: O(1) membership during the
|
||||
+ // remove pass (was O(C) in_array per previous item -> O(P*C) total).
|
||||
+ [$collObj, $collScalar, $collFallback] = self::buildLookup($collection);
|
||||
foreach ($previousValue as $key => $item) {
|
||||
- if (!\in_array($item, $collection, true)) {
|
||||
+ if (!self::lookupContains($collObj, $collScalar, $collFallback, $item, $collection)) {
|
||||
unset($previousValue[$key]);
|
||||
$zval[self::VALUE]->$removeMethodName($item);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$previousValue = false;
|
||||
}
|
||||
|
||||
+ // Build dual lookup for $previousValue (now an array after the remove
|
||||
+ // pass) so the add pass is O(C) instead of O(P*C).
|
||||
+ if ($previousValue && \is_array($previousValue)) {
|
||||
+ [$prevObj, $prevScalar, $prevFallback] = self::buildLookup($previousValue);
|
||||
+ } else {
|
||||
+ $prevObj = $prevScalar = $prevFallback = null;
|
||||
+ }
|
||||
foreach ($collection as $item) {
|
||||
- if (!$previousValue || !\in_array($item, $previousValue, true)) {
|
||||
+ if (!$previousValue || !self::lookupContains($prevObj, $prevScalar, $prevFallback, $item, $previousValue)) {
|
||||
$zval[self::VALUE]->$addMethodName($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
+
|
||||
+ /**
|
||||
+ * Builds dual lookup (SplObjectStorage + serialize-keyed assoc array)
|
||||
+ * over $items for O(1) strict-equality membership checks against scalars
|
||||
+ * and objects. Resources / unhashable items go to a fallback list that
|
||||
+ * triggers slow-path in_array() lookup.
|
||||
+ */
|
||||
+ private static function buildLookup(array $items): array
|
||||
+ {
|
||||
+ $obj = new \SplObjectStorage();
|
||||
+ $scalar = [];
|
||||
+ $fallback = [];
|
||||
+ foreach ($items as $item) {
|
||||
+ if (\is_object($item)) {
|
||||
+ $obj->attach($item);
|
||||
+ } elseif (\is_resource($item)) {
|
||||
+ $fallback[] = $item;
|
||||
+ } else {
|
||||
+ $scalar[serialize($item)] = true;
|
||||
+ }
|
||||
+ }
|
||||
+ return [$obj, $scalar, $fallback];
|
||||
+ }
|
||||
+
|
||||
+ private static function lookupContains(\SplObjectStorage $obj, array $scalar, array $fallback, $needle, array $allItems): bool
|
||||
+ {
|
||||
+ if (\is_object($needle)) {
|
||||
+ return $obj->contains($needle);
|
||||
+ }
|
||||
+ if (\is_resource($needle)) {
|
||||
+ return \in_array($needle, $allItems, true); // rare; can't hash
|
||||
+ }
|
||||
+ return isset($scalar[serialize($needle)]);
|
||||
+ }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue