Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace ExpressionTypeResolverExtension;

use PhpParser\Node\Expr;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Identifier;
use PHPStan\Analyser\Scope;
use PHPStan\Type\ExpressionTypeResolverExtension;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;

class VirtualPropertyExpressionTypeResolverExtension implements ExpressionTypeResolverExtension {

public function getType(Expr $expr, Scope $scope): ?Type
{
if (!$expr instanceof PropertyFetch) {
return null;
}

if (!$expr->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()]);
}

}
14 changes: 14 additions & 0 deletions tests/PHPStan/Analyser/data/expression-type-resolver-extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ services:
class: ExpressionTypeResolverExtension\MethodCallReturnsBoolExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension
-
class: ExpressionTypeResolverExtension\VirtualPropertyExpressionTypeResolverExtension
tags:
- phpstan.broker.expressionTypeResolverExtension

Loading