Skip to content

Commit 28c61b5

Browse files
phpstan-botclaude
andcommitted
Support intersecting ObjectShapeType with HasPropertyType
When intersecting an object shape with HasPropertyType, if the property doesn't exist in the shape, add it as mixed. If it exists, make it required. This is handled before isSuperTypeOf checks, similar to the ObjectShapeType-ObjectShapeType intersection. Also fix the NeverType check for overlapping properties with incompatible types: only skip if the property is optional in both shapes (not just one). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e0c9a72 commit 28c61b5

2 files changed

Lines changed: 37 additions & 18 deletions

File tree

src/Type/TypeCombinator.php

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,10 @@ public static function intersect(Type ...$types): Type
13091309
if (array_key_exists($propertyName, $mergedProperties)) {
13101310
$intersectedPropertyType = self::intersect($mergedProperties[$propertyName], $propertyType);
13111311
if ($intersectedPropertyType instanceof NeverType) {
1312-
if (in_array($propertyName, $types[$j]->getOptionalProperties(), true)) {
1312+
if (
1313+
in_array($propertyName, $mergedOptionalProperties, true)
1314+
&& in_array($propertyName, $types[$j]->getOptionalProperties(), true)
1315+
) {
13131316
continue;
13141317
}
13151318

@@ -1339,6 +1342,36 @@ public static function intersect(Type ...$types): Type
13391342
continue;
13401343
}
13411344

1345+
if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
1346+
$propertyName = $types[$j]->getPropertyName();
1347+
if (!array_key_exists($propertyName, $types[$i]->getProperties())) {
1348+
$properties = $types[$i]->getProperties();
1349+
$properties[$propertyName] = new MixedType();
1350+
ksort($properties);
1351+
$types[$i] = new ObjectShapeType($properties, $types[$i]->getOptionalProperties());
1352+
} else {
1353+
$types[$i] = $types[$i]->makePropertyRequired($propertyName);
1354+
}
1355+
array_splice($types, $j--, 1);
1356+
$typesCount--;
1357+
continue;
1358+
}
1359+
1360+
if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
1361+
$propertyName = $types[$i]->getPropertyName();
1362+
if (!array_key_exists($propertyName, $types[$j]->getProperties())) {
1363+
$properties = $types[$j]->getProperties();
1364+
$properties[$propertyName] = new MixedType();
1365+
ksort($properties);
1366+
$types[$j] = new ObjectShapeType($properties, $types[$j]->getOptionalProperties());
1367+
} else {
1368+
$types[$j] = $types[$j]->makePropertyRequired($propertyName);
1369+
}
1370+
array_splice($types, $i--, 1);
1371+
$typesCount--;
1372+
continue 2;
1373+
}
1374+
13421375
if ($types[$j] instanceof IterableType) {
13431376
$isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]);
13441377
} else {
@@ -1449,20 +1482,6 @@ public static function intersect(Type ...$types): Type
14491482
continue 2;
14501483
}
14511484

1452-
if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
1453-
$types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName());
1454-
array_splice($types, $j--, 1);
1455-
$typesCount--;
1456-
continue;
1457-
}
1458-
1459-
if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
1460-
$types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName());
1461-
array_splice($types, $i--, 1);
1462-
$typesCount--;
1463-
continue 2;
1464-
}
1465-
14661485
if ($types[$i] instanceof ConstantArrayType && ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType)) {
14671486
$newArray = ConstantArrayTypeBuilder::createEmpty();
14681487
$valueTypes = $types[$i]->getValueTypes();

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4632,8 +4632,8 @@ public static function dataIntersect(): iterable
46324632
new ObjectShapeType(['foo' => new IntegerType()], []),
46334633
new HasPropertyType('bar'),
46344634
],
4635-
NeverType::class,
4636-
'*NEVER*=implicit',
4635+
ObjectShapeType::class,
4636+
'object{bar: mixed, foo: int}',
46374637
];
46384638
yield [
46394639
[
@@ -4705,7 +4705,7 @@ public static function dataIntersect(): iterable
47054705
new ObjectShapeType(['foo' => new StringType()], ['foo']),
47064706
],
47074707
NeverType::class,
4708-
'object{foo: int}',
4708+
'*NEVER*=implicit',
47094709
];
47104710
yield [
47114711
[

0 commit comments

Comments
 (0)