Skip to content

Commit 2c42ef1

Browse files
committed
Dependent types - understand truthy BooleanOr and falsey BooleanAnd scope
1 parent 5d37113 commit 2c42ef1

File tree

5 files changed

+139
-6
lines changed

5 files changed

+139
-6
lines changed

src/Analyser/MutatingScope.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3560,7 +3560,7 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
35603560
$typeGuards['$' . $variableName] = $typeGuard;
35613561
}
35623562

3563-
$newConditionalExpressions = [];
3563+
$newConditionalExpressions = $specifiedTypes->getNewConditionalExpressionHolders();
35643564
foreach ($this->conditionalExpressions as $variableExprString => $conditionalExpressions) {
35653565
if (array_key_exists($variableExprString, $typeGuards)) {
35663566
continue;
@@ -3614,9 +3614,33 @@ public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
36143614
}
36153615
}
36163616

3617-
$scope->conditionalExpressions = $newConditionalExpressions;
3617+
return $scope->changeConditionalExpressions($newConditionalExpressions);
3618+
}
36183619

3619-
return $scope;
3620+
/**
3621+
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
3622+
* @return self
3623+
*/
3624+
public function changeConditionalExpressions(array $newConditionalExpressionHolders): self
3625+
{
3626+
return $this->scopeFactory->create(
3627+
$this->context,
3628+
$this->isDeclareStrictTypes(),
3629+
$this->constantTypes,
3630+
$this->getFunction(),
3631+
$this->getNamespace(),
3632+
$this->variableTypes,
3633+
$this->moreSpecificTypes,
3634+
$newConditionalExpressionHolders,
3635+
$this->inClosureBindScopeClass,
3636+
$this->anonymousFunctionReflection,
3637+
$this->inFirstLevelStatement,
3638+
$this->currentlyAssignedExpressions,
3639+
$this->nativeExpressionTypes,
3640+
$this->inFunctionCallsStack,
3641+
$this->afterExtractCall,
3642+
$this->parentScope
3643+
);
36203644
}
36213645

36223646
/**

src/Analyser/SpecifiedTypes.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,26 @@ class SpecifiedTypes
1515

1616
private bool $overwrite;
1717

18+
/** @var array<string, ConditionalExpressionHolder[]> */
19+
private array $newConditionalExpressionHolders;
20+
1821
/**
1922
* @param mixed[] $sureTypes
2023
* @param mixed[] $sureNotTypes
2124
* @param bool $overwrite
25+
* @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressionHolders
2226
*/
2327
public function __construct(
2428
array $sureTypes = [],
2529
array $sureNotTypes = [],
26-
bool $overwrite = false
30+
bool $overwrite = false,
31+
array $newConditionalExpressionHolders = []
2732
)
2833
{
2934
$this->sureTypes = $sureTypes;
3035
$this->sureNotTypes = $sureNotTypes;
3136
$this->overwrite = $overwrite;
37+
$this->newConditionalExpressionHolders = $newConditionalExpressionHolders;
3238
}
3339

3440
/**
@@ -52,6 +58,14 @@ public function shouldOverwrite(): bool
5258
return $this->overwrite;
5359
}
5460

61+
/**
62+
* @return array<string, ConditionalExpressionHolder[]>
63+
*/
64+
public function getNewConditionalExpressionHolders(): array
65+
{
66+
return $this->newConditionalExpressionHolders;
67+
}
68+
5569
public function intersectWith(SpecifiedTypes $other): self
5670
{
5771
$sureTypeUnion = [];

src/Analyser/TypeSpecifier.php

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PhpParser\Node\Expr\StaticPropertyFetch;
2020
use PhpParser\Node\Name;
2121
use PHPStan\Reflection\ReflectionProvider;
22+
use PHPStan\TrinaryLogic;
2223
use PHPStan\Type\Accessory\HasOffsetType;
2324
use PHPStan\Type\Accessory\HasPropertyType;
2425
use PHPStan\Type\Accessory\NonEmptyArrayType;
@@ -578,11 +579,21 @@ public function specifyTypesInCondition(
578579
} elseif ($expr instanceof BooleanAnd || $expr instanceof LogicalAnd) {
579580
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
580581
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
581-
return $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes);
582+
$types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->intersectWith($rightTypes);
583+
if ($context->false()) {
584+
return $this->processBooleanConditionalTypes($scope, $types, $leftTypes, $rightTypes);
585+
}
586+
587+
return $types;
582588
} elseif ($expr instanceof BooleanOr || $expr instanceof LogicalOr) {
583589
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context);
584590
$rightTypes = $this->specifyTypesInCondition($scope, $expr->right, $context);
585-
return $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes);
591+
$types = $context->true() ? $leftTypes->intersectWith($rightTypes) : $leftTypes->unionWith($rightTypes);
592+
if ($context->true()) {
593+
return $this->processBooleanConditionalTypes($scope, $types, $leftTypes, $rightTypes);
594+
}
595+
596+
return $types;
586597
} elseif ($expr instanceof Node\Expr\BooleanNot && !$context->null()) {
587598
return $this->specifyTypesInCondition($scope, $expr->expr, $context->negate());
588599
} elseif ($expr instanceof Node\Expr\Assign) {
@@ -744,6 +755,46 @@ private function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $contex
744755
return new SpecifiedTypes();
745756
}
746757

