[6.x] Fix parallel scan crash on PHP-native intersection types#11841
Open
alies-dev wants to merge 1 commit into
Open
[6.x] Fix parallel scan crash on PHP-native intersection types#11841alies-dev wants to merge 1 commit into
alies-dev wants to merge 1 commit into
Conversation
Type::intersectUnionTypes was crashing during parallel scan when a worker encountered a PHP-native intersection type like `Map&ResultInterface&stdClass&Vector` and one of the named types was a final class with storage already populated in the worker, while another required-for-comparison class was not. The per-atomic-pair branches inside intersectAtomicTypes already handle this scenario by catching InvalidArgumentException with the comment "Ignore non-existing classes during initial scan". The post-foreach narrowing block running UnionTypeComparator::isContainedBy on the union types as a whole was missing the same guard, so it bubbled the throw out to the parallel worker and aborted the scan with messages like "Could not get class storage for stdclass". The narrowing block only runs when intersection_performed stayed false, which happens for pairs of TNamedObject where mayHaveIntersection returns false for at least one side (final class with storage available). When storage is missing for a class needed downstream by the comparator, the correct behavior is the same as for the per-atomic case: skip the narrowing optimization and let the un-narrowed type stand.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scanning a codebase that uses PHP-native intersection types in function signatures (e.g.
Map&ResultInterface&stdClass&Vectorfrom azjezz/psl) intermittently crashed parallel scan workers withCould not get class storage for stdclass(or various other class names) thrown fromClassLikeStorageProvider::get.The crash path is
FunctionLikeNodeScanner::start→getTranslatedFunctionParam→TypeHintResolver::resolve(intersection branch) →Type::intersectUnionTypes. InsideintersectUnionTypesthere is a per-atomic-pair narrowing pass and a final union-level narrowing pass. The per-atomic pass already wrapsUnionTypeComparator::isContainedByintry { ... } catch (InvalidArgumentException) { /* Ignore non-existing classes during initial scan */ }because this exact scenario was anticipated. The final union-level narrowing block at line 808 was missing the same guard, so it bubbled the throw out of the worker.That block only runs when
$intersection_performedis false after the cross-product loop, which happens for pairs ofTNamedObjectwheremayHaveIntersectionreturns false on at least one side (a final class with storage already populated). Whether a given parallel worker has the storage for the final class depends on the order in which it scanned files — Vector and Map in psl are bothfinal readonly class, so workers that processed those files beforeintersection.phphit the crash, and workers that didn't processed it via the defensivemayHaveIntersection=truepath. Hence the ~50% crash rate. Confirmed via debug logging: whenmhi1=0 mhi2=1(Vector storage present, ResultInterface storage absent) the unwrapped narrowing block ran and threw on the missing class.The fix wraps the union-level narrowing block in the same
try/catchalready used at lines 928 and 988 of the same function. When containment can't be determined because storage is missing, the narrowing optimization is skipped and the un-narrowed type is returned, which is exactly what the user-visible "lighter-weight intersection that doesn't need containment data" should look like.Verification:
--scan-threads=1: zero crashes, deterministic 3127 errors (matches single-thread baseline).classExtendsdirectly broke 8 of those tests becauseInternalCallMapHandlerTest::assertTypeValidityexplicitly catchesInvalidArgumentExceptionfrom that path as a "skip this assertion when storage isn't loaded" signal; that contract is preserved here.DocblockTypeContradiction/NullableReturnStatementissues in agetBlockTypefunction whose?intnative return contradicts its@return Tokens::BLOCK_TYPE_*docblock; psl goes from 3,473 to 3,474 because the test filepackages/type/tests/static-analysis/intersection.php(designed specifically to exerciseMap&ResultInterface&stdClass&Vector) now consistently flags that the docblock and native parsers produce different intersection representations for the same syntax. The +1 in psl is a side effect ofintersectUnionTypesreturning null when narrowing fails, whichTypeHintResolverthen converts toType::getNever(); the previous behavior was to either crash or non-deterministically hit the defensivemayHaveIntersection=truepath. A follow-up could refine the null-degradation path to preserve the cross-product, but that is a separate, pre-existing issue.Bench summary table (relevant column =
6.16.1-12-32347e114):