Skip to content

Commit bc8479f

Browse files
Support intersecting object shapes in TypeCombinator
- Added ObjectShapeType merging logic in TypeCombinator::intersect() so that two object shapes can be intersected by combining their properties - Properties present in both shapes have their types intersected; if the intersection is never, the whole result is never - A property is optional in the result only if it is optional in both shapes - Updated TypeCombinatorTest expectations and baseline for new instanceof count - New regression test in tests/PHPStan/Analyser/nsrt/bug-13227.php
1 parent 8b36ae3 commit bc8479f

File tree

4 files changed

+66
-3
lines changed

4 files changed

+66
-3
lines changed

phpstan-baseline.neon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1734,7 +1734,7 @@ parameters:
17341734
-
17351735
rawMessage: 'Doing instanceof PHPStan\Type\ObjectShapeType is error-prone and deprecated. Use Type::isObject() and Type::hasProperty() instead.'
17361736
identifier: phpstanApi.instanceofType
1737-
count: 2
1737+
count: 4
17381738
path: src/Type/TypeCombinator.php
17391739

17401740
-

src/Type/TypeCombinator.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\Type\Generic\TemplateType;
2727
use PHPStan\Type\Generic\TemplateTypeFactory;
2828
use PHPStan\Type\Generic\TemplateUnionType;
29+
use function array_filter;
2930
use function array_key_exists;
3031
use function array_key_first;
3132
use function array_merge;
@@ -36,6 +37,7 @@
3637
use function get_class;
3738
use function in_array;
3839
use function is_int;
40+
use function ksort;
3941
use function sprintf;
4042
use function usort;
4143
use const PHP_INT_MAX;
@@ -1300,6 +1302,38 @@ public static function intersect(Type ...$types): Type
13001302
}
13011303
}
13021304

1305+
if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof ObjectShapeType) {
1306+
$mergedProperties = $types[$i]->getProperties();
1307+
$mergedOptionalProperties = $types[$i]->getOptionalProperties();
1308+
foreach ($types[$j]->getProperties() as $propertyName => $propertyType) {
1309+
if (array_key_exists($propertyName, $mergedProperties)) {
1310+
$intersectedPropertyType = self::intersect($mergedProperties[$propertyName], $propertyType);
1311+
if ($intersectedPropertyType instanceof NeverType) {
1312+
return new NeverType();
1313+
}
1314+
$mergedProperties[$propertyName] = $intersectedPropertyType;
1315+
$isOptionalInI = in_array($propertyName, $mergedOptionalProperties, true);
1316+
$isOptionalInJ = in_array($propertyName, $types[$j]->getOptionalProperties(), true);
1317+
if ($isOptionalInI && !$isOptionalInJ) {
1318+
$mergedOptionalProperties = array_values(array_filter(
1319+
$mergedOptionalProperties,
1320+
static fn ($p) => $p !== $propertyName,
1321+
));
1322+
}
1323+
} else {
1324+
$mergedProperties[$propertyName] = $propertyType;
1325+
if (in_array($propertyName, $types[$j]->getOptionalProperties(), true)) {
1326+
$mergedOptionalProperties[] = $propertyName;
1327+
}
1328+
}
1329+
}
1330+
ksort($mergedProperties);
1331+
$types[$i] = new ObjectShapeType($mergedProperties, $mergedOptionalProperties);
1332+
array_splice($types, $j--, 1);
1333+
$typesCount--;
1334+
continue;
1335+
}
1336+
13031337
if ($types[$j] instanceof IterableType) {
13041338
$isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]);
13051339
} else {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace Bug13227;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
/**
8+
* @phpstan-type Type1 object{ a: int }
9+
* @phpstan-type Type2 Type1 & object{ b: int }
10+
* @phpstan-type Type3 object{ a: int, b?: string } & object{ b: string, c?: int }
11+
*/
12+
class Foo
13+
{
14+
/**
15+
* @param Type2 $x
16+
*/
17+
public function doFoo($x): void
18+
{
19+
assertType('object{a: int, b: int}', $x);
20+
}
21+
22+
/**
23+
* @param Type3 $y
24+
*/
25+
public function doBar($y): void
26+
{
27+
assertType('object{a: int, b: string, c?: int}', $y);
28+
}
29+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4616,8 +4616,8 @@ public static function dataIntersect(): iterable
46164616
new ObjectShapeType(['foo' => new IntegerType()], []),
46174617
new ObjectShapeType(['bar' => new StringType()], []),
46184618
],
4619-
NeverType::class,
4620-
'*NEVER*=implicit',
4619+
ObjectShapeType::class,
4620+
'object{bar: string, foo: int}',
46214621
];
46224622
yield [
46234623
[

0 commit comments

Comments
 (0)