Skip to content
Merged
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 @@ -1113,7 +1113,7 @@ parameters:
-
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 3
count: 4
path: src/Type/Generic/GenericObjectType.php

-
Expand Down
13 changes: 11 additions & 2 deletions src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe
return $nakedSuperTypeOf;
}

// Type arguments of a class name written without them are resolved to the
// template bounds, so a `mixed` there means "not parameterized" and stays
// compatible with any other type argument. When the other side is explicitly
// parameterized, `mixed` is a type argument like any other and the variance
// has to be evaluated strictly - otherwise `Foo<mixed>` and `Foo<int>` would
// be supertypes of each other and TypeCombinator::union() would discard one
// of them depending on their order.
$strictVariance = !$acceptsContext && $type instanceof self;

$typeList = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
$results = [];
foreach ($typeList as $i => $templateType) {
Expand All @@ -191,9 +200,9 @@ private function isSuperTypeOfInternal(Type $type, bool $acceptsContext): IsSupe
$thisVariance = $this->variances[$i] ?? TemplateTypeVariance::createInvariant();
$ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant();
if (!$thisVariance->invariant()) {
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]);
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i], $strictVariance);
} else {
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i], $strictVariance);
}

$results[] = IsSuperTypeOfResult::createFromBoolean($thisVariance->validPosition($ancestorVariance));
Expand Down
2 changes: 1 addition & 1 deletion src/Type/Generic/TemplateType.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function toArgument(): TemplateType;

public function isArgument(): bool;

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

public function getVariance(): TemplateTypeVariance;

Expand Down
4 changes: 2 additions & 2 deletions src/Type/Generic/TemplateTypeTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ public function toArgument(): TemplateType
);
}

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

public function subtract(Type $typeToRemove): Type
Expand Down
58 changes: 44 additions & 14 deletions src/Type/Generic/TemplateTypeVariance.php
Original file line number Diff line number Diff line change
Expand Up @@ -149,30 +149,42 @@ public function compose(self $other): self
return $other;
}

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

if ($a instanceof MixedType && !$a instanceof TemplateType) {
return IsSuperTypeOfResult::createYes();
}

