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
17 changes: 15 additions & 2 deletions src/Type/Generic/TemplateTypeVariance.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,11 +232,11 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo
}

if ($this->covariant()) {
return $a->isSuperTypeOf($b);
return self::compareTypeArguments($a, $b);
}

if ($this->contravariant()) {
return $b->isSuperTypeOf($a);
return self::compareTypeArguments($b, $a);
}

if ($this->bivariant()) {
Expand All @@ -246,6 +246,19 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo
throw new ShouldNotHappenException();
}

/**
* The "exactly this class" flavour of a `new Foo()` value cannot be written in a
* PHPDoc type argument, and `Foo` with the flavour is only a maybe-supertype of a
* plain `Foo`. Comparing the two directly would make a type argument carrying the
* flavour unmatchable against the very same written type - the invariant branch
* above does not have the problem because equals() ignores the flavour.
*/
private static function compareTypeArguments(Type $super, Type $sub): IsSuperTypeOfResult
{
return TemplateTypeHelper::removeFinalByKeywordOverrides($super)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is almost a one liner, this is not worth introducing a private method.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still running — I'll report once both complete.

->isSuperTypeOf(TemplateTypeHelper::removeFinalByKeywordOverrides($sub));
}

public function equals(self $other): bool
{
return $other->value === $this->value;
Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4452,6 +4452,14 @@ public function testBug8441(): void
}

#[RequiresPhp('>= 8.1.0')]
public function testBug15235(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-15235.php'], []);
}

public function testBug15166(): void
{
$this->checkThisOnly = false;
Expand Down
173 changes: 173 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15235.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15235;

/**
* @template T of object
*/
class A
{
/**
* @param T $object
*/
public function __construct(public object $object) {}
}

class B
{
/**
* @param A<covariant static> $a
*/
public function method(A $a): void {}

/**
* @param A<static> $a
*/
public function invariant(A $a): void {}

/**
* @param A<contravariant static> $a
*/
public function contravariant(A $a): void {}
}

class D
{
public function inconsistent(B $b): void
{
$b->method(new A($b));
}
}

function fromNew(): void
{
$b = new B();
$b->method(new A($b));
$b->invariant(new A($b));
$b->contravariant(new A($b));
}

function fromGetClass(B $b): void
{
if (get_class($b) !== B::class) {
return;
}

$b->method(new A($b));
}

/** @template-covariant T of object */
class Covariant
{
/** @param T $object */
public function __construct(public object $object) {}
}

/** @template-contravariant T of object */
class Contravariant
{
/** @param T $object */
public function set(object $object): void {}
}

class E
{
/** @param Covariant<static> $c */
public function declaredCovariant(Covariant $c): void {}

/** @param Contravariant<static> $c */
public function declaredContravariant(Contravariant $c): void {}

/** @param \Traversable<int, static> $it */
public function traversable(\Traversable $it): void {}

/** @return Covariant<static> */
public function makeCovariant(): Covariant
{
return new Covariant($this);
}

/** @return Wrapper<static> */
public function makeWrapper(): Wrapper
{
return new Wrapper($this);
}
}

class Consumer
{
/** @param Covariant<E> $c */
public function takesCovariant(Covariant $c): void {}

/** @param Wrapper<contravariant E> $wrapper */
public function takesContravariantWrapper(Wrapper $wrapper): void {}

/** @param Wrapper<Covariant<E>> $wrapper */
public function takesNested(Wrapper $wrapper): void {}
}

/**
* @param Covariant<E> $covariant
* @param Contravariant<E> $contravariant
* @param \Traversable<int, E> $traversable
*/
function declaredVariances(Covariant $covariant, Contravariant $contravariant, \Traversable $traversable): void
{
$e = new E();
$e->declaredCovariant($covariant);
$e->declaredContravariant($contravariant);
$e->traversable($traversable);
}

function flavourOnTheArgumentSide(Consumer $consumer): void
{
$e = new E();
$consumer->takesCovariant($e->makeCovariant());
$consumer->takesContravariantWrapper($e->makeWrapper());
}

/** @template T of object */
class Wrapper
{
/** @param T $object */
public function __construct(public object $object) {}
}

function nestedInGenericType(Consumer $consumer): void
{
$e = new E();
$consumer->takesNested(new Wrapper($e->makeCovariant()));
}

class Composite
{
/** @param Covariant<static>|null $c */
public function inUnion(?Covariant $c): void {}

/** @param array<int, Covariant<static>> $c */
public function inArray(array $c): void {}

/** @param Covariant<Covariant<static>> $c */
public function nested(Covariant $c): void {}

/** @param Wrapper<Covariant<static>> $c */
public function nestedInvariant(Wrapper $c): void {}
}

/**
* @param Covariant<Composite> $covariant
* @param array<int, Covariant<Composite>> $array
* @param Covariant<Covariant<Composite>> $nested
* @param Wrapper<Covariant<Composite>> $nestedInvariant
*/
function compositeShapes(Covariant $covariant, array $array, Covariant $nested, Wrapper $nestedInvariant): void
{
$composite = new Composite();
$composite->inUnion($covariant);
$composite->inUnion(null);
$composite->inArray($array);
$composite->nested($nested);
$composite->nestedInvariant($nestedInvariant);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,11 @@ public function testBug8441(): void
}

#[RequiresPhp('>= 8.1.0')]
public function testBug15235(): void
{
$this->analyse([__DIR__ . '/data/bug-15235.php'], []);
}

public function testBug15166(): void
{
$this->analyse([__DIR__ . '/data/bug-15166.php'], []);
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Rules/Properties/data/bug-15235.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace Bug15235Properties;

/**
* @template T of object
*/
class A
{
/**
* @param T $object
*/
public function __construct(public object $object) {}
}

/** @template-covariant T of object */
class Covariant
{
/** @param T $object */
public function __construct(public object $object) {}
}

class B
{
/** @var A<covariant static>|null */
public ?A $callSiteVariance = null;

/** @var Covariant<static>|null */
public ?Covariant $declaredVariance = null;
}

function fromParameter(B $b): void
{
$b->callSiteVariance = new A($b);
$b->declaredVariance = new Covariant($b);
}

function fromNew(): void
{
$b = new B();
$b->callSiteVariance = new A($b);
$b->declaredVariance = new Covariant($b);
}
Loading