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
2 changes: 2 additions & 0 deletions src/Type/Generic/GenericObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@
$ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant();
if (!$thisVariance->invariant()) {
$results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]);
} elseif ($acceptsContext && $templateType->getVariance()->invariant() && $this->types[$i]->isSuperTypeOf($ancestor->types[$i])->yes()) {

Check warning on line 193 in src/Type/Generic/GenericObjectType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); if (!$thisVariance->invariant()) { $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]); - } elseif ($acceptsContext && $templateType->getVariance()->invariant() && $this->types[$i]->isSuperTypeOf($ancestor->types[$i])->yes()) { + } elseif ($acceptsContext && $templateType->getVariance()->invariant() && !$this->types[$i]->isSuperTypeOf($ancestor->types[$i])->no()) { $results[] = IsSuperTypeOfResult::createYes(); } else { $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);

Check warning on line 193 in src/Type/Generic/GenericObjectType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $ancestorVariance = $ancestor->variances[$i] ?? TemplateTypeVariance::createInvariant(); if (!$thisVariance->invariant()) { $results[] = $thisVariance->isValidVariance($templateType, $this->types[$i], $ancestor->types[$i]); - } elseif ($acceptsContext && $templateType->getVariance()->invariant() && $this->types[$i]->isSuperTypeOf($ancestor->types[$i])->yes()) { + } elseif ($acceptsContext && $templateType->getVariance()->invariant() && !$this->types[$i]->isSuperTypeOf($ancestor->types[$i])->no()) { $results[] = IsSuperTypeOfResult::createYes(); } else { $results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
$results[] = IsSuperTypeOfResult::createYes();
} else {
$results[] = $templateType->isValidVariance($this->types[$i], $ancestor->types[$i]);
}
Expand Down
13 changes: 13 additions & 0 deletions tests/PHPStan/Rules/Functions/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,17 @@ public function testBug12397(): void
$this->analyse([__DIR__ . '/data/bug-12397.php'], []);
}

#[RequiresPhp('>= 8.2')]
public function testBug10290(): void
{
$this->checkNullables = true;
$this->checkExplicitMixed = false;
$this->analyse([__DIR__ . '/data/bug-10290.php'], [
[
'Function Bug10290\g() should return Bug10290\Err<string>|Bug10290\Ok<true> but returns Bug10290\Ok<bool>.',
56,
],
]);
}

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

declare(strict_types = 1);

namespace Bug10290;

/** @template T */
abstract class Result
{
}

/** @template T */
final readonly class Ok extends Result
{
/** @param T $data */
public function __construct(public mixed $data)
{
}
}

/** @template E */
final readonly class Err extends Result
{
/** @param E $data */
public function __construct(public mixed $data)
{
}
}

/**
* @return Ok<non-empty-string>|Err<array<mixed>>
*/
function f(string $json): Result
{
$data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
assert(is_array($data));

if (isset($data['has_error']) && $data['has_error']) {
return new Err($data);
}

$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
if ($email === false) {
return new Err($data);
}

return new Ok($email);
}

/**
* @return Ok<true>|Err<string>
*/
function g(): Result
{
if (rand() === 1) {
return new Ok(true);
}

return new Err('error');
}
46 changes: 2 additions & 44 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2220,13 +2220,7 @@ public function testGenericObjectLowerBound(): void
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], [
[
'Parameter #1 $c of method GenericObjectLowerBound\Foo::doFoo() expects GenericObjectLowerBound\Collection<GenericObjectLowerBound\Cat|GenericObjectLowerBound\Dog>, GenericObjectLowerBound\Collection<GenericObjectLowerBound\Dog> given.',
48,
'Template type T on class GenericObjectLowerBound\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
]);
$this->analyse([__DIR__ . '/../../Analyser/nsrt/generic-object-lower-bound.php'], []);
}

public function testNonEmptyStringVerbosity(): void
Expand Down Expand Up @@ -2255,33 +2249,7 @@ public function testBug5372(): void
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-5372.php'], [
[
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, non-falsy-string> given.',
64,
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, class-string> given.',
68,
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, class-string> given.',
72,
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, literal-string> given.',
81,
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $list of method Bug5372\Foo::takesStrings() expects Bug5372\Collection<int, string>, Bug5372\Collection<int, literal-string> given.',
85,
'Template type T on class Bug5372\Collection is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
]);
$this->analyse([__DIR__ . '/data/bug-5372.php'], []);
}