if ($a instanceof BenevolentUnionType) {
if (!$a->isSuperTypeOf($b)->no()) {
if (!$strict) {
if ($a instanceof MixedType && !$a instanceof TemplateType) {
return IsSuperTypeOfResult::createYes();
}
}

if ($b instanceof BenevolentUnionType) {
if (!$b->isSuperTypeOf($a)->no()) {
return IsSuperTypeOfResult::createYes();
if ($a instanceof BenevolentUnionType) {
if (!$a->isSuperTypeOf($b)->no()) {
return IsSuperTypeOfResult::createYes();
}
}
}

if ($b instanceof MixedType && !$b instanceof TemplateType) {
return IsSuperTypeOfResult::createYes();
if ($b instanceof BenevolentUnionType) {
if (!$b->isSuperTypeOf($a)->no()) {
return IsSuperTypeOfResult::createYes();
}
}

if ($b instanceof MixedType && !$b instanceof TemplateType) {
return IsSuperTypeOfResult::createYes();
}
}

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

if ($strict) {
if ($a instanceof MixedType && !$a instanceof TemplateType) {
return IsSuperTypeOfResult::createYes();
}

if ($a instanceof BenevolentUnionType && !$a->isSuperTypeOf($b)->no()) {
return IsSuperTypeOfResult::createYes();
}

if ($b instanceof BenevolentUnionType && !$b->isSuperTypeOf($a)->no()) {
return IsSuperTypeOfResult::createMaybe();
}

if ($b instanceof MixedType && !$b instanceof TemplateType) {
return IsSuperTypeOfResult::createMaybe();
}
}

$result = $a->equals($b);
$reasons = [];
if (!$result) {
Expand Down
135 changes: 135 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15198.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug15198;

use Exception;
use function PHPStan\Testing\assertType;

/** @template T */
class Foo
{

/** @param T $value */
public function __construct(
public mixed $value,
)
{
}

}

/** @template T */
interface FooInterface
{

}

/** @template-covariant T */
class CovariantFoo
{

}

/** @template-contravariant T */
class ContravariantFoo
{

}

/** @template T */
abstract class Option
{

}

/** @extends Option<mixed> */
final class None extends Option
{

}

function original(): void
{
try {
/** @var mixed */
$bar = 'bar';
$foo = new Foo($bar);
} catch (Exception $e) {
$foo = new Foo(123);
}

assertType('Bug15198\Foo<mixed>', $foo);
}

function ifElse(bool $c, mixed $bar): void
{
if ($c) {
$foo = new Foo($bar);
} else {
$foo = new Foo(123);
}

assertType('Bug15198\Foo<mixed>', $foo);
}

function ifElseReversed(bool $c, mixed $bar): void
{
if ($c) {
$foo = new Foo(123);
} else {
$foo = new Foo($bar);
}

assertType('Bug15198\Foo<mixed>', $foo);
}

/**
* @param Foo<mixed>|Foo<int> $a
* @param Foo<int>|Foo<mixed> $b
* @param CovariantFoo<mixed>|CovariantFoo<int> $c
* @param CovariantFoo<int>|CovariantFoo<mixed> $d
* @param ContravariantFoo<mixed>|ContravariantFoo<int> $e
* @param ContravariantFoo<int>|ContravariantFoo<mixed> $f
* @param FooInterface<mixed>|FooInterface<int> $g
* @param FooInterface<int>|FooInterface<mixed> $h
* @param class-string<Foo<mixed>>|class-string<Foo<int>> $i
* @param class-string<Foo<int>>|class-string<Foo<mixed>> $j
*/
function unions($a, $b, $c, $d, $e, $f, $g, $h, $i, $j): void
{
assertType('Bug15198\Foo<mixed>', $a);
assertType('Bug15198\Foo<mixed>', $b);
assertType('Bug15198\CovariantFoo<mixed>', $c);
assertType('Bug15198\CovariantFoo<mixed>', $d);
assertType('Bug15198\ContravariantFoo<int>', $e);
assertType('Bug15198\ContravariantFoo<int>', $f);
assertType('Bug15198\FooInterface<mixed>', $g);
assertType('Bug15198\FooInterface<mixed>', $h);
assertType('class-string<Bug15198\Foo<mixed>>', $i);
assertType('class-string<Bug15198\Foo<mixed>>', $j);
}

/**
* @param Foo<mixed>&Foo<int> $a
* @param Foo<int>&Foo<mixed> $b
*/
function intersections($a, $b): void
{
assertType('Bug15198\Foo<int>', $a);
assertType('Bug15198\Foo<int>', $b);
}

/**
* A class name written without type arguments has them resolved to the template
* bounds, so `None` stays compatible with `Option<string>`.
*
* @param None|Option<string> $a
* @param Option<string>|None $b
*/
function unparameterizedSubclass($a, $b): void
{
assertType('Bug15198\Option<string>', $a);
assertType('Bug15198\Option<string>', $b);
}
24 changes: 24 additions & 0 deletions tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -435,4 +435,28 @@ public function testBug13810(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13810.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11935(): void
{
// Below level 9 mixed is compatible with every type argument in both
// directions - that is how gradual typing works.
$this->analyse([__DIR__ . '/data/bug-11935.php'], []);
}

#[RequiresPhp('>= 8.0.0')]
public function testBug11935WithCheckExplicitMixed(): void
{
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/data/bug-11935.php'], [
[
'Parameter #1 of callable callable(Bug11935\Inv<A>): Bug11935\Inv<A> expects Bug11935\Inv<A>, Bug11935\Inv<mixed> given.',
34,
],
[
'Parameter #1 of callable callable(Bug11935\Inv<int>): void expects Bug11935\Inv<int>, Bug11935\Inv<mixed> given.',
43,
],
]);
}

}
44 changes: 44 additions & 0 deletions tests/PHPStan/Rules/Functions/data/bug-11935.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php // lint >= 8.0

declare(strict_types = 1);

namespace Bug11935;

/**
* @template A
*
* @param mixed $a Intentionally mixed
* @param callable(A): A $aa
* @return A
*/
function identity(mixed $a, callable $aa): mixed
{
return $aa($a);
}

/** @template T */
class Inv
{

}

/**
* @template A
*
* @param Inv<mixed> $a Intentionally Inv<mixed>
* @param callable(Inv<A>): Inv<A> $aa
* @return Inv<A>
*/
function identityInv(Inv $a, callable $aa): Inv
{
return $aa($a);
}

/**
* @param Inv<mixed> $a
* @param callable(Inv<int>): void $aa
*/
function passMixedTypeArgument(Inv $a, callable $aa): void
{
$aa($a);
}
25 changes: 25 additions & 0 deletions tests/PHPStan/Type/Generic/GenericObjectTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ public static function dataIsSuperTypeOf(): array
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
TrinaryLogic::createNo(),
],
// https://github.com/phpstan/phpstan/issues/11935 - `mixed` as a type
// argument stays compatible with any other one in the accepts context,
// but isSuperTypeOf() has to be a one-way relation, otherwise
// TypeCombinator would discard one of the two based on their order.
'same class, mixed type arg on the super side' => [
new GenericObjectType(A\A::class, [new MixedType(true)]),
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
TrinaryLogic::createYes(),
],
'same class, mixed type arg on the sub side' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
new GenericObjectType(A\A::class, [new MixedType(true)]),
TrinaryLogic::createMaybe(),
],
'same class, one naked' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
new ObjectType(A\A::class),
Expand Down Expand Up @@ -302,6 +316,17 @@ public static function dataAccepts(): array
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
TrinaryLogic::createNo(),
],
// https://github.com/phpstan/phpstan/issues/11935
'same class, mixed type arg on the accepting side' => [
new GenericObjectType(A\A::class, [new MixedType(true)]),
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
TrinaryLogic::createYes(),
],
'same class, mixed type arg on the accepted side' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
new GenericObjectType(A\A::class, [new MixedType(true)]),
TrinaryLogic::createYes(),
],
'same class, one naked' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
new ObjectType(A\A::class),
Expand Down
Loading
Loading