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
11 changes: 6 additions & 5 deletions src/Type/Generic/TemplateTypeVariance.php
Original file line number Diff line number Diff line change
Expand Up @@ -233,12 +233,13 @@ public function isValidVariance(TemplateType $templateType, Type $a, Type $b, bo
return new IsSuperTypeOfResult(TrinaryLogic::createFromBoolean($result), $reasons);
}

if ($this->covariant()) {
return $a->isSuperTypeOf($b);
}
if ($this->covariant() || $this->contravariant()) {
// The "exactly this class" flavour of a `new Foo()` value cannot be written
// in a PHPDoc type argument - ignore it like equals() does for invariance.
$a = TemplateTypeHelper::removeFinalByKeywordOverrides($a);
$b = TemplateTypeHelper::removeFinalByKeywordOverrides($b);

if ($this->contravariant()) {
return $b->isSuperTypeOf($a);
return $this->covariant() ? $a->isSuperTypeOf($b) : $b->isSuperTypeOf($a);
}

if ($this->bivariant()) {
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Rules/Functions/CallCallablesRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,12 @@ public function testBug15442(): void
$this->analyse([__DIR__ . '/data/bug-15442.php'], []);
}

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

#[RequiresPhp('>= 8.0.0')]
public function testBug11935WithCheckExplicitMixed(): void
{
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4537,6 +4537,15 @@ public function testBug15166(): void
$this->analyse([__DIR__ . '/data/bug-15166.php'], []);
}

#[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 testBug6732(): void
{
$this->checkThisOnly = false;
Expand Down
7 changes: 7 additions & 0 deletions tests/PHPStan/Rules/Methods/CallStaticMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1158,4 +1158,11 @@ public function testBug11421(): void
]);
}

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

}
182 changes: 182 additions & 0 deletions tests/PHPStan/Rules/Methods/data/bug-15235.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
<?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<covariant static> $a
*/
public static function staticMethod(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));
$b::staticMethod(new A($b));

$callable = $b->method(...);
$callable(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 @@ -1101,6 +1101,12 @@ public function testBug15166(): void
$this->analyse([__DIR__ . '/data/bug-15166.php'], []);
}

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

public function testBug6732(): void
{
$this->analyse([__DIR__ . '/data/bug-6732.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);
}
5 changes: 5 additions & 0 deletions turbo-ext/src/TemplateTypeHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,11 @@ zv::Val pt_type_template_type_helper_to_argument(zval *type)
return TemplateTypeHelper::toArgument(type);
}

zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type)
{
return TemplateTypeHelper::removeFinalByKeywordOverrides(type);
}

/* }}} */

/* {{{ engine ABI glue: parameter parsing + registration */
Expand Down
14 changes: 11 additions & 3 deletions turbo-ext/src/TemplateTypeVariance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -436,9 +436,17 @@ zv::Val TemplateTypeVariance::isValidVariance(zval *templateType, zval *a, zval
return pt_type_new_ce(pt_ce_is_super_type_of_result, 2, args);
}

if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(a), PT_OP_IS_SUPER_TYPE_OF, 1, b);

if (thisValue == CONTRAVARIANT) return pt_type_op(Z_OBJ_P(b), PT_OP_IS_SUPER_TYPE_OF, 1, a);
if (thisValue == COVARIANT || thisValue == CONTRAVARIANT) {
/* the "exactly this class" flavour of a `new Foo()` value cannot be
* written in a PHPDoc type argument - ignore it like equals() does
* for invariance */
zv::Val strippedA = pt_type_template_type_helper_remove_final_by_keyword_overrides(a);
if (UNEXPECTED(strippedA.isUndef())) return zv::Val();
zv::Val strippedB = pt_type_template_type_helper_remove_final_by_keyword_overrides(b);
if (UNEXPECTED(strippedB.isUndef())) return zv::Val();
if (thisValue == COVARIANT) return pt_type_op(Z_OBJ_P(strippedA.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedB.raw());
return pt_type_op(Z_OBJ_P(strippedB.raw()), PT_OP_IS_SUPER_TYPE_OF, 1, strippedA.raw());
}

if (thisValue == BIVARIANT) return pt_type_is_super_type_of_result(PT_TRI_YES);

Expand Down
5 changes: 3 additions & 2 deletions turbo-ext/src/TypeTraits.h
Original file line number Diff line number Diff line change
Expand Up @@ -946,12 +946,13 @@ zv::Val pt_type_template_type_variance_map_new(zval *variances);
zv::Val pt_type_template_type_map_empty();
zv::Val pt_type_template_type_map_new(zval *types, zval *lowerBoundTypes = NULL);
/* TemplateTypeHelper::resolveTemplateTypes() / resolveToDefaults() /
* resolveToBounds() / toArgument() (the arguments borrowed); UNDEF =
* pending exception */
* resolveToBounds() / toArgument() / removeFinalByKeywordOverrides() (the
* arguments borrowed); UNDEF = pending exception */
zv::Val pt_type_template_type_helper_resolve_template_types(zval *type, zval *standins, zval *callSiteVariances, zval *positionVariance, bool keepErrorTypes);
zv::Val pt_type_template_type_helper_resolve_to_defaults(zval *type);
zv::Val pt_type_template_type_helper_resolve_to_bounds(zval *type);
zv::Val pt_type_template_type_helper_to_argument(zval *type);
zv::Val pt_type_template_type_helper_remove_final_by_keyword_overrides(zval *type);
/* $reference->getType() / ->getPositionVariance() of a reference instance
* (TemplateTypeReference.cpp): the slots of a native instance, the methods
* of anything else (the PHP twin declared next to the native class in the
Expand Down
Loading