Repository navigation
Walk each isset() subject narrowed by the subjects before it - #7062
Merged
Merged
Conversation
ondrejmirtes
force-pushed
the
isset-narrow-each-subject-by-previous
branch
from
November 14, 2026 09:28
967564f to
33f97a9
Compare
isset() evaluates its arguments left to right and stops at the first one that is not set, so isset($key, $map[$key]) never reads $map[$key] with $key = null. IssetHandler walked every subject on the same scope, so rules saw the offset of the second subject as possibly null - with PHP 8.5 in the analysed version range, "Possibly invalid array key type string|null". Each subject after the first is now walked on the scope narrowed by the previous subject being set, like isset($a) && isset($b). The walk may stop after any subject, so the resulting scope merges the scopes after each subject. The turbo-ext mirror gets the same change. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d722-733e-76bf-aded-6de7e1ddd980
ondrejmirtes
force-pushed
the
isset-narrow-each-subject-by-previous
branch
from
November 14, 2026 09:40
33f97a9 to
0c8b6b9
Compare
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.
Closes phpstan/phpstan#15729
isset()evaluates its arguments left to right and stops at the first one that isn't set, soisset($key, $map[$key])never reads$map[$key]with$key = null.IssetHandlerwalked every subject on the same scope, soInvalidKeyInArrayDimFetchRulesaw the second subject's offset asstring|nulland reported "Possibly invalid array key type string|null" whenever PHP 8.5 is in the analysed version range. Since 8cf6b64 that includes any project whose range reaches 8.5.Each subject after the first is now walked on the scope narrowed by the previous subject being set (
DefaultNarrowingHelper::createIssetTruthyChainTypes(), the same truthy narrowing the handler already composes), likeisset($a) && isset($b). Each subject's ensured non-nullability is reverted right after its own walk, and its chain results are captured then. TheoffsetExists()throw-point block is unchanged. The walk may stop after any subject, so the resulting scope merges the scope after each subject. The truthy/falsey narrowing of the wholeisset()is unchanged.The turbo-ext mirror (
turbo-ext/src/IssetHandler.cpp) gets the same change.Verification:
InvalidKeyInArrayDimFetchRuleConfigPhpTest(PHP version range 7.4–8.5). It fails without the fix (lines 8 and 20 reported) and passes with it.isset($map[$key], $key)still reports, since there the key is checked only after the offset.make tests, with and without the extension loaded. The regression test also passes with only the native port applied.turbo-ext/tests/walk-trace.php --shards=8gives identical traces, andsmoke.phpandsignature-parity.phppass. The strict-Werrorbuild is clean.make phpstanand phpcs are clean.Benchmark:
tests/bench/data/bug-15061.php(a 26-subjectisset($entity['a'], ..., $entity['z'])) goes from 3.166B to 3.920B instructions (+23.8%), and its numbers are accepted from this PR'stally-results. Each subject is now narrowed by the one before it, so$entity's native type (mixed) is intersected with one morehasOffset()per subject, andTypeCombinator::intersect()re-normalizes the growing(non-empty-array&hasOffset(...)...)|(ArrayAccess&hasOffset(...)...)each time. The equivalentisset($entity['a']) && ... && isset($entity['z'])chain does exactly the same narrowing. Measured locally on 2.3.x it costs about what the newisset()costs (within ~3% under a sampling profiler), so this is the price of narrowing like&&and not something specific to IssetHandler.