Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1734,7 +1734,7 @@ parameters:
-
rawMessage: 'Doing instanceof PHPStan\Type\ObjectShapeType is error-prone and deprecated. Use Type::isObject() and Type::hasProperty() instead.'
identifier: phpstanApi.instanceofType
count: 2
count: 4
path: src/Type/TypeCombinator.php

-
Expand Down
21 changes: 11 additions & 10 deletions src/Type/ObjectShapeType.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\PhpDocParser\Ast\Type\ObjectShapeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Reflection\ClassMemberAccessAnswerer;
use PHPStan\Reflection\Dummy\DummyPropertyReflection;
use PHPStan\Reflection\ExtendedPropertyReflection;
use PHPStan\Reflection\MissingPropertyFromReflectionException;
use PHPStan\Reflection\Php\UniversalObjectCratesClassReflectionExtension;
Expand Down Expand Up @@ -113,15 +114,14 @@ public function getUnresolvedPropertyPrototype(string $propertyName, ClassMember

public function hasInstanceProperty(string $propertyName): TrinaryLogic
{
if (!array_key_exists($propertyName, $this->properties)) {
return TrinaryLogic::createNo();
if (
array_key_exists($propertyName, $this->properties)
&& !in_array($propertyName, $this->optionalProperties, true)
) {
return TrinaryLogic::createYes();
}

if (in_array($propertyName, $this->optionalProperties, true)) {
return TrinaryLogic::createMaybe();
}

return TrinaryLogic::createYes();
return TrinaryLogic::createMaybe();
}

public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswerer $scope): ExtendedPropertyReflection
Expand All @@ -131,11 +131,12 @@ public function getInstanceProperty(string $propertyName, ClassMemberAccessAnswe

public function getUnresolvedInstancePropertyPrototype(string $propertyName, ClassMemberAccessAnswerer $scope): UnresolvedPropertyPrototypeReflection
{
if (!array_key_exists($propertyName, $this->properties)) {
throw new ShouldNotHappenException();
if (array_key_exists($propertyName, $this->properties)) {
$property = new ObjectShapePropertyReflection($propertyName, $this->properties[$propertyName]);
} else {
$property = new DummyPropertyReflection($propertyName);
}

$property = new ObjectShapePropertyReflection($propertyName, $this->properties[$propertyName]);
return new CallbackUnresolvedPropertyPrototypeReflection(
$property,
$property->getDeclaringClass(),
Expand Down
91 changes: 77 additions & 14 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateUnionType;
use function array_filter;
use function array_key_exists;
use function array_key_first;
use function array_merge;
Expand All @@ -36,6 +37,7 @@
use function get_class;
use function in_array;
use function is_int;
use function ksort;
use function sprintf;
use function usort;
use const PHP_INT_MAX;
Expand Down Expand Up @@ -1300,6 +1302,81 @@ public static function intersect(Type ...$types): Type
}
}

if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof ObjectShapeType) {
$mergedProperties = $types[$i]->getProperties();
$mergedOptionalProperties = $types[$i]->getOptionalProperties();
foreach ($types[$j]->getProperties() as $propertyName => $propertyType) {
if (array_key_exists($propertyName, $mergedProperties)) {
$intersectedPropertyType = self::intersect($mergedProperties[$propertyName], $propertyType);
if ($intersectedPropertyType instanceof NeverType) {
if (
in_array($propertyName, $mergedOptionalProperties, true)
&& in_array($propertyName, $types[$j]->getOptionalProperties(), true)
) {
unset($mergedProperties[$propertyName]);
$mergedOptionalProperties = array_values(array_filter(
$mergedOptionalProperties,
static fn ($p) => $p !== $propertyName,
));
continue;
}

return new NeverType();
}
$mergedProperties[$propertyName] = $intersectedPropertyType;
if (
in_array($propertyName, $mergedOptionalProperties, true)
&& !in_array($propertyName, $types[$j]->getOptionalProperties(), true)
) {
$mergedOptionalProperties = array_values(array_filter(
$mergedOptionalProperties,
static fn ($p) => $p !== $propertyName,
));
}
} else {
$mergedProperties[$propertyName] = $propertyType;
if (in_array($propertyName, $types[$j]->getOptionalProperties(), true)) {
$mergedOptionalProperties[] = $propertyName;
}
}
}
ksort($mergedProperties);
$types[$i] = new ObjectShapeType($mergedProperties, $mergedOptionalProperties);
array_splice($types, $j--, 1);
$typesCount--;
continue;
}

if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
$propertyName = $types[$j]->getPropertyName();
if (!array_key_exists($propertyName, $types[$i]->getProperties())) {
$properties = $types[$i]->getProperties();
$properties[$propertyName] = new MixedType();
ksort($properties);
$types[$i] = new ObjectShapeType($properties, $types[$i]->getOptionalProperties());
} else {
$types[$i] = $types[$i]->makePropertyRequired($propertyName);
}
array_splice($types, $j--, 1);
$typesCount--;
continue;
}

if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
$propertyName = $types[$i]->getPropertyName();
if (!array_key_exists($propertyName, $types[$j]->getProperties())) {
$properties = $types[$j]->getProperties();
$properties[$propertyName] = new MixedType();
ksort($properties);
$types[$j] = new ObjectShapeType($properties, $types[$j]->getOptionalProperties());
} else {
$types[$j] = $types[$j]->makePropertyRequired($propertyName);
}
array_splice($types, $i--, 1);
$typesCount--;
continue 2;
}

