Skip to content

Commit c55506a

Browse files
committed
Merge branch 2.2.x into 2.3.x
2 parents 6471caf + adb28cf commit c55506a

10 files changed

Lines changed: 317 additions & 73 deletions

File tree

‎phpstan-baseline.neon‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1089,7 +1089,7 @@ parameters:
10891089
-
10901090
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
10911091
identifier: phpstanApi.instanceofType
1092-
count: 3
1092+
count: 4
10931093
path: src/Type/Generic/GenericObjectType.php
10941094

10951095
-

‎src/Type/Generic/GenericObjectType.php‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,15 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe
172172
return $nakedSuperTypeOf;
173173
}
174174

175+
// Type arguments of a class name written without them are resolved to the
176+
// template bounds, so a `mixed` there means "not parameterized" and stays
177+
// compatible with any other type argument. When the other side is explicitly
178+
// parameterized, `mixed` is a type argument like any other and the variance
179+
// has to be evaluated strictly - otherwise `Foo<mixed>` and `Foo<int>` would
180+
// be supertypes of each other and TypeCombinator::union() would discard one
181+
// of them depending on their order.
182+
$strictVariance = !$acceptsContext && $type instanceof self;
183+
175184
$typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
176185
$results = [];
177186
foreach ($typeList as $i => $templateType) {
@@ -191,9 +200,9 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe
191200
$thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
192201
$ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant();
193202
if (!$thisVariance->invariant()) {
194-
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]);
203+
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i], $strictVariance);
195204
} else {
196-
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
205+
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i], $strictVariance);
197206
}
198207

199208
$results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance));

‎src/Type/Generic/TemplateType.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function toArgument(): TemplateType;
2626

2727
public function isArgument(): bool;
2828

29-
public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult;
29+
public function isValidVariance(Type $a, Type $b, bool $strict = false): IsSuperTypeOfResult;
3030

3131
public function getVariance(): TemplateTypeVariance;
3232

‎src/Type/Generic/TemplateTypeTrait.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ public function toArgument(): TemplateType
115115
);
116116
}
117117

118-
public function isValidVariance(Type $a, Type $b): IsSuperTypeOfResult
118+
public function isValidVariance(Type $a, Type $b, bool $strict = false): IsSuperTypeOfResult
119119
{
120-
return $this->variance->isValidVariance($this, $a, $b);
120+
return $this->variance->isValidVariance($this, $a, $b, $strict);
121121
}
122122

123123
public function subtract(Type $typeToRemove): Type

‎src/Type/Generic/TemplateTypeVariance.php‎

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -149,30 +149,42 @@ public function compose(self $other): self
149149
return $other;
150150
}
151151

152-
public function isValidVariance(TemplateType $templateType, Type $a, Type $b): IsSuperTypeOfResult
152+
/**
153+
* By default `mixed` and benevolent unions are compatible with any other type
154+
* argument in both directions - that is how gradual typing works.
155+
*
156+
* With $strict that answer is not given: it makes the relation symmetric
157+
* (`Foo<mixed>` and `Foo<int>` end up supertypes of each other) and
158+
* TypeCombinator::union() then discards one of them depending on the order the
159+
* types come in. In the strict mode invariance is relaxed just enough to keep
160+
* `Foo<mixed>` a supertype of `Foo<int>` and not the other way around.
161+
*/
162+
public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bool $strict = false): IsSuperTypeOfResult
153163
{
154164
if ($b instanceof NeverType) {
155165
return IsSuperTypeOfResult::createYes();
156166
}
157167

158-
if ($a instanceof MixedType && !$a instanceof TemplateType) {
159-
return IsSuperTypeOfResult::createYes();
160-
}
161-
162-
if ($a instanceof BenevolentUnionType) {
163-
if (!$a->isSuperTypeOf($b)->no()) {
168+
if (!$strict) {
169+
if ($a instanceof MixedType && !$a instanceof TemplateType) {
164170
return IsSuperTypeOfResult::createYes();
165171
}
166-
}
167172

168-
if ($b instanceof BenevolentUnionType) {
169-
if (!$b->isSuperTypeOf($a)->no()) {
170-
return IsSuperTypeOfResult::createYes();
173+
if ($a instanceof BenevolentUnionType) {
174+
if (!$a->isSuperTypeOf($b)->no()) {
175+
return IsSuperTypeOfResult::createYes();
176+
}
171177
}
172-
}
173178

174-
if ($b instanceof MixedType && !$b instanceof TemplateType) {
175-
return IsSuperTypeOfResult::createYes();
179+
if ($b instanceof BenevolentUnionType) {
180+
if (!$b->isSuperTypeOf($a)->no()) {
181+
return IsSuperTypeOfResult::createYes();
182+
}
183+
}
184+
185+
if ($b instanceof MixedType && !$b instanceof TemplateType) {
186+
return IsSuperTypeOfResult::createYes();
187+
}
176188
}
177189

178190
if ($this->invariant()) {
@@ -183,6 +195,24 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b): I
183195
return IsSuperTypeOfResult::createYes();
184196
}
185197

198+
if ($strict) {
199+
if ($a instanceof MixedType && !$a instanceof TemplateType) {
200+
return IsSuperTypeOfResult::createYes();
201+
}
202+
203+
if ($a instanceof BenevolentUnionType && !$a->isSuperTypeOf($b)->no()) {
204+
return IsSuperTypeOfResult::createYes();
205+
}
206+
207+
if ($b instanceof BenevolentUnionType && !$b->isSuperTypeOf($a)->no()) {
208+
return IsSuperTypeOfResult::createMaybe();
209+
}
210+
211+
if ($b instanceof MixedType && !$b instanceof TemplateType) {
212+
return IsSuperTypeOfResult::createMaybe();
213+
}
214+
}
215+
186216
$result = $a->equals($b);
187217
$reasons = [];
188218
if (!$result) {
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug15198;
6+
7+
use Exception;
8+
use function PHPStan\Testing\assertType;
9+
10+
/** @template T */
11+
class Foo
12+
{
13+
14+
/** @param T $value */
15+
public function __construct(
16+
public mixed $value,
17+
)
18+
{
19+
}
20+
21+
}
22+
23+
/** @template T */
24+
interface FooInterface
25+
{
26+
27+
}
28+
29+
/** @template-covariant T */
30+
class CovariantFoo
31+
{
32+
33+
}
34+
35+
/** @template-contravariant T */
36+
class ContravariantFoo
37+
{
38+
39+
}
40+
41+
/** @template T */
42+
abstract class Option
43+
{
44+
45+
}
46+
47+
/** @extends Option<mixed> */
48+
final class None extends Option
49+
{
50+
51+
}
52+
53+
function original(): void
54+
{
55+
try {
56+
/** @var mixed */
57+
$bar = 'bar';
58+
$foo = new Foo($bar);
59+
} catch (Exception $e) {
60+
$foo = new Foo(123);
61+
}
62+
63+
assertType('Bug15198\Foo<mixed>', $foo);
64+
}
65+
66+
function ifElse(bool $c, mixed $bar): void
67+
{
68+
if ($c) {
69+
$foo = new Foo($bar);
70+
} else {
71+
$foo = new Foo(123);
72+
}
73+
74+
assertType('Bug15198\Foo<mixed>', $foo);
75+
}
76+
77+
function ifElseReversed(bool $c, mixed $bar): void
78+
{
79+
if ($c) {
80+
$foo = new Foo(123);
81+
} else {
82+
$foo = new Foo($bar);
83+
}
84+
85+
assertType('Bug15198\Foo<mixed>', $foo);
86+
}
87+
88+
/**
89+
* @param Foo<mixed>|Foo<int> $a
90+
* @param Foo<int>|Foo<mixed> $b
91+
* @param CovariantFoo<mixed>|CovariantFoo<int> $c
92+
* @param CovariantFoo<int>|CovariantFoo<mixed> $d
93+
* @param ContravariantFoo<mixed>|ContravariantFoo<int> $e
94+
* @param ContravariantFoo<int>|ContravariantFoo<mixed> $f
95+
* @param FooInterface<mixed>|FooInterface<int> $g
96+
* @param FooInterface<int>|FooInterface<mixed> $h
97+
* @param class-string<Foo<mixed>>|class-string<Foo<int>> $i
98+
* @param class-string<Foo<int>>|class-string<Foo<mixed>> $j
99+
*/
100+
function unions($a, $b, $c, $d, $e, $f, $g, $h, $i, $j): void
101+
{
102+
assertType('Bug15198\Foo<mixed>', $a);
103+
assertType('Bug15198\Foo<mixed>', $b);
104+
assertType('Bug15198\CovariantFoo<mixed>', $c);
105+
assertType('Bug15198\CovariantFoo<mixed>', $d);
106+
assertType('Bug15198\ContravariantFoo<int>', $e);
107+
assertType('Bug15198\ContravariantFoo<int>', $f);
108+
assertType('Bug15198\FooInterface<mixed>', $g);
109+
assertType('Bug15198\FooInterface<mixed>', $h);
110+
assertType('class-string<Bug15198\Foo<mixed>>', $i);
111+
assertType('class-string<Bug15198\Foo<mixed>>', $j);
112+
}
113+
114+
/**
115+
* @param Foo<mixed>&Foo<int> $a
116+
* @param Foo<int>&Foo<mixed> $b
117+
*/
118+
function intersections($a, $b): void
119+
{
120+
assertType('Bug15198\Foo<int>', $a);
121+
assertType('Bug15198\Foo<int>', $b);
122+
}
123+
124+
/**
125+
* A class name written without type arguments has them resolved to the template
126+
* bounds, so `None` stays compatible with `Option<string>`.
127+
*
128+
* @param None|Option<string> $a
129+
* @param Option<string>|None $b
130+
*/
131+
function unparameterizedSubclass($a, $b): void
132+
{
133+
assertType('Bug15198\Option<string>', $a);
134+
assertType('Bug15198\Option<string>', $b);
135+
}

‎tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,4 +435,28 @@ public function testBug13810(): void
435435
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13810.php'], []);
436436
}
437437

438+
#[RequiresPhp('>= 8.0.0')]
439+
public function testBug11935(): void
440+
{
441+
// Below level 9 mixed is compatible with every type argument in both
442+
// directions - that is how gradual typing works.
443+
$this->analyse([__DIR__ . '/data/bug-11935.php'], []);
444+
}
445+
446+
#[RequiresPhp('>= 8.0.0')]
447+
public function testBug11935WithCheckExplicitMixed(): void
448+
{
449+
$this->checkExplicitMixed = true;
450+
$this->analyse([__DIR__ . '/data/bug-11935.php'], [
451+
[
452+
'Parameter #1 of callable callable(Bug11935\Inv<A>): Bug11935\Inv<A> expects Bug11935\Inv<A>, Bug11935\Inv<mixed> given.',
453+
34,
454+
],
455+
[
456+
'Parameter #1 of callable callable(Bug11935\Inv<int>): void expects Bug11935\Inv<int>, Bug11935\Inv<mixed> given.',
457+
43,
458+
],
459+
]);
460+
}
461+
438462
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php // lint >= 8.0
2+
3+
declare(strict_types = 1);
4+
5+
namespace Bug11935;
6+
7+
/**
8+
* @template A
9+
*
10+
* @param mixed $a Intentionally mixed
11+
* @param callable(A): A $aa
12+
* @return A
13+
*/
14+
function identity(mixed $a, callable $aa): mixed
15+
{
16+
return $aa($a);
17+
}
18+
19+
/** @template T */
20+
class Inv
21+
{
22+
23+
}
24+
25+
/**
26+
* @template A
27+
*
28+
* @param Inv<mixed> $a Intentionally Inv<mixed>
29+
* @param callable(Inv<A>): Inv<A> $aa
30+
* @return Inv<A>
31+
*/
32+
function identityInv(Inv $a, callable $aa): Inv
33+
{
34+
return $aa($a);
35+
}
36+
37+
/**
38+
* @param Inv<mixed> $a
39+
* @param callable(Inv<int>): void $aa
40+
*/
41+
function passMixedTypeArgument(Inv $a, callable $aa): void
42+
{
43+
$aa($a);
44+
}

‎tests/PHPStan/Type/Generic/GenericObjectTypeTest.php‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ public static function dataIsSuperTypeOf(): array
6060
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
6161
TrinaryLogic::createNo(),
6262
],
63+
// https://github.com/phpstan/phpstan/issues/11935 - `mixed` as a type
64+
// argument stays compatible with any other one in the accepts context,
65+
// but isSuperTypeOf() has to be a one-way relation, otherwise
66+
// TypeCombinator would discard one of the two based on their order.
67+
'same class, mixed type arg on the super side' => [
68+
new GenericObjectType(A\A::class, [new MixedType(true)]),
69+
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
70+
TrinaryLogic::createYes(),
71+
],
72+
'same class, mixed type arg on the sub side' => [
73+
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
74+
new GenericObjectType(A\A::class, [new MixedType(true)]),
75+
TrinaryLogic::createMaybe(),
76+
],
6377
'same class, one naked' => [
6478
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
6579
new ObjectType(A\A::class),
@@ -302,6 +316,17 @@ public static function dataAccepts(): array
302316
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
303317
TrinaryLogic::createNo(),
304318
],
319+
// https://github.com/phpstan/phpstan/issues/11935
320+
'same class, mixed type arg on the accepting side' => [
321+
new GenericObjectType(A\A::class, [new MixedType(true)]),
322+
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
323+
TrinaryLogic::createYes(),
324+
],
325+
'same class, mixed type arg on the accepted side' => [
326+
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
327+
new GenericObjectType(A\A::class, [new MixedType(true)]),
328+
TrinaryLogic::createYes(),
329+
],
305330
'same class, one naked' => [
306331
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
307332
new ObjectType(A\A::class),

0 commit comments

Comments
 (0)