Skip to content

Commit c6be672

Browse files
phpstan-botclaude
andcommitted
Cover mixed type arguments in both directions with regression tests
Pins the gradual-typing leniency the strict variance mode has to keep: `accepts()` stays symmetric for a `mixed` type argument, while `isSuperTypeOf()` only answers `yes` when the `mixed` is on the supertype side. See phpstan/phpstan#11935 Co-Authored-By: Claude Opus 5 <[email protected]>
1 parent e5fb80d commit c6be672

3 files changed

Lines changed: 93 additions & 0 deletions

File tree

‎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)