Skip to content

Commit 56ae015

Browse files
committed
Dependent types - save conditional expression after variable assignment
1 parent c471c7b commit 56ae015

4 files changed

Lines changed: 269 additions & 7 deletions

File tree

src/Analyser/NodeScopeResolver.php

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,6 @@
9393
use PHPStan\TrinaryLogic;
9494
use PHPStan\Type\Accessory\NonEmptyArrayType;
9595
use PHPStan\Type\ArrayType;
96-
use PHPStan\Type\BooleanType;
9796
use PHPStan\Type\CallableType;
9897
use PHPStan\Type\ClosureType;
9998
use PHPStan\Type\Constant\ConstantArrayType;
@@ -2627,12 +2626,89 @@ private function processAssignVar(
26272626
$result = $processExprCallback($scope);
26282627
$hasYield = $result->hasYield();
26292628
$type = $scope->getType($assignedExpr);
2630-
$scope = $result->getScope()->assignVariable($var->name, $type);
2629+
$truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
2630+
$falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
2631+
2632+
$conditionalExpressions = [];
2633+
2634+
// todo DRY
2635+
foreach ($truthySpecifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) {
2636+
if (!$expr instanceof Variable) {
2637+
continue;
2638+
}
2639+
if (!is_string($expr->name)) {
2640+
continue;
2641+
}
2642+
2643+
if (!isset($conditionalExpressions[$exprString])) {
2644+
$conditionalExpressions[$exprString] = [];
2645+
}
2646+
2647+
$conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([
2648+
'$' . $var->name => TypeCombinator::remove($type, StaticTypeFactory::falsey()),
2649+
], VariableTypeHolder::createYes(
2650+
TypeCombinator::intersect($scope->getType($expr), $exprType)
2651+
)); // todo why createYes?
2652+
}
2653+
foreach ($truthySpecifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) {
2654+
if (!$expr instanceof Variable) {
2655+
continue;
2656+
}
2657+
if (!is_string($expr->name)) {
2658+
continue;
2659+
}
2660+
2661+
if (!isset($conditionalExpressions[$exprString])) {
2662+
$conditionalExpressions[$exprString] = [];
2663+
}
2664+
2665+
$conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([
2666+
'$' . $var->name => TypeCombinator::remove($type, StaticTypeFactory::falsey()),
2667+
], VariableTypeHolder::createYes(
2668+
TypeCombinator::remove($scope->getType($expr), $exprType)
2669+
)); // todo why createYes?
2670+
}
26312671

2632-
if ($type instanceof BooleanType) {
2633-
$truthyScope = $scope->filterByTruthyValue($assignedExpr)->assignVariable($var->name, TypeCombinator::remove($type, StaticTypeFactory::falsey()));
2634-
$falseyScope = $scope->filterByFalseyValue($assignedExpr)->assignVariable($var->name, TypeCombinator::intersect($type, StaticTypeFactory::falsey()));
2635-
$scope = $truthyScope->mergeWith($falseyScope);
2672+
foreach ($falseySpecifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) {
2673+
if (!$expr instanceof Variable) {
2674+
continue;
2675+
}
2676+
if (!is_string($expr->name)) {
2677+
continue;
2678+
}
2679+
2680+
if (!isset($conditionalExpressions[$exprString])) {
2681+
$conditionalExpressions[$exprString] = [];
2682+
}
2683+
2684+
$conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([
2685+
'$' . $var->name => TypeCombinator::intersect($type, StaticTypeFactory::falsey()),
2686+
], VariableTypeHolder::createYes(
2687+
TypeCombinator::intersect($scope->getType($expr), $exprType)
2688+
)); // todo why createYes?
2689+
}
2690+
foreach ($falseySpecifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) {
2691+
if (!$expr instanceof Variable) {
2692+
continue;
2693+
}
2694+
if (!is_string($expr->name)) {
2695+
continue;
2696+
}
2697+
2698+
if (!isset($conditionalExpressions[$exprString])) {
2699+
$conditionalExpressions[$exprString] = [];
2700+
}
2701+
2702+
$conditionalExpressions[$exprString][] = new ConditionalExpressionHolder([
2703+
'$' . $var->name => TypeCombinator::intersect($type, StaticTypeFactory::falsey()),
2704+
], VariableTypeHolder::createYes(
2705+
TypeCombinator::remove($scope->getType($expr), $exprType)
2706+
)); // todo why createYes?
2707+
}
2708+
2709+
$scope = $result->getScope()->assignVariable($var->name, $type);
2710+
foreach ($conditionalExpressions as $exprString => $holders) {
2711+
$scope = $scope->addConditionalExpressions($exprString, $holders);
26362712
}
26372713
} elseif ($var instanceof ArrayDimFetch) {
26382714
$dimExprStack = [];

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5740,6 +5740,16 @@ public function dataBug4695(): array
57405740
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4695.php');
57415741
}
57425742

5743+
public function dataBug2977(): array
5744+
{
5745+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-2977.php');
5746+
}
5747+
5748+
public function dataBug3190(): array
5749+
{
5750+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3190.php');
5751+
}
5752+
57435753
/**
57445754
* @dataProvider dataArrayFunctions
57455755
* @param string $description
@@ -6531,7 +6541,7 @@ public function dataTypeSpecifyingExtensions(): array
65316541
false,
65326542
],
65336543
[
6534-
'int',
6544+
'int|null',
65356545
'$bar',
65366546
false,
65376547
],
@@ -11371,6 +11381,8 @@ private function gatherAssertTypes(string $file): array
1137111381
* @dataProvider dataBug3677
1137211382
* @dataProvider dataBug4215
1137311383
* @dataProvider dataBug4695
11384+
* @dataProvider dataBug2977
11385+
* @dataProvider dataBug3190
1137411386
* @param string $assertType
1137511387
* @param string $file
1137611388
* @param mixed ...$args
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php
2+
3+
namespace Bug2977;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
interface MyInterface
8+
{
9+
/**
10+
* @return self|null The parent or null if there is none
11+
*/
12+
public function getParent();
13+
14+
/**
15+
* @return mixed data
16+
*
17+
* @throws \Exception If the form inherits data but has no parent
18+
*/
19+
public function getData();
20+
}
21+
22+
class My implements MyInterface
23+
{
24+
/**
25+
* @var MyInterface|null
26+
*/
27+
private $parent;
28+
29+
/**
30+
* @var mixed
31+
*/
32+
private $data;
33+
34+
/**
35+
* {@inheritdoc}
36+
*/
37+
public function getParent()
38+
{
39+
return $this->parent;
40+
}
41+
42+
/**
43+
* {@inheritdoc}
44+
*/
45+
public function getData()
46+
{
47+
return $this->data;
48+
}
49+
}
50+
51+
class Bar
52+
{
53+
public function baz(MyInterface $my): string
54+
{
55+
$parent = $my->getParent();
56+
if (!$parent) {
57+
assertType('null', $parent);
58+
return 'ok';
59+
}
60+
61+
assertType(MyInterface::class, $parent);
62+
63+
return $parent->getData();
64+
}
65+
66+
public function baz2(MyInterface $my): string
67+
{
68+
$parent = $my->getParent();
69+
70+
$case = $parent;
71+
if (!$case) {
72+
assertType('null', $parent);
73+
return 'ok';
74+
}
75+
76+
assertType(MyInterface::class, $parent);
77+
78+
return $parent->getData();
79+
}
80+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace Bug3190;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
interface Server
8+
{
9+
public function isDedicated(): bool;
10+
11+
public function getSize(): int;
12+
}
13+
14+
class Deployer
15+
{
16+
public function deploy(object $component): void
17+
{
18+
$dedicated = $component instanceof Server ? $component->isDedicated() : false;
19+
if (!$dedicated) {
20+
return;
21+
}
22+
23+
assertType(Server::class, $component);
24+
}
25+
26+
public function deploy2(object $component): void
27+
{
28+
$dedicated = $component instanceof Server && $component->isDedicated();
29+
if (!$dedicated) {
30+
return;
31+
}
32+
33+
assertType(Server::class, $component);
34+
}
35+
36+
public function deploy3(object $component): void
37+
{
38+
$dedicated = $component instanceof Server;
39+
if (!$dedicated) {
40+
return;
41+
}
42+
43+
assertType(Server::class, $component);
44+
}
45+
46+
public function deploy4(object $component): void
47+
{
48+
$dedicated = $component instanceof Server ? $component->isDedicated() : rand(0, 1);
49+
if (!$dedicated) {
50+
return;
51+
}
52+
53+
assertType('object', $component);
54+
}
55+
}
56+
57+
class Deployer2
58+
{
59+
public function deploy(object $component): void
60+
{
61+
$dedicated = $component instanceof Server ? $component->isDedicated() : false;
62+
if ($dedicated) {
63+
assertType(Server::class, $component);
64+
return;
65+
}
66+
}
67+
68+
public function deploy2(object $component): void
69+
{
70+
$dedicated = $component instanceof Server && $component->isDedicated();
71+
if ($dedicated) {
72+
assertType(Server::class, $component);
73+
return;
74+
}
75+
}
76+
77+
public function deploy3(object $component): void
78+
{
79+
$dedicated = $component instanceof Server;
80+
if ($dedicated) {
81+
assertType(Server::class, $component);
82+
return;
83+
}
84+
}
85+
86+
public function deploy4(object $component): void
87+
{
88+
$dedicated = $component instanceof Server ? $component->isDedicated() : rand(0, 1);
89+
if ($dedicated) {
90+
assertType('object', $component);
91+
return;
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)