diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b31ccecf985..51d0915650d 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -4238,6 +4238,11 @@ public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self // a combinatorially-grown offset union doubles it with every isset()- // style check and gets skipped (overwrites assign, they never narrow) continue; + } elseif ($trackedNativeType === null) { + // tracked with its PHPDoc type only (e.g. by a conditional holder) - + // the native base is still read from the scope, not taken from + // the PHPDoc type + $trackedNativeType = $scope->getCurrentTypesOfSpecifiedExpr($expr)[1] ?? null; } if (isset($typeSpecification['terms'])) { diff --git a/src/Turbo/TurboExtensionEnabler.php b/src/Turbo/TurboExtensionEnabler.php index 374d29043e6..b868c5d001c 100644 --- a/src/Turbo/TurboExtensionEnabler.php +++ b/src/Turbo/TurboExtensionEnabler.php @@ -33,7 +33,7 @@ final class TurboExtensionEnabler { - public const EXPECTED_EXTENSION_VERSION = 'a0f7086'; + public const EXPECTED_EXTENSION_VERSION = 'f13ee08'; private static bool $active = false; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php index 635d4eff976..3f0606e1e1f 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php @@ -272,4 +272,9 @@ public function testBug15250(): void $this->analyse([__DIR__ . '/data/bug-15250.php'], []); } + public function testBug15404(): void + { + $this->analyse([__DIR__ . '/data/bug-15404.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-15404.php b/tests/PHPStan/Rules/Comparison/data/bug-15404.php new file mode 100644 index 00000000000..490ad1d2896 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-15404.php @@ -0,0 +1,17 @@ +} $s */ +function a(array $s): void +{ + $has = isset($s['projects']); + if (!$has) { + return; + } + foreach ($s['projects'] as $p) { + if (!is_string($p)) { + throw new \InvalidArgumentException(); + } + } +} diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 66e26133896..df008a07b10 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -7999,6 +7999,30 @@ class MutatingScope skipSpecification = true; } } + if (!skipSpecification && !hasTrackedNative) { + /* tracked with its PHPDoc type only (e.g. by a conditional + * holder) - the native base is still read from the scope, + * not taken from the PHPDoc type */ + zval exprZv; + ZVAL_OBJ(&exprZv, expr); + zv::Val currentTypes = otherPrivate(scopeObject, PT_LC("getcurrenttypesofspecifiedexpr"), 1, &exprZv, [&](MutatingScope &other) { + return other.getCurrentTypesOfSpecifiedExpr(expr); + }); + if (UNEXPECTED(currentTypes.isUndef())) return zv::Val(); + if (Z_TYPE_P(currentTypes.raw()) != IS_NULL) { + if (UNEXPECTED(Z_TYPE_P(currentTypes.raw()) != IS_ARRAY)) { + zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape"); + return zv::Val(); + } + zval *native = zend_hash_index_find(Z_ARRVAL_P(currentTypes.raw()), 1); + if (UNEXPECTED(native == NULL)) { + zend_throw_error(NULL, "phpstan_turbo: getCurrentTypesOfSpecifiedExpr() answered an unexpected shape"); + return zv::Val(); + } + trackedNativeType = zv::Val::copyOf(zv::Ref(native)); + hasTrackedNative = true; + } + } } if (skipSpecification) continue;