Skip to content

Do not use PHPDoc types as native types in foreach shape unrolling, list destructuring and by-ref writeback - #6463

Merged
ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-nxwvhq4
Sep 17, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-nxwvhq4

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

With treatPhpDocTypesAsCertain: false, PHPStan reported function.impossibleType /
identical.alwaysFalse errors that only follow from PHPDoc knowledge, because several
analyser 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 a foreach over a constant array
    shape 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 plain array parameter). They now
    fall 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(...) = $x and foreach ($rows as [$a, $b]))
    built the per-item assigned expression as a TypeExpr carrying the PHPDoc offset value
    type, which is then used for both type tables. It now builds a NativeTypeExpr whose
    native 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. The
    native side now uses ExtendedParameterReflection::getNativeType(), i.e. the
    parameter's own type declaration, which is all PHP guarantees about the written-back
    value.
  • src/Analyser/MutatingScope.php — resolveIntertwinedAssignedType() read PHPDoc dim
    types 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
$keyLoopNativeTypes and $arrayDimFetchLoopNativeTypes are read through
getNativeType()), the conditional-expression holders added for constant-array
foreach (they are dropped when native types are promoted), enterForeach() /
enterForeachKey(), the array_keys() special case in ForeachHandler::enterForeach(),
the array_pop/array_push/array_splice/sort/array_walk scope effects in
FuncCallScopeEffectsHelper (all already use NativeTypeExpr), @var handling in
VarAnnotationProcessor (native is mixed there), 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 a
NativeTypeExpr pair). Wherever a code path had only one type at hand it used the PHPDoc
type for both slots, so PHPDoc-only knowledge became native knowledge. Once that happens,
treatPhpDocTypesAsCertain: false no longer suppresses errors derived from it, and with
treatPhpDocTypesAsCertain: true the "Because the type is coming from a PHPDoc…" tip
disappears because the native type already carries the information.

Affected locations (each fixed by reading the native counterpart instead):

  • the native key/value fallback of the unrolled constant-array foreach
  • the offset value type of a list-destructuring item
  • the by-ref writeback type of a function/method argument
  • the dim type used while resolving a by-ref slot's native type

Test

  • tests/PHPStan/Analyser/nsrt/bug-15250.php — assertType() + assertNativeType() for
    the reported snippet (the iterated array's native type stays array and the key
    variable's native type stays (int|string)), for the unrolled shape ((int|string) /
    mixed natively, both inside the loop and after it), for list destructuring both as a
    plain assignment and inside foreach, and for by-ref @param / @param-out
    writeback. Two companion cases where the shape is genuinely native
    (['a' => 1, 'b' => 'foo'], [1, 'foo']) assert that the precise native types are
    kept, so the fix does not simply widen everything.
  • tests/PHPStan/Rules/Comparison/data/bug-15250.php with
    ImpossibleCheckTypeFunctionCallRuleTest::testBug15250() (treatPhpDocTypesAsCertain: false, expects no errors) and
    ImpossibleCheckTypeFunctionCallRuleTest::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

… 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
ondrejmirtes merged commit 091f2cf into phpstan:2.2.x Sep 17, 2026
529 of 537 checks passed
@ondrejmirtes
ondrejmirtes deleted the create-pull-request/patch-nxwvhq4 branch September 17, 2026 09:36
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants