Skip to content

Commit d41aeb1

Browse files
phpstan-botclaude
andcommitted
Add ObjectShapeType isSuperTypeOf tests, intersection branch coverage, and union tests
- Create ObjectShapeTypeTest with isSuperTypeOf tests covering: same types, wider/narrower types, incompatible types, disjoint properties, required vs optional, empty shapes, and subset/superset relationships - Add dataIntersect tests with optional-in-i ordering to cover the isOptionalInI && !isOptionalInJ branch in TypeCombinator - Add dataUnion tests for object shapes: optional absorbs required, wider type absorbs narrower, disjoint properties stay as union, subset absorbs superset Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c26a999 commit d41aeb1

File tree

2 files changed

+226
-0
lines changed

2 files changed

+226
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PHPStan\Testing\PHPStanTestCase;
6+
use PHPStan\TrinaryLogic;
7+
use PHPStan\Type\Constant\ConstantIntegerType;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use function sprintf;
10+
11+
class ObjectShapeTypeTest extends PHPStanTestCase
12+
{
13+
14+
public static function dataIsSuperTypeOf(): iterable
15+
{
16+
// Same properties, same types
17+
yield [
18+
new ObjectShapeType(['foo' => new IntegerType()], []),
19+
new ObjectShapeType(['foo' => new IntegerType()], []),
20+
TrinaryLogic::createYes(),
21+
];
22+
23+
// Wider property type is supertype
24+
yield [
25+
new ObjectShapeType(['foo' => new IntegerType()], []),
26+
new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []),
27+
TrinaryLogic::createYes(),
28+
];
29+
30+
// Narrower property type is maybe supertype
31+
yield [
32+
new ObjectShapeType(['foo' => new ConstantIntegerType(1)], []),
33+
new ObjectShapeType(['foo' => new IntegerType()], []),
34+
TrinaryLogic::createMaybe(),
35+
];
36+
37+
// Incompatible property types
38+
yield [
39+
new ObjectShapeType(['foo' => new IntegerType()], []),
40+
new ObjectShapeType(['foo' => new StringType()], []),
41+
TrinaryLogic::createNo(),
42+
];
43+
44+
// Disjoint properties - object shapes are open types
45+
yield [
46+
new ObjectShapeType(['foo' => new IntegerType()], []),
47+
new ObjectShapeType(['bar' => new StringType()], []),
48+
TrinaryLogic::createMaybe(),
49+
];
50+
51+
yield [
52+
new ObjectShapeType(['bar' => new StringType()], []),
53+
new ObjectShapeType(['foo' => new IntegerType()], []),
54+
TrinaryLogic::createMaybe(),
55+
];
56+
57+
// Required vs optional: optional is supertype of required
58+
yield [
59+
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
60+
new ObjectShapeType(['foo' => new IntegerType()], []),
61+
TrinaryLogic::createYes(),
62+
];
63+
64+
// Required vs optional: required is maybe supertype of optional
65+
yield [
66+
new ObjectShapeType(['foo' => new IntegerType()], []),
67+
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
68+
TrinaryLogic::createMaybe(),
69+
];
70+
71+
// Wider type with required property
72+
yield [
73+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], []),
74+
new ObjectShapeType(['foo' => new IntegerType()], []),
75+
TrinaryLogic::createYes(),
76+
];
77+
78+
// Narrower type checking wider
79+
yield [
80+
new ObjectShapeType(['foo' => new IntegerType()], []),
81+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], []),
82+
TrinaryLogic::createMaybe(),
83+
];
84+
85+
// Optional wider type vs required narrower
86+
yield [
87+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], ['foo']),
88+
new ObjectShapeType(['foo' => new IntegerType()], []),
89+
TrinaryLogic::createYes(),
90+
];
91+
92+
// Required narrower vs optional wider
93+
yield [
94+
new ObjectShapeType(['foo' => new IntegerType()], []),
95+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], ['foo']),
96+
TrinaryLogic::createMaybe(),
97+
];
98+
99+
// Disjoint with optional property
100+
yield [
101+
new ObjectShapeType(['foo' => new IntegerType()], []),
102+
new ObjectShapeType(['bar' => new IntegerType()], ['bar']),
103+
TrinaryLogic::createMaybe(),
104+
];
105+
106+
yield [
107+
new ObjectShapeType(['bar' => new IntegerType()], ['bar']),
108+
new ObjectShapeType(['foo' => new IntegerType()], []),
109+
TrinaryLogic::createMaybe(),
110+
];
111+
112+
// Optional property with incompatible types
113+
yield [
114+
new ObjectShapeType(['foo' => new IntegerType()], []),
115+
new ObjectShapeType(['foo' => new StringType()], ['foo']),
116+
TrinaryLogic::createMaybe(),
117+
];
118+
119+
// Superset has extra required property - maybe because shapes are open
120+
yield [
121+
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], []),
122+
new ObjectShapeType(['foo' => new IntegerType()], []),
123+
TrinaryLogic::createMaybe(),
124+
];
125+
126+
// Subset is supertype
127+
yield [
128+
new ObjectShapeType(['foo' => new IntegerType()], []),
129+
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], []),
130+
TrinaryLogic::createYes(),
131+
];
132+
133+
// Empty shape is supertype of any shape
134+
yield [
135+
new ObjectShapeType([], []),
136+
new ObjectShapeType(['foo' => new IntegerType()], []),
137+
TrinaryLogic::createYes(),
138+
];
139+
140+
// Any shape is maybe supertype of empty shape
141+
yield [
142+
new ObjectShapeType(['foo' => new IntegerType()], []),
143+
new ObjectShapeType([], []),
144+
TrinaryLogic::createMaybe(),
145+
];
146+
}
147+
148+
/**
149+
* @param TrinaryLogic $expectedResult
150+
*/
151+
#[DataProvider('dataIsSuperTypeOf')]
152+
public function testIsSuperTypeOf(ObjectShapeType $type, Type $otherType, TrinaryLogic $expectedResult): void
153+
{
154+
$actualResult = $type->isSuperTypeOf($otherType);
155+
$this->assertSame(
156+
$expectedResult->describe(),
157+
$actualResult->describe(),
158+
sprintf('%s -> isSuperTypeOf(%s)', $type->describe(VerbosityLevel::precise()), $otherType->describe(VerbosityLevel::precise())),
159+
);
160+
}
161+
162+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2578,6 +2578,54 @@ public static function dataUnion(): iterable
25782578
UnionType::class,
25792579
'object{bar: string}|object{foo: int}',
25802580
];
2581+
yield [
2582+
[
2583+
new ObjectShapeType(['foo' => new IntegerType()], []),
2584+
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
2585+
],
2586+
ObjectShapeType::class,
2587+
'object{foo?: int}',
2588+
];
2589+
yield [
2590+
[
2591+
new ObjectShapeType(['foo' => new IntegerType()], []),
2592+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], []),
2593+
],
2594+
ObjectShapeType::class,
2595+
'object{foo: int|null}',
2596+
];
2597+
yield [
2598+
[
2599+
new ObjectShapeType(['foo' => new IntegerType()], []),
2600+
new ObjectShapeType(['foo' => TypeCombinator::union(new IntegerType(), new NullType())], ['foo']),
2601+
],
2602+
ObjectShapeType::class,
2603+
'object{foo?: int|null}',
2604+
];
2605+
yield [
2606+
[
2607+
new ObjectShapeType(['foo' => new IntegerType()], []),
2608+
new ObjectShapeType(['bar' => new IntegerType()], ['bar']),
2609+
],
2610+
UnionType::class,
2611+
'object{bar?: int}|object{foo: int}',
2612+
];
2613+
yield [
2614+
[
2615+
new ObjectShapeType(['foo' => new IntegerType()], []),
2616+
new ObjectShapeType(['foo' => new StringType()], ['foo']),
2617+
],
2618+
UnionType::class,
2619+
'object{foo: int}|object{foo?: string}',
2620+
];
2621+
yield [
2622+
[
2623+
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], []),
2624+
new ObjectShapeType(['foo' => new IntegerType()], []),
2625+
],
2626+
ObjectShapeType::class,
2627+
'object{foo: int}',
2628+
];
25812629

25822630
yield [
25832631
[
@@ -4659,6 +4707,22 @@ public static function dataIntersect(): iterable
46594707
NeverType::class,
46604708
'*NEVER*=implicit',
46614709
];
4710+
yield [
4711+
[
4712+
new ObjectShapeType(['foo' => new IntegerType()], ['foo']),
4713+
new ObjectShapeType(['foo' => new IntegerType()], []),
4714+
],
4715+
ObjectShapeType::class,
4716+
'object{foo: int}',
4717+
];
4718+
yield [
4719+
[
4720+
new ObjectShapeType(['foo' => new IntegerType(), 'bar' => new StringType()], ['foo']),
4721+
new ObjectShapeType(['foo' => new IntegerType()], []),
4722+
],
4723+
ObjectShapeType::class,
4724+
'object{bar: string, foo: int}',
4725+
];
46624726
yield [
46634727
[
46644728
new ObjectShapeType(['foo' => new IntegerType()], []),

0 commit comments

Comments
 (0)