Repository navigation
Do not use PHPDoc types as native types in foreach shape unrolling, list destructuring and by-ref writeback - #6463
Merged
ondrejmirtes merged 1 commit intoSep 17, 2026
Conversation
… list destructuring and by-ref writeback - ForeachHandler::tryProcessUnrolledConstantArrayForeach() fell back to the PHPDoc key/value type of the unrolled array shape whenever the native iteratee type had no matching constant array. The fallback now comes from the native iteratee's iterable key/value type. - AssignHandler's list-destructuring write built the per-item assigned expr from a `TypeExpr` holding the PHPDoc offset value type, so `[$a, $b] = $x` and `foreach ($rows as [$a, $b])` wrote PHPDoc types into the native types of the destructured variables. It now builds a `NativeTypeExpr` with the native offset value type read off the native receiver and dim types. - The by-ref argument writeback in NodeScopeResolver assigned the `@param` / `@param-out` / parameter-out-extension type as the native type too. The native side now uses the parameter's own native type, which is all PHP guarantees about what the call writes back. - MutatingScope::resolveIntertwinedAssignedType() read PHPDoc dim types while resolving the native type of a by-ref slot; it now reads native dim types in the native pass (consistency hardening, no reproducer found). The originally reported path (the key-type rewrite of the iterated array after the loop) already reads native types on this branch; the regression test keeps it covered.
ondrejmirtes
pushed a commit
to SanderMuller/phpstan-src
that referenced
this pull request
Sep 24, 2026
… fits it After a by-reference argument, the native type of the variable became the parameter's own type: its declaration, or for a builtin its signature map entry. PHP checks a declaration only on the way in and a signature map entry not at all, so neither describes what the call writes back when the two disagree. preg_match() with PREG_OFFSET_CAPTURE writes arrays into a slot the signature map declares as string[]. The guard `if (! preg_match(...))` then intersected the offset-capture shape with array<string>, the native type became never, and with treatPhpDocTypesAsCertain: false isset($m[2]) reported "Offset 2 on *NEVER* in isset() always exists and is not nullable". The same happens to a userland @param-out that contradicts its declaration. The native type now falls back to mixed when the written-back type is not a subtype of the declaration. Where they agree, as in phpstan#6463's own cases, nothing changes. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
reviewtypo3org
pushed a commit
to TYPO3/typo3
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96034 Tested-by: core-ci <typo3@b13.com>
reviewtypo3org
pushed a commit
to TYPO3/typo3
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96033 Reviewed-by: Oliver Klee <typo3-coding@oliverklee.de> Tested-by: Oliver Klee <typo3-coding@oliverklee.de> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Sascha Nowak <typo3@saschanowak.me> Tested-by: core-ci <typo3@b13.com>
reviewtypo3org
pushed a commit
to TYPO3/typo3
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96035 Tested-by: core-ci <typo3@b13.com>
TYPO3IncTeam
pushed a commit
to TYPO3-CMS/core
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96033 Reviewed-by: Oliver Klee <typo3-coding@oliverklee.de> Tested-by: Oliver Klee <typo3-coding@oliverklee.de> Tested-by: Benni Mack <benni@typo3.org> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Sascha Nowak <typo3@saschanowak.me> Tested-by: core-ci <typo3@b13.com>
TYPO3IncTeam
pushed a commit
to TYPO3-CMS/core
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96034 Tested-by: core-ci <typo3@b13.com>
TYPO3IncTeam
pushed a commit
to TYPO3-CMS/core
that referenced
this pull request
Sep 24, 2026
Updates the PHPStan development dependency from `^2.2.14` to `^2.2.15`, keeping all maintained branches on the same version. PHPStan v2.2.15 knows that `vsprintf()` only throws a `ValueError` [1] and reports the `ArgumentCountError` catch in `LogDataTrait` as dead catch on all branches, which is valid and therefore removed. On main and v14.3, which disable `treatPhpDocTypesAsCertain` globally, it further reports two false positives caused by a regression of the native type of by-reference arguments [2][3], which is fixed upstream [4] but not released yet: * The null coalescing on the `scope` match in `CorrelationId::fromString()` is redundant anyway, because `PREG_UNMATCHED_AS_NULL` always provides the offset. It is removed on all branches. * The `is_array()` check after the workspace overlay in `SuggestWizardDefaultReceiver` is required, because `BackendUtility::workspaceOL()` can set the record to false. It is added to the PHPStan baseline of main and v14.3 until the upstream fix is released, which will then report the unmatched baseline entry for removal. [1] phpstan/phpstan-src#6517 [2] phpstan/phpstan-src#6463 [3] phpstan/phpstan#15297 [4] phpstan/phpstan-src#6564 Executed commands: > Build/Scripts/runTests.sh -s composer -- \ require --dev phpstan/phpstan:^2.2.15 > Build/Scripts/runTests.sh -s phpstanGenerateBaseline Resolves: #110772 Releases: main, 14.3, 13.4 Signed-off-by: Stefan Bürk <stefan@buerk.tech> Change-Id: Id0314ca79afa609489d4b2bfae7e64a9d9f7ea4a Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/96035 Tested-by: core-ci <typo3@b13.com>
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.
Summary
With
treatPhpDocTypesAsCertain: false, PHPStan reportedfunction.impossibleType/identical.alwaysFalseerrors that only follow from PHPDoc knowledge, because severalanalyser code paths wrote PHPDoc-derived types into the native type slot of the scope.
The native type is supposed to describe only what PHP itself guarantees, so it must never
be computed from a PHPDoc type.
This fixes every instance of that pattern I could reproduce around
foreach,destructuring and by-ref writeback.
Changes
src/Analyser/StmtHandler/ForeachHandler.php— when aforeachover a constant arrayshape is unrolled, the per-iteration native key/value types fell back to the PHPDoc
key/value type whenever the native iteratee type had no matching constant array (the
usual case:
@param array{a: int, b: string}on a plainarrayparameter). They nowfall back to the native iteratee's iterable key/value type.
src/Analyser/ExprHandler/AssignHandler.php— the list-destructuring write(
KIND_LIST, used by[$a, $b] = $x,list(...) = $xandforeach ($rows as [$a, $b]))built the per-item assigned expression as a
TypeExprcarrying the PHPDoc offset valuetype, which is then used for both type tables. It now builds a
NativeTypeExprwhosenative side is read off the native receiver and native dim types.
src/Analyser/NodeScopeResolver.php— the by-ref argument writeback assigned the@param/@param-out/ parameter-out-extension type as the native type as well. Thenative side now uses
ExtendedParameterReflection::getNativeType(), i.e. theparameter's own type declaration, which is all PHP guarantees about the written-back
value.
src/Analyser/MutatingScope.php—resolveIntertwinedAssignedType()read PHPDoc dimtypes while resolving the native type of a by-ref slot (foreach-by-ref / by-ref args).
It now reads native dim types in the native pass. This one is a consistency hardening:
I could not construct an input where it is observable, and it is noted here so a
reviewer can drop it if unwanted.
Probed and found already correct (no change needed): the key/value-type rewrite of the
iterated array after the loop (the path the issue points at — on this branch both
$keyLoopNativeTypesand$arrayDimFetchLoopNativeTypesare read throughgetNativeType()), the conditional-expression holders added for constant-arrayforeach(they are dropped when native types are promoted),enterForeach()/enterForeachKey(), thearray_keys()special case inForeachHandler::enterForeach(),the
array_pop/array_push/array_splice/sort/array_walkscope effects inFuncCallScopeEffectsHelper(all already useNativeTypeExpr),@varhandling inVarAnnotationProcessor(native ismixedthere), and the inc/dec virtual assigns.Root cause
The pattern is "PHPDoc type used as a native type". The analyser tracks two types per
expression and writes them together (
assignVariable()/assignExpression(), or aNativeTypeExprpair). Wherever a code path had only one type at hand it used the PHPDoctype for both slots, so PHPDoc-only knowledge became native knowledge. Once that happens,
treatPhpDocTypesAsCertain: falseno longer suppresses errors derived from it, and withtreatPhpDocTypesAsCertain: truethe "Because the type is coming from a PHPDoc…" tipdisappears because the native type already carries the information.
Affected locations (each fixed by reading the native counterpart instead):
foreachTest
tests/PHPStan/Analyser/nsrt/bug-15250.php—assertType()+assertNativeType()forthe reported snippet (the iterated array's native type stays
arrayand the keyvariable's native type stays
(int|string)), for the unrolled shape ((int|string)/mixednatively, both inside the loop and after it), for list destructuring both as aplain assignment and inside
foreach, and for by-ref@param/@param-outwriteback. Two companion cases where the shape is genuinely native
(
['a' => 1, 'b' => 'foo'],[1, 'foo']) assert that the precise native types arekept, so the fix does not simply widen everything.
tests/PHPStan/Rules/Comparison/data/bug-15250.phpwithImpossibleCheckTypeFunctionCallRuleTest::testBug15250()(treatPhpDocTypesAsCertain: false, expects no errors) andImpossibleCheckTypeFunctionCallRuleTest::testBug15250TreatPhpDocTypesAsCertain()(
treatPhpDocTypesAsCertain: true, expects all seven errors with the PHPDoc tip).Before the fix the first test reported six false positives and the second one lost the
tip on six of the seven errors.
Fixes phpstan/phpstan#15250