Skip to content

Commit 463995c

Browse files
authored
Fix constant-string handling in union-types
1 parent 9128321 commit 463995c

File tree

11 files changed

+368
-94
lines changed

11 files changed

+368
-94
lines changed

phpstan-baseline.neon

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ parameters:
1515
count: 1
1616
path: src/Analyser/LazyInternalScopeFactory.php
1717

18+
-
19+
message: """
20+
#^Call to deprecated method getAnyArrays\\(\\) of class PHPStan\\\\Type\\\\TypeUtils\\:
21+
Use PHPStan\\\\Type\\\\Type\\:\\:getArrays\\(\\) instead\\.$#
22+
"""
23+
count: 2
24+
path: src/Analyser/MutatingScope.php
25+
1826
-
1927
message: """
2028
#^Call to deprecated method getTypeFromValue\\(\\) of class PHPStan\\\\Type\\\\ConstantTypeHelper\\:

src/Analyser/MutatingScope.php

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4078,7 +4078,7 @@ public function processClosureScope(
40784078
$prevVariableType = $prevScope->getVariableType($variableName);
40794079
if (!$variableType->equals($prevVariableType)) {
40804080
$variableType = TypeCombinator::union($variableType, $prevVariableType);
4081-
$variableType = self::generalizeType($variableType, $prevVariableType);
4081+
$variableType = self::generalizeType($variableType, $prevVariableType, 0);
40824082
}
40834083
}
40844084

@@ -4200,15 +4200,15 @@ private function generalizeVariableTypeHolders(
42004200

42014201
$variableTypeHolders[$variableExprString] = new ExpressionTypeHolder(
42024202
$variableTypeHolder->getExpr(),
4203-
self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType()),
4203+
self::generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0),
42044204
$variableTypeHolder->getCertainty(),
42054205
);
42064206
}
42074207

42084208
return $variableTypeHolders;
42094209
}
42104210

4211-
private static function generalizeType(Type $a, Type $b): Type
4211+
private static function generalizeType(Type $a, Type $b, int $depth): Type
42124212
{
42134213
if ($a->equals($b)) {
42144214
return $a;
@@ -4301,6 +4301,7 @@ private static function generalizeType(Type $a, Type $b): Type
43014301
self::generalizeType(
43024302
$constantArraysA->getOffsetValueType($keyType),
43034303
$constantArraysB->getOffsetValueType($keyType),
4304+
$depth + 1,
43044305
),
43054306
!$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(),
43064307
);
@@ -4309,8 +4310,8 @@ private static function generalizeType(Type $a, Type $b): Type
43094310
$resultTypes[] = $resultArrayBuilder->getArray();
43104311
} else {
43114312
$resultType = new ArrayType(
4312-
TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType())),
4313-
TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType())),
4313+
TypeCombinator::union(self::generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)),
4314+
TypeCombinator::union(self::generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)),
43144315
);
43154316
if ($constantArraysA->isIterableAtLeastOnce()->yes() && $constantArraysB->isIterableAtLeastOnce()->yes()) {
43164317
$resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType());
@@ -4334,16 +4335,14 @@ private static function generalizeType(Type $a, Type $b): Type
43344335

43354336
$aValueType = $generalArraysA->getIterableValueType();
43364337
$bValueType = $generalArraysB->getIterableValueType();
4337-
$aArrays = $aValueType->getArrays();
4338-
$bArrays = $bValueType->getArrays();
43394338
if (
4340-
count($aArrays) === 1
4341-
&& $aArrays[0]->isConstantArray()->no()
4342-
&& count($bArrays) === 1
4343-
&& $bArrays[0]->isConstantArray()->no()
4339+
$aValueType->isArray()->yes()
4340+
&& $aValueType->isConstantArray()->no()
4341+
&& $bValueType->isArray()->yes()
4342+
&& $bValueType->isConstantArray()->no()
43444343
) {
4345-
$aDepth = self::getArrayDepth($aArrays[0]);
4346-
$bDepth = self::getArrayDepth($bArrays[0]);
4344+
$aDepth = self::getArrayDepth($aValueType) + $depth;
4345+
$bDepth = self::getArrayDepth($bValueType) + $depth;
43474346
if (
43484347
($aDepth > 2 || $bDepth > 2)
43494348
&& abs($aDepth - $bDepth) > 0
@@ -4354,8 +4353,8 @@ private static function generalizeType(Type $a, Type $b): Type
43544353
}
43554354

43564355
$resultType = new ArrayType(
4357-
TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType())),
4358-
TypeCombinator::union(self::generalizeType($aValueType, $bValueType)),
4356+
TypeCombinator::union(self::generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)),
4357+
TypeCombinator::union(self::generalizeType($aValueType, $bValueType, $depth + 1)),
43594358
);
43604359
if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) {
43614360
$resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType());
@@ -4514,17 +4513,14 @@ private static function generalizeType(Type $a, Type $b): Type
45144513
);
45154514
}
45164515

4517-
private static function getArrayDepth(ArrayType $type): int
4516+
private static function getArrayDepth(Type $type): int
45184517
{
45194518
$depth = 0;
4520-
while ($type instanceof ArrayType) {
4519+
$arrays = TypeUtils::getAnyArrays($type);
4520+
while (count($arrays) > 0) {
45214521
$temp = $type->getIterableValueType();
4522-
$arrays = $temp->getArrays();
4523-
if (count($arrays) === 1) {
4524-
$type = $arrays[0];
4525-
} else {
4526-
$type = $temp;
4527-
}
4522+
$type = $temp;
4523+
$arrays = TypeUtils::getAnyArrays($type);
45284524
$depth++;
45294525
}
45304526

src/Type/IntersectionType.php

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,52 @@ public function inferTemplateTypesOn(Type $templateType): TemplateTypeMap
9393
return $types;
9494
}
9595

96-
/**
97-
* @return string[]
98-
*/
9996
public function getReferencedClasses(): array
10097
{
101-
return UnionTypeHelper::getReferencedClasses($this->types);
98+
$classes = [];
99+
foreach ($this->types as $type) {
100+
foreach ($type->getReferencedClasses() as $className) {
101+
$classes[] = $className;
102+
}
103+
}
104+
105+
return $classes;
102106
}
103107

104108
public function getArrays(): array
105109
{
106-
return UnionTypeHelper::getArrays($this->getTypes());
110+
$arrays = [];
111+
foreach ($this->types as $type) {
112+
foreach ($type->getArrays() as $array) {
113+
$arrays[] = $array;
114+
}
115+
}
116+
117+
return $arrays;
107118
}
108119

109120
public function getConstantArrays(): array
110121
{
111-
return UnionTypeHelper::getConstantArrays($this->getTypes());
122+
$constantArrays = [];
123+
foreach ($this->types as $type) {
124+
foreach ($type->getConstantArrays() as $constantArray) {
125+
$constantArrays[] = $constantArray;
126+
}
127+
}
128+
129+
return $constantArrays;
112130
}
113131

114132
public function getConstantStrings(): array
115133
{
116-
return UnionTypeHelper::getConstantStrings($this->getTypes());
134+
$strings = [];
135+
foreach ($this->types as $type) {
136+
foreach ($type->getConstantStrings() as $string) {
137+
$strings[] = $string;
138+
}
139+
}
140+
141+
return $strings;
117142
}
118143

119144
public function accepts(Type $otherType, bool $strictTypes): TrinaryLogic

src/Type/TypeUtils.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,19 @@ private static function map(
218218
if ($type instanceof UnionType) {
219219
$matchingTypes = [];
220220
foreach ($type->getTypes() as $innerType) {
221-
if (!$innerType instanceof $typeClass) {
221+
$matchingInner = self::map($typeClass, $innerType, $inspectIntersections, $stopOnUnmatched);
222+
223+
if ($matchingInner === []) {
222224
if ($stopOnUnmatched) {
223225
return [];
224226
}
225227

226228
continue;
227229
}
228230

229-
$matchingTypes[] = $innerType;
231+
foreach ($matchingInner as $innerMapped) {
232+
$matchingTypes[] = $innerMapped;
233+
}
230234
}
231235

232236
return $matchingTypes;

src/Type/UnionType.php

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,22 +102,67 @@ private function getSortedTypes(): array
102102
*/
103103
public function getReferencedClasses(): array
104104
{
105-
return UnionTypeHelper::getReferencedClasses($this->getTypes());
105+
$classes = [];
106+
foreach ($this->types as $type) {
107+
foreach ($type->getReferencedClasses() as $className) {
108+
$classes[] = $className;
109+
}
110+
}
111+
112+
return $classes;
106113
}
107114

108115
public function getArrays(): array
109116
{
110-
return UnionTypeHelper::getArrays($this->getTypes());
117+
$arrays = [];
118+
foreach ($this->types as $type) {
119+
$innerTypeArrays = $type->getArrays();
120+
if ($innerTypeArrays === []) {
121+
return [];
122+
}
123+
124+
foreach ($innerTypeArrays as $array) {
125+
$arrays[] = $array;
126+
}
127+
}
128+
129+
return $arrays;
111130
}
112131

113132
public function getConstantArrays(): array
114133
{
115-
return UnionTypeHelper::getConstantArrays($this->getTypes());
134+
$constantArrays = [];
135+
foreach ($this->types as $type) {
136+
$typeAsConstantArrays = $type->getConstantArrays();
137+
138+
if ($typeAsConstantArrays === []) {
139+
return [];
140+
}
141+
142+
foreach ($typeAsConstantArrays as $constantArray) {
143+
$constantArrays[] = $constantArray;
144+
}
145+
}
146+
147+
return $constantArrays;
116148
}
117149

118150
public function getConstantStrings(): array
119151
{
120-
return UnionTypeHelper::getConstantStrings($this->getTypes());
152+
$strings = [];
153+
foreach ($this->types as $type) {
154+
$constantStrings = $type->getConstantStrings();
155+
156+
if ($constantStrings === []) {
157+
return [];
158+
}
159+
160+
foreach ($constantStrings as $string) {
161+
$strings[] = $string;
162+
}
163+
}
164+
165+
return $strings;
121166
}
122167

123168
public function accepts(Type $type, bool $strictTypes): TrinaryLogic

src/Type/UnionTypeHelper.php

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,10 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Type\Accessory\AccessoryType;
6-
use PHPStan\Type\Constant\ConstantArrayType;
76
use PHPStan\Type\Constant\ConstantBooleanType;
87
use PHPStan\Type\Constant\ConstantFloatType;
98
use PHPStan\Type\Constant\ConstantIntegerType;
109
use PHPStan\Type\Constant\ConstantStringType;
11-
use function array_merge;
1210
use function count;
1311
use function strcasecmp;
1412
use function usort;
@@ -17,62 +15,6 @@
1715
class UnionTypeHelper
1816
{
1917

20-
/**
21-
* @param Type[] $types
22-
* @return string[]
23-
*/
24-
public static function getReferencedClasses(array $types): array
25-
{
26-
$referencedClasses = [];
27-
foreach ($types as $type) {
28-
$referencedClasses[] = $type->getReferencedClasses();
29-
}
30-
31-
return array_merge(...$referencedClasses);
32-
}
33-
34-
/**
35-
* @param Type[] $types
36-
* @return list<ArrayType>
37-
*/
38-
public static function getArrays(array $types): array
39-
{
40-
$arrays = [];
41-
foreach ($types as $type) {
42-
$arrays[] = $type->getArrays();
43-
}
44-
45-
return array_merge(...$arrays);
46-
}
47-
48-
/**
49-
* @param Type[] $types
50-
* @return list<ConstantArrayType>
51-
*/
52-
public static function getConstantArrays(array $types): array
53-
{
54-
$constantArrays = [];
55-
foreach ($types as $type) {
56-
$constantArrays[] = $type->getConstantArrays();
57-
}
58-
59-
return array_merge(...$constantArrays);
60-
}
61-
62-
/**
63-
* @param Type[] $types
64-
* @return list<ConstantStringType>
65-
*/
66-
public static function getConstantStrings(array $types): array
67-
{
68-
$strings = [];
69-
foreach ($types as $type) {
70-
$strings[] = $type->getConstantStrings();
71-
}
72-
73-
return array_merge(...$strings);
74-
}
75-
7618
/**
7719
* @param Type[] $types
7820
* @return Type[]

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,6 +1154,7 @@ public function dataFileAsserts(): iterable
11541154
yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo-php8.php');
11551155
}
11561156
yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo.php');
1157+
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8568.php');
11571158
}
11581159

11591160
/**
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug8568;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
public function sayHello(): void
10+
{
11+
assertType('non-falsy-string', 'a' . $this->get());
12+
}
13+
14+
public function get(): ?int
15+
{
16+
return rand() ? 5 : null;
17+
}
18+
19+
/**
20+
* @param numeric-string $numericS
21+
*/
22+
public function intersections($numericS): void {
23+
assertType('non-falsy-string', 'a'. $numericS);
24+
assertType('numeric-string', (string) $numericS);
25+
}
26+
}

tests/PHPStan/Rules/Comparison/UnreachableIfBranchesRuleTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,10 @@ public function testBug8076(): void
122122
$this->analyse([__DIR__ . '/data/bug-8076.php'], []);
123123
}
124124

125+
public function testBug8562(): void
126+
{
127+
$this->treatPhpDocTypesAsCertain = true;
128+
$this->analyse([__DIR__ . '/data/bug-8562.php'], []);
129+
}
130+
125131
}

0 commit comments

Comments
 (0)