public function testLiteralString(): void
Expand Down Expand Up @@ -2641,11 +2609,6 @@ public function testGenericVariance(): void
'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant<GenericVarianceCall\B>, GenericVarianceCall\Invariant<GenericVarianceCall\A> given.',
45,
],
[
'Parameter #1 $param of method GenericVarianceCall\Foo::invariant() expects GenericVarianceCall\Invariant<GenericVarianceCall\B>, GenericVarianceCall\Invariant<GenericVarianceCall\C> given.',
53,
'Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Parameter #1 $param of method GenericVarianceCall\Foo::covariant() expects GenericVarianceCall\Covariant<GenericVarianceCall\B>, GenericVarianceCall\Covariant<GenericVarianceCall\A> given.',
60,
Expand All @@ -2654,11 +2617,6 @@ public function testGenericVariance(): void
'Parameter #1 $param of method GenericVarianceCall\Foo::contravariant() expects GenericVarianceCall\Contravariant<GenericVarianceCall\B>, GenericVarianceCall\Contravariant<GenericVarianceCall\C> given.',
83,
],
[
'Parameter #1 $param of method GenericVarianceCall\Foo::invariantArray() expects array{GenericVarianceCall\Invariant<GenericVarianceCall\B>}, array{GenericVarianceCall\Invariant<GenericVarianceCall\C>} given.',
97,
'Offset 0 (GenericVarianceCall\Invariant<GenericVarianceCall\B>) does not accept type GenericVarianceCall\Invariant<GenericVarianceCall\C>: Template type T on class GenericVarianceCall\Invariant is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
]);
}

Expand Down
23 changes: 1 addition & 22 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -469,28 +469,7 @@ public function testInferArrayKey(): void

public function testBug4590(): void
{
$this->analyse([__DIR__ . '/data/bug-4590.php'], [
[
'Method Bug4590\OkResponse::testGenericStatic() should return static(Bug4590\OkResponse<array<string, string>>) but returns static(Bug4590\OkResponse<array{ok: string}>).',
36,
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Method Bug4590\\Controller::test1() should return Bug4590\\OkResponse<array<string, string>> but returns Bug4590\\OkResponse<array{ok: string}>.',
47,
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Method Bug4590\\Controller::test2() should return Bug4590\\OkResponse<array<int, string>> but returns Bug4590\\OkResponse<array{string}>.',
55,
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
[
'Method Bug4590\\Controller::test3() should return Bug4590\\OkResponse<array<string>> but returns Bug4590\\OkResponse<array{string}>.',
63,
'Template type T on class Bug4590\OkResponse is not covariant. Learn more: <fg=cyan>https://phpstan.org/blog/whats-up-with-template-covariant</>',
],
]);
$this->analyse([__DIR__ . '/data/bug-4590.php'], []);
}

public function testTemplateStringBound(): void
Expand Down
16 changes: 13 additions & 3 deletions tests/PHPStan/Type/Generic/GenericObjectTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ public static function dataAccepts(): array
'same class, different type args' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
new GenericObjectType(A\A::class, [new ObjectType('DateTime')]),
TrinaryLogic::createNo(),
TrinaryLogic::createYes(),
],
'same class, one naked' => [
new GenericObjectType(A\A::class, [new ObjectType('DateTimeInterface')]),
Expand All @@ -310,7 +310,7 @@ public static function dataAccepts(): array
'implementation with @extends with different type args' => [
new GenericObjectType(B\I::class, [new ObjectType('DateTimeInterface')]),
new GenericObjectType(B\IImpl::class, [new ObjectType('DateTime')]),
TrinaryLogic::createNo(),
TrinaryLogic::createYes(),
],
'generic object accepts normal object of same type' => [
new GenericObjectType(Traversable::class, [new MixedType(true), new ObjectType('DateTimeInterface')]),
Expand All @@ -330,8 +330,18 @@ public static function dataAccepts(): array
];
}

public static function dataAcceptsTypeProjections(): array
{
$data = self::dataTypeProjections();
// In accepts context, invariant generics accept subtypes (covariant behavior)
// Entry #2: [$invariantB, $invariantC, No] → Yes because C extends B
$data[2][2] = TrinaryLogic::createYes();

return $data;
}

#[DataProvider('dataAccepts')]
#[DataProvider('dataTypeProjections')]
#[DataProvider('dataAcceptsTypeProjections')]
public function testAccepts(
Type $acceptingType,
Type $acceptedType,
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Type/StaticTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public static function dataAccepts(): iterable
new StringType(),
])], null, []),
new GenericStaticType($c, [new IntegerType()], null, []),
TrinaryLogic::createNo(),
TrinaryLogic::createYes(),
];

yield [
Expand Down
Loading