758+
private function processBooleanConditionalTypes(Scope $scope, SpecifiedTypes $types, SpecifiedTypes $leftTypes, SpecifiedTypes $rightTypes): SpecifiedTypes
759+
{
760+
$conditionExpressionTypes = [];
761+
foreach ($leftTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
762+
if (!$expr instanceof Expr\Variable) {
763+
continue;
764+
}
765+
if (!is_string($expr->name)) {
766+
continue;
767+
}
768+
769+
$conditionExpressionTypes[$exprString] = TypeCombinator::intersect($scope->getType($expr), $type);
770+
}
771+
772+
if (count($conditionExpressionTypes) > 0) {
773+
$holders = [];
774+
foreach ($rightTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
775+
if (!$expr instanceof Expr\Variable) {
776+
continue;
777+
}
778+
if (!is_string($expr->name)) {
779+
continue;
780+
}
781+
782+
if (!isset($holders[$exprString])) {
783+
$holders[$exprString] = [];
784+
}
785+
786+
$holders[$exprString][] = new ConditionalExpressionHolder(
787+
$conditionExpressionTypes,
788+
new VariableTypeHolder(TypeCombinator::remove($scope->getType($expr), $type), TrinaryLogic::createYes()) // todo yes is wrong
789+
);
790+
}
791+
792+
return new SpecifiedTypes($types->getSureTypes(), $types->getSureNotTypes(), false, $holders);
793+
}
794+
795+
return $types;
796+
}
797+
747798
/**
748799
* @param \PHPStan\Analyser\Scope $scope
749800
* @param \PhpParser\Node\Expr\BinaryOp $binaryOperation

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5710,6 +5710,11 @@ public function dataBug4725(): array
57105710
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4725.php');
57115711
}
57125712

5713+
public function dataBug4733(): array
5714+
{
5715+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4733.php');
5716+
}
5717+
57135718
/**
57145719
* @dataProvider dataArrayFunctions
57155720
* @param string $description
@@ -11335,6 +11340,7 @@ private function gatherAssertTypes(string $file): array
1133511340
* @dataProvider dataBug4545
1133611341
* @dataProvider dataBug4714
1133711342
* @dataProvider dataBug4725
11343+
* @dataProvider dataBug4733
1133811344
* @param string $assertType
1133911345
* @param string $file
1134011346
* @param mixed ...$args
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Bug4733;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class HelloWorld
8+
{
9+
public function getDescription(?\DateTimeImmutable $start, ?string $someObject): void
10+
{
11+
if ($start === null && $someObject === null) {
12+
return;
13+
}
14+
15+
// $start !== null || $someObject !== null
16+
17+
if ($start !== null) {
18+
return;
19+
}
20+
21+
// $start === null therefore $someObject !== null
22+
23+
assertType('string', $someObject);
24+
}
25+
26+
public function getDescription2(?\DateTimeImmutable $start, ?string $someObject): void
27+
{
28+
if ($start !== null || $someObject !== null) {
29+
if ($start !== null) {
30+
return;
31+
}
32+
33+
// $start === null therefore $someObject !== null
34+
35+
assertType('string', $someObject);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)