Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b072257

Browse files
authored
Augment BooleanAnd falsey and BooleanOr truthy type narrowing when left and right conditions narrow different expression keys (#5595)
1 parent f149ab5 commit b072257

8 files changed

Lines changed: 334 additions & 6 deletions

File tree

src/Analyser/TypeSpecifier.php

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,14 @@ public function specifyTypesInCondition(
719719
$leftTypes = $this->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
720720
$rightScope = $scope->filterByTruthyValue($expr->left);
721721
$rightTypes = $this->specifyTypesInCondition($rightScope, $expr->right, $context)->setRootExpr($expr);
722-
$types = $context->true() ? $leftTypes->unionWith($rightTypes) : $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope));
722+
if ($context->true()) {
723+
$types = $leftTypes->unionWith($rightTypes);
724+
} else {
725+
$leftNormalized = $leftTypes->normalize($scope);
726+
$rightNormalized = $rightTypes->normalize($rightScope);
727+
$types = $leftNormalized->intersectWith($rightNormalized);
728+
$types = $this->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, false, $types);
729+
}
723730
if ($context->false()) {
724731
$leftTypesForHolders = $leftTypes;
725732
$rightTypesForHolders = $rightTypes;
@@ -773,8 +780,11 @@ public function specifyTypesInCondition(
773780
) {
774781
$types = $leftTypes->normalize($scope);
775782
} else {
776-
$types = $leftTypes->normalize($scope)->intersectWith($rightTypes->normalize($rightScope));
783+
$leftNormalized = $leftTypes->normalize($scope);
784+
$rightNormalized = $rightTypes->normalize($rightScope);
785+
$types = $leftNormalized->intersectWith($rightNormalized);
777786
$types = $this->augmentBooleanOrTruthyWithConditionalHolders($scope, $rightScope, $expr, $types);
787+
$types = $this->augmentDisjunctionTypes($scope, $rightScope, $leftNormalized, $rightNormalized, $expr->left, $expr->right, true, $types);
778788
}
779789
} else {
780790
$types = $leftTypes->unionWith($rightTypes);
@@ -2061,6 +2071,83 @@ private function augmentBooleanOrTruthyWithConditionalHolders(MutatingScope $sco
20612071
return $types;
20622072
}
20632073

2074+
private function augmentDisjunctionTypes(
2075+
MutatingScope $scope,
2076+
MutatingScope $rightScope,
2077+
SpecifiedTypes $leftNormalized,
2078+
SpecifiedTypes $rightNormalized,
2079+
Expr $leftExpr,
2080+
Expr $rightExpr,
2081+
bool $truthy,
2082+
SpecifiedTypes $types,
2083+
): SpecifiedTypes
2084+
{
2085+
$candidateExprs = [];
2086+
foreach ($leftNormalized->getSureTypes() as $exprString => [$exprNode, $type]) {
2087+
$candidateExprs[$exprString] = $exprNode;
2088+
}
2089+
foreach ($rightNormalized->getSureTypes() as $exprString => [$exprNode, $type]) {
2090+
$candidateExprs[$exprString] = $exprNode;
2091+
}
2092+
2093+
$existingSureTypes = $types->getSureTypes();
2094+
2095+
$viableCandidates = [];
2096+
foreach ($candidateExprs as $exprString => $targetExpr) {
2097+
if (isset($existingSureTypes[$exprString])) {
2098+
continue;
2099+
}
2100+
if (!$scope->hasExpressionType($targetExpr)->yes()) {
2101+
continue;
2102+
}
2103+
$viableCandidates[$exprString] = $targetExpr;
2104+
}
2105+
2106+
if ($viableCandidates === []) {
2107+
return $types;
2108+
}
2109+
2110+
if ($truthy) {
2111+
$leftFilteredScope = $scope->filterByTruthyValue($leftExpr);
2112+
$rightFilteredScope = $rightScope->filterByTruthyValue($rightExpr);
2113+
} else {
2114+
$leftFilteredScope = $scope->filterByFalseyValue($leftExpr);
2115+
$rightFilteredScope = $rightScope->filterByFalseyValue($rightExpr);
2116+
}
2117+
2118+
foreach ($viableCandidates as $targetExpr) {
2119+
if (!$leftFilteredScope->hasExpressionType($targetExpr)->yes()) {
2120+
continue;
2121+
}
2122+
if (!$rightFilteredScope->hasExpressionType($targetExpr)->yes()) {
2123+
continue;
2124+
}
2125+
2126+
$originalType = $scope->getType($targetExpr);
2127+
$leftType = $leftFilteredScope->getType($targetExpr);
2128+
$rightType = $rightFilteredScope->getType($targetExpr);
2129+
2130+
if ($leftType->equals($originalType) || !$originalType->isSuperTypeOf($leftType)->yes()) {
2131+
continue;
2132+
}
2133+
2134+
if ($rightType->equals($originalType) || !$originalType->isSuperTypeOf($rightType)->yes()) {
2135+
continue;
2136+
}
2137+
2138+
$unionType = TypeCombinator::union($leftType, $rightType);
2139+
if ($unionType->equals($originalType)) {
2140+
continue;
2141+
}
2142+
2143+
$types = $types->unionWith(
2144+
$this->create($targetExpr, $unionType, TypeSpecifierContext::createTrue(), $scope),
2145+
);
2146+
}
2147+
2148+
return $types;
2149+
}
2150+
20642151
/**
20652152
* @return array<string, ConditionalExpressionHolder[]>
20662153
*/

tests/PHPStan/Analyser/data/type-specifying-extensions-2-false.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
if ((new \PHPStan\Tests\AssertionClass())->assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) {
1111
}
1212

13-
assertType('string|null', $foo);
13+
assertType('string', $foo);
1414
assertType('int|null', $bar);

tests/PHPStan/Analyser/data/type-specifying-extensions-2-null.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
if ((new \PHPStan\Tests\AssertionClass())->assertString($foo) && \PHPStan\Tests\AssertionClass::assertInt($bar)) {
1111
}
1212

13-
assertType('string|null', $foo);
13+
assertType('string', $foo);
1414
assertType('int|null', $bar);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug13061;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
interface ScenarioInterface {}
8+
9+
class ScenarioNode implements ScenarioInterface {}
10+
class OutlineNode implements ScenarioInterface {}
11+
12+
class FeatureNode
13+
{
14+
/**
15+
* @param ScenarioInterface[] $scenarios
16+
*/
17+
public function __construct(
18+
public readonly ?string $title,
19+
public readonly array $scenarios,
20+
) {
21+
}
22+
}
23+
24+
/**
25+
* @phpstan-type TFeatureHash array{title?: string|null, scenarios?: array<int, TScenarioHash|TOutlineHash>}
26+
* @phpstan-type TScenarioHash array{type?: 'scenario', title?: string|null}
27+
* @phpstan-type TOutlineHash array{type: 'outline', title?: string|null, examples?: array<array-key, TExampleTableHash>}
28+
* @phpstan-type TExampleTableHash array<int, list<string>>
29+
*/
30+
abstract class GherkinArrayLoader
31+
{
32+
/**
33+
* @phpstan-param TFeatureHash $hash
34+
*/
35+
protected function loadFeatureHash(array $hash, int $line = 0): FeatureNode
36+
{
37+
$hash = array_merge(
38+
[
39+
'title' => null,
40+
'scenarios' => [],
41+
],
42+
$hash
43+
);
44+
45+
$scenarios = [];
46+
foreach ((array) $hash['scenarios'] as $scenarioIterator => $scenarioHash) {
47+
if (isset($scenarioHash['type']) && $scenarioHash['type'] === 'outline') {
48+
assertType("array{type: 'outline', title?: string|null, examples?: array<array<int, list<string>>>}", $scenarioHash);
49+
$scenarios[] = $this->loadOutlineHash($scenarioHash, $scenarioIterator);
50+
} else {
51+
assertType("array{type?: 'scenario', title?: string|null}", $scenarioHash);
52+
$scenarios[] = $this->loadScenarioHash($scenarioHash, $scenarioIterator);
53+
}
54+
}
55+
56+
return new FeatureNode($hash['title'], $scenarios);
57+
}
58+
59+
/**
60+
* @phpstan-param TScenarioHash $hash
61+
*/
62+
abstract protected function loadScenarioHash(array $hash, int $line = 0): ScenarioNode;
63+
64+
/**
65+
* @phpstan-param TOutlineHash $hash
66+
*/
67+
abstract protected function loadOutlineHash(array $hash, int $line = 0): OutlineNode;
68+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14566;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
9+
*/
10+
function fooNestedIfs(array $test): void {
11+
if (isset($test['hi'])) {
12+
if (is_string($test['hi'])) {
13+
return;
14+
}
15+
}
16+
assertType("array{}|array{hi: array{0: 42, 1?: 42}}", $test);
17+
}
18+
19+
/**
20+
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
21+
*/
22+
function fooCombinedAnd(array $test): void {
23+
if (isset($test['hi']) && is_string($test['hi'])) {
24+
return;
25+
}
26+
assertType("array{}|array{hi: array{0: 42, 1?: 42}}", $test);
27+
}
28+
29+
/**
30+
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
31+
*/
32+
function fooCombinedAndAssign(array $test): void {
33+
if (isset($test['hi']) && is_string($test['hi'])) {
34+
return;
35+
}
36+
$test['hi'][] = 42;
37+
}
38+
39+
/**
40+
* @param array{}|array{hi: 'hello'}|array{hi: array{0: 42, 1?: 42}} $test
41+
*/
42+
function fooBooleanOrDual(array $test): void {
43+
if (!isset($test['hi']) || !is_string($test['hi'])) {
44+
assertType("array{}|array{hi: array{0: 42, 1?: 42}}", $test);
45+
return;
46+
}
47+
assertType("array{hi: 'hello'}", $test);
48+
}
49+
50+
/**
51+
* @param array{}|array{hi: 42}|array{hi: array{0: 42, 1?: 42}} $testIsArray
52+
*/
53+
function fooIsArray(array $testIsArray): void {
54+
if (isset($testIsArray['hi']) && is_array($testIsArray['hi'])) {
55+
return;
56+
}
57+
assertType("array{}|array{hi: 42}", $testIsArray);
58+
}
59+
60+
/**
61+
* @param array{}|array{hi: 42}|array{hi: 'hello'} $testIsInt
62+
*/
63+
function fooIsInt(array $testIsInt): void {
64+
if (isset($testIsInt['hi']) && is_int($testIsInt['hi'])) {
65+
return;
66+
}
67+
assertType("array{}|array{hi: 'hello'}", $testIsInt);
68+
}
69+
70+
/**
71+
* @param array{}|array{hi: 42}|array{hi: 1.5} $testIsFloat
72+
*/
73+
function fooIsFloat(array $testIsFloat): void {
74+
if (isset($testIsFloat['hi']) && is_float($testIsFloat['hi'])) {
75+
return;
76+
}
77+
assertType("array{}|array{hi: 42}", $testIsFloat);
78+
}
79+
80+
/**
81+
* @param array{}|array{hi: true}|array{hi: 'hello'} $testIsBool
82+
*/
83+
function fooIsBool(array $testIsBool): void {
84+
if (isset($testIsBool['hi']) && is_bool($testIsBool['hi'])) {
85+
return;
86+
}
87+
assertType("array{}|array{hi: 'hello'}", $testIsBool);
88+
}
89+
90+
/**
91+
* @param array{}|array{val: 'hello'}|array{val: array{0: 42}} $testArrayKeyExists
92+
*/
93+
function fooArrayKeyExists(array $testArrayKeyExists): void {
94+
if (array_key_exists('val', $testArrayKeyExists) && is_string($testArrayKeyExists['val'])) {
95+
return;
96+
}
97+
assertType("array{}|array{val: array{42}}", $testArrayKeyExists);
98+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug7259;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorldNullable
8+
{
9+
public function __construct(
10+
private ?\DateTimeImmutable $from,
11+
private ?\DateTimeImmutable $till,
12+
)
13+
{
14+
$newFrom = $this->from;
15+
$newTill = $this->till;
16+
17+
if ($newFrom !== null || $newTill !== null) {
18+
if ($newFrom !== null && $newTill === null) {
19+
$newFrom = $newFrom->setTime(0, 0);
20+
$newTill = new \DateTimeImmutable('2300-12-31 23:59:59');
21+
}
22+
23+
if ($newTill !== null && $newFrom === null) {
24+
$newTill = $newTill->setTime(23, 59, 59, 999999);
25+
$newFrom = new \DateTimeImmutable('1970-01-01 00:00:00');
26+
}
27+
28+
assertType('DateTimeImmutable', $newFrom);
29+
assertType('DateTimeImmutable', $newTill);
30+
$this->checkDates($newFrom, $newTill);
31+
}
32+
}
33+
34+
private function checkDates(
35+
\DateTimeImmutable $from,
36+
\DateTimeImmutable $till,
37+
): void
38+
{
39+
}
40+
}
41+
42+
class HelloWorldStringInt
43+
{
44+
public function __construct(
45+
private string|int $from,
46+
private string|int $till,
47+
)
48+
{
49+
$newFrom = $this->from;
50+
$newTill = $this->till;
51+
52+
if (is_string($newFrom) || is_string($newTill)) {
53+
if (is_string($newFrom) && is_string($newTill) === false) {
54+
$newTill = 'test';
55+
}
56+
57+
if (is_string($newTill) && is_string($newFrom) === false) {
58+
$newFrom = 'test2';
59+
}
60+
61+
assertType('string', $newFrom);
62+
assertType('string', $newTill);
63+
$this->checkDates($newFrom, $newTill);
64+
}
65+
}
66+
67+
private function checkDates(
68+
string $from,
69+
string $till,
70+
): void
71+
{
72+
}
73+
}

tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,10 @@ public function testBug3632(): void
511511
[
512512
'Instanceof between Bug3632\NiceClass and Bug3632\NiceClass will always evaluate to true.',
513513
36,
514-
$tipText,
514+
],
515+
[
516+
'Instanceof between null and Bug3632\NiceClass will always evaluate to false.',
517+
36,
515518
],
516519
]);
517520
}

tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,6 @@ public function testBug11903(): void
170170
[
171171
'Negated boolean expression is always true.',
172172
21,
173-
'Because the type is coming from a PHPDoc, you can turn off this check by setting <fg=cyan>treatPhpDocTypesAsCertain: false</> in your <fg=cyan>%configurationFile%</>.',
174173
],
175174
]);
176175
}

0 commit comments

Comments
 (0)