if ($types[$j] instanceof IterableType) {
$isSuperTypeA = $types[$j]->isSuperTypeOfMixed($types[$i]);
} else {
Expand Down Expand Up @@ -1410,20 +1487,6 @@ public static function intersect(Type ...$types): Type
continue 2;
}

if ($types[$i] instanceof ObjectShapeType && $types[$j] instanceof HasPropertyType) {
$types[$i] = $types[$i]->makePropertyRequired($types[$j]->getPropertyName());
array_splice($types, $j--, 1);
$typesCount--;
continue;
}

if ($types[$j] instanceof ObjectShapeType && $types[$i] instanceof HasPropertyType) {
$types[$j] = $types[$j]->makePropertyRequired($types[$i]->getPropertyName());
array_splice($types, $i--, 1);
$typesCount--;
continue 2;
}

if ($types[$i] instanceof ConstantArrayType && ($types[$j] instanceof ArrayType || $types[$j] instanceof ConstantArrayType)) {
$newArray = ConstantArrayTypeBuilder::createEmpty();
$valueTypes = $types[$i]->getValueTypes();
Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13227.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Bug13227;

use function PHPStan\Testing\assertType;

/**
* @phpstan-type Type1 object{ a: int }
* @phpstan-type Type2 Type1 & object{ b: int }
* @phpstan-type Type3 object{ a: int, b?: string } & object{ b: string, c?: int }
*/
class Foo
{
/**
* @param Type2 $x
*/
public function doFoo($x): void
{
assertType('object{a: int, b: int}', $x);
}

/**
* @param Type3 $y
*/
public function doBar($y): void
{
assertType('object{a: int, b: string, c?: int}', $y);
}
}
159 changes: 159 additions & 0 deletions tests/PHPStan/Type/ObjectShapeTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type;

use PHPStan\Testing\PHPStanTestCase;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPUnit\Framework\Attributes\DataProvider;
use function sprintf;

class ObjectShapeTypeTest extends PHPStanTestCase
{

public static function dataIsSuperTypeOf(): iterable
{
// Same properties, same types
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createYes(),
];

// Wider property type is supertype
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []),
TrinaryLogic::createYes(),
];

// Narrower property type is maybe supertype
yield [
new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createMaybe(),
];

// Incompatible property types
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new StringType()], []),
TrinaryLogic::createNo(),
];

// Disjoint properties - object shapes are open types
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['bar' => new StringType()], []),
TrinaryLogic::createMaybe(),
];

yield [
new ObjectShapeType(['bar' => new StringType()], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createMaybe(),
];

// Required vs optional: optional is supertype of required
yield [
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createYes(),
];

// Required vs optional: required is maybe supertype of optional
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
TrinaryLogic::createMaybe(),
];

// Wider type with required property
yield [
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createYes(),
];

// Narrower type checking wider
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], []),
TrinaryLogic::createMaybe(),
];

// Optional wider type vs required narrower
yield [
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], ['foo']),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createYes(),
];

// Required narrower vs optional wider
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], ['foo']),
TrinaryLogic::createMaybe(),
];

// Disjoint with optional property
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['bar' => new IntegerType()], ['bar']),
TrinaryLogic::createMaybe(),
];

yield [
new ObjectShapeType(['bar' => new IntegerType()], ['bar']),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createMaybe(),
];

// Optional property with incompatible types
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new StringType()], ['foo']),
TrinaryLogic::createMaybe(),
];

// Superset has extra required property - maybe because shapes are open
yield [
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createMaybe(),
];

// Subset is supertype
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], []),
TrinaryLogic::createYes(),
];

// Empty shape is supertype of any shape
yield [
new ObjectShapeType([], []),
new ObjectShapeType(['foo' => new IntegerType()], []),
TrinaryLogic::createYes(),
];

// Any shape is maybe supertype of empty shape
yield [
new ObjectShapeType(['foo' => new IntegerType()], []),
new ObjectShapeType([], []),
TrinaryLogic::createMaybe(),
];
}

#[DataProvider('dataIsSuperTypeOf')]
public function testIsSuperTypeOf(ObjectShapeType $type, Type $otherType, TrinaryLogic $expectedResult): void
{
$actualResult = $type->isSuperTypeOf($otherType);
$this->assertSame(
$expectedResult->describe(),
$actualResult->describe(),
sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())),
);
}

}
Loading
Loading