Skip to content
Closed
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
14 changes: 13 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -3274,7 +3274,11 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
{
$conditionalExpressions = $this->conditionalExpressions;
$conditionalExpressions[$exprString] = $conditionalExpressionHolders;
if (isset($conditionalExpressions[$exprString])) {
$conditionalExpressions[$exprString] = array_merge($conditionalExpressions[$exprString], $conditionalExpressionHolders);
} else {
$conditionalExpressions[$exprString] = $conditionalExpressionHolders;
}
return $this->scopeFactory->create(
$this->context,
$this->isDeclareStrictTypes(),
Expand All @@ -3295,6 +3299,14 @@ public function addConditionalExpressions(string $exprString, array $conditional
);
}

/**
* @return array<string, ConditionalExpressionHolder[]>
*/
public function getConditionalExpressions(): array
{
return $this->conditionalExpressions;
}

public function exitFirstLevelStatements(): self
{
if (!$this->inFirstLevelStatement) {
Expand Down
57 changes: 56 additions & 1 deletion src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,9 @@ public function specifyTypesInCondition(
) {
$types = $leftTypes->normalize($scope);
} else {
$types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope));
$leftNormalized = $this->resolveConditionalExpressions($scope, $leftTypes->normalize($scope));
$rightNormalized = $this->resolveConditionalExpressions($rightScope, $rightTypes->normalize($rightScope));
$types = $leftNormalized->intersectWith($rightNormalized);
}
} else {
$types = $leftTypes->unionWith($rightTypes);
Expand Down Expand Up @@ -2098,6 +2100,59 @@ private function processBooleanNotSureConditionalTypes(Scope $scope, SpecifiedTy
return [];
}

private function resolveConditionalExpressions(MutatingScope $scope, SpecifiedTypes $specifiedTypes): SpecifiedTypes
{
$sureTypes = $specifiedTypes->getSureTypes();
$specifiedExpressions = [];
foreach ($sureTypes as $exprString => [$expr, $type]) {
$specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $type);
}

if (count($specifiedExpressions) === 0) {
return $specifiedTypes;
}

$additionalSureTypes = [];
foreach ($scope->getConditionalExpressions() as $conditionalExprString => $conditionalExpressions) {
foreach ($conditionalExpressions as $conditionalExpression) {
foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) {
continue 2;
}
}

$holder = $conditionalExpression->getTypeHolder();
if (isset($additionalSureTypes[$conditionalExprString])) {
$additionalSureTypes[$conditionalExprString] = [
$holder->getExpr(),
TypeCombinator::intersect($additionalSureTypes[$conditionalExprString][1], $holder->getType()),
];
} else {
$additionalSureTypes[$conditionalExprString] = [$holder->getExpr(), $holder->getType()];
}
}
}

if (count($additionalSureTypes) === 0) {
return $specifiedTypes;
}

foreach ($additionalSureTypes as $additionalExprString => [$additionalExpr, $additionalType]) {
if (isset($sureTypes[$additionalExprString])) {
$sureTypes[$additionalExprString] = [$additionalExpr, TypeCombinator::union($sureTypes[$additionalExprString][1], $additionalType)];
} else {
$sureTypes[$additionalExprString] = [$additionalExpr, $additionalType];
}
}

$result = new SpecifiedTypes($sureTypes, $specifiedTypes->getSureNotTypes());
if ($specifiedTypes->shouldOverwrite()) {
$result = $result->setAlwaysOverwriteTypes();
}

return $result->setRootExpr($specifiedTypes->getRootExpr());
}

/**
* @return array{Expr, ConstantScalarType, Type}|null
*/
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/bug-7716.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public function sayHello(array $array): int
$hasBar = isset($array['bar']) && $array['bar'] > 1;

if ($hasFoo) {
assertType('array{foo?: int, bar?: int}', $array);
assertType('array{foo: int, bar?: int}', $array);
assertType('int<2, max>', $array['foo']);
return $array['foo'];
}
Expand Down Expand Up @@ -44,7 +44,7 @@ public function sayHello2(array $array): int
}

if ($hasBar) {
assertType('array{foo?: int, bar?: int}', $array);
assertType('array{foo?: int, bar: int}', $array);
assertType('int<2, max>', $array['bar']);
return $array['bar'];
}
Expand Down
87 changes: 87 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-9519.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace Bug9519;

use function PHPStan\Testing\assertType;

class ClassA {
public function sayHello(): void
{
echo 'Hello';
}
}

class ClassB {
public function sayHello(): void
{
echo 'Hello';
}
}

function test1(mixed $obj): void {
$isA = $obj instanceof ClassA;
$isB = $obj instanceof ClassB;

if ($isA || $isB) {
assertType('Bug9519\ClassA|Bug9519\ClassB', $obj);
}
}

function test2(mixed $obj): void {
// Direct instanceof in condition should work (and already does)
if (($obj instanceof ClassA) || ($obj instanceof ClassB)) {
assertType('Bug9519\ClassA|Bug9519\ClassB', $obj);
}
}

function test3(mixed $obj): void {
$isA = $obj instanceof ClassA;
$isB = $obj instanceof ClassB;

if ($isA) {
assertType('Bug9519\ClassA', $obj);
}

if ($isB) {
assertType('Bug9519\ClassB', $obj);
}
}

interface SomeInterface {
public function test(): void;
}

class ObjectClass {
}

class OtherClass extends ObjectClass {
}

/**
* @template T of object
* @param class-string<T> $class_name
* @return T
*/
function getObject(string $class_name): object {
return new $class_name;
}

function test4(): void {
$obj = getObject(ObjectClass::class);
$is_other = $obj instanceof OtherClass;
$is_interface = $obj instanceof SomeInterface;

if ($is_interface) {
assertType('Bug9519\ObjectClass&Bug9519\SomeInterface', $obj);
}
}

function test5(): void {
$obj = getObject(ObjectClass::class);
$is_interface = $obj instanceof SomeInterface;
$is_other = $obj instanceof OtherClass;

if ($is_interface) {
assertType('Bug9519\ObjectClass&Bug9519\SomeInterface', $obj);
}
}
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/nsrt/multi-assign.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ function (bool $b): void {
function (bool $b): void {
$foo = $bar = $baz = $b;
if ($bar) {
assertType('bool', $b);
assertType('true', $b);
assertType('bool', $foo);
assertType('true', $bar);
assertType('bool', $baz);
} else {
assertType('bool', $b);
assertType('false', $b);
assertType('bool', $foo);
assertType('false', $bar);
assertType('bool', $baz);
Expand All @@ -70,12 +70,12 @@ function (bool $b): void {
function (bool $b): void {
$foo = $bar = $baz = $b;
if ($baz) {
assertType('bool', $b);
assertType('true', $b);
assertType('bool', $foo);
assertType('bool', $bar);
assertType('true', $baz);
} else {
assertType('bool', $b);
assertType('false', $b);
assertType('bool', $foo);
assertType('bool', $bar);
assertType('false', $baz);
Expand Down
Loading