diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index b31ccecf985..3e897ecc83d 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -3584,6 +3584,18 @@ private function resolveScopeStateType(Expr $expr, bool $native): Type return $scope->hasVariableType($expr->name)->no() ? new ErrorType() : $scope->getVariableType($expr->name); } + // an untracked expression is priced the way resolveType() prices it - + // an ExpressionTypeResolverExtension answers before the reflection-derived + // reads below, which know nothing of what the extension resolves (a virtual + // property would otherwise be narrowed starting from ErrorType) + $askScope = $native ? $this->promoteNativeTypes() : $this; + foreach ($this->expressionTypeResolverExtensions->getAll() as $extension) { + $type = $extension->getType($expr, $askScope); + if ($type !== null) { + return TypeUtils::resolveLateResolvableTypes($type); + } + } + if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) { $varStateType = $this->resolveScopeStateType($expr->var, $native); if ($varStateType instanceof NeverType) { diff --git a/tests/PHPStan/Analyser/data/VirtualPropertyExpressionTypeResolverExtension.php b/tests/PHPStan/Analyser/data/VirtualPropertyExpressionTypeResolverExtension.php new file mode 100644 index 00000000000..d39445721e1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/VirtualPropertyExpressionTypeResolverExtension.php @@ -0,0 +1,41 @@ +name instanceof Identifier || $expr->name->name !== 'virtualProperty') { + return null; + } + + // a narrowed type must win + if ($scope->hasExpressionType($expr)->yes()) { + return null; + } + + $classType = new ObjectType('ExpressionTypeResolverExtensionTest\ClassWithVirtualProperty'); + if (!$classType->isSuperTypeOf($scope->getType($expr->var))->yes()) { + return null; + } + + return new UnionType([new StringType(), new NullType()]); + } + +} diff --git a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php index 83c3ebab370..8b8929fd719 100644 --- a/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php +++ b/tests/PHPStan/Analyser/data/expression-type-resolver-extension.php @@ -27,3 +27,17 @@ public function methodReturningBoolNoMatterTheCallerUnlessReturnsString(): strin $closure = static function () use ($assigned): void { assertType('bool', $assigned); }; + +// the extension answers a property PHPStan itself does not know - narrowing it must start from that answer +class ClassWithVirtualProperty {} + +function (ClassWithVirtualProperty $o): void { + assertType('string|null', $o->virtualProperty); + if ($o->virtualProperty !== null) { + assertType('string', $o->virtualProperty); + } + if ($o->virtualProperty === null) { + return; + } + assertType('string', $o->virtualProperty); +}; diff --git a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon index de0f92640b6..39debf95ce1 100644 --- a/tests/PHPStan/Analyser/expression-type-resolver-extension.neon +++ b/tests/PHPStan/Analyser/expression-type-resolver-extension.neon @@ -4,4 +4,8 @@ services: class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension tags: - phpstan.broker.expressionTypeResolverExtension + - + class: ExpressionTypeResolverExtension\VirtualPropertyExpressionTypeResolverExtension + tags: + - phpstan.broker.expressionTypeResolverExtension