Skip to content

Read the native narrowing base from the scope when only the PHPDoc type is tracked - #6752

Open
HawkForge wants to merge 2 commits into
phpstan:2.3.xfrom
HawkForge:fix/phpdoc-only-tracked-expr-native-narrowing
Open

HawkForge wants to merge 2 commits into
phpstan:2.3.xfrom
HawkForge:fix/phpdoc-only-tracked-expr-native-narrowing

Conversation

@HawkForge

Copy link
Copy Markdown
Contributor

Fixes phpstan/phpstan#15404. Thanks @zajca for the report and the reproducer.

MutatingScope::applySpecifiedTypes() reads the base type of a narrowing from the tracked expression holders. An expression can be tracked with its PHPDoc type and have no native holder. In the issue, $has = isset($s['projects']); if (!$has) return; applies the conditional holder of $has, which tracks $s['projects'] as list<string> in the PHPDoc types only. The native type is still read through $s (array), so it is mixed.

For a sure-not narrowing, the native result then fell back to the PHPDoc result when no native holder was tracked. foreach narrows its iteratee with $s['projects'] !== [], so the native type became non-empty-list<string> and $p was natively string. With treatPhpDocTypesAsCertain: false, is_string($p) was reported as always true. The inline if (!isset($s['projects'])) form tracks $s['projects'] with a native holder too (mixed~null), so it was not affected. 2.2.17 read the native base with getNativeType() in removeTypeFromExpression().

When only the PHPDoc type is tracked, the native base is now read from the scope with getCurrentTypesOfSpecifiedExpr(), the same way untracked expressions already get both types. Inside the loop $s['projects'] is natively mixed~array{} and $p is mixed, as on 2.2.17. The PHPDoc types do not change.

MutatingScope is shadowed by turbo-ext, so MutatingScope.cpp gets the same change. A separate commit bumps the expected turbo version.

ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest::testBug15404 analyses the issue's code and expects no errors. Without the fix it fails with Call to function is_string() with string will always evaluate to true. on line 13. The full test suite passes with and without the turbo extension loaded, smoke.php and signature-parity.php pass, and phpcs is clean on the changed files.

Closes phpstan/phpstan#15404

…pe is tracked

MutatingScope::applySpecifiedTypes() takes the base type of a narrowing
from the tracked expression holders. An expression can be tracked with
its PHPDoc type but not with a native one, for example `$s['projects']`
after `$has = isset($s['projects']); if (!$has) return;`, where the
conditional holder of `$has` only carries the PHPDoc type. The native
result of a sure-not narrowing then fell back to the PHPDoc result, so
`$s['projects'] !== []` (which foreach applies to its iteratee) made the
native type `non-empty-list<string>` and `$p` natively `string`. With
treatPhpDocTypesAsCertain: false, is_string($p) was then reported as
always true.

When only the PHPDoc type is tracked, the native base is now read from
the scope, as it already is for untracked expressions. 2.2.17 read it
through getNativeType() in removeTypeFromExpression().

Closes phpstan/phpstan#15404
@staabm

staabm commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

please stop flooding the repo with more pull requests.

you should learn from feedback on your already opened ones and open new ones after some of them landed.

@HawkForge

Copy link
Copy Markdown
Contributor Author

Understood, sorry about the volume. I won't open any more PRs here until the open ones have been reviewed, and I'll work through the feedback on them first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants