Skip to content

Commit

Permalink
Fix implicit @phpstan-assert PHPDoc inheritance with generics
Browse files Browse the repository at this point in the history
  • Loading branch information
RobertMe committed Feb 10, 2024
1 parent a7ff362 commit 813d15e
Show file tree
Hide file tree
Showing 6 changed files with 274 additions and 2 deletions.
8 changes: 6 additions & 2 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -931,8 +931,12 @@ private static function mergeAssertTags(array $assertTags, array $parents, array
$phpDocBlock = $parentPhpDocBlocks[$i];

return array_map(
static fn (AssertTag $assertTag) => $assertTag->withParameter(
$phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
static fn (AssertTag $assertTag) => self::resolveTemplateTypeInTag(
$assertTag->withParameter(
$phpDocBlock->transformAssertTagParameterWithParameterNameMapping($assertTag->getParameter()),
),
$phpDocBlock,
TemplateTypeVariance::createCovariant(),
),
$result,
);
Expand Down
3 changes: 3 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1427,6 +1427,9 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-interface-extends.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-extends.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10302-trait-implements.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/assert-inheritance.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-9123.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-10037.php');
}

/**
Expand Down
110 changes: 110 additions & 0 deletions tests/PHPStan/Analyser/data/assert-inheritance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

namespace AssertInheritance;

use function PHPStan\Testing\assertType;

/**
* @template T
*/
interface WrapperInterface
{
/**
* @phpstan-assert T $param
*/
public function assert(mixed $param): void;

/**
* @phpstan-assert-if-true T $param
*/
public function supports(mixed $param): bool;

/**
* @phpstan-assert-if-false T $param
*/
public function notSupports(mixed $param): bool;
}

/**
* @implements WrapperInterface<int>
*/
class IntWrapper implements WrapperInterface
{
public function assert(mixed $param): void
{
}

public function supports(mixed $param): bool
{
return is_int($param);
}

public function notSupports(mixed $param): bool
{
return !is_int($param);
}
}

/**
* @template T of object
* @implements WrapperInterface<T>
*/
abstract class ObjectWrapper implements WrapperInterface
{
}

/**
* @extends ObjectWrapper<\DateTimeInterface>
*/
class DateTimeInterfaceWrapper extends ObjectWrapper
{
public function assert(mixed $param): void
{
}

public function supports(mixed $param): bool
{
return $param instanceof \DateTimeInterface;
}

public function notSupports(mixed $param): bool
{
return !$param instanceof \DateTimeInterface;
}
}

function (IntWrapper $test, $val) {
if ($test->supports($val)) {
assertType('int', $val);
} else {
assertType('mixed~int', $val);
}

if ($test->notSupports($val)) {
assertType('mixed~int', $val);
} else {
assertType('int', $val);
}

assertType('mixed', $val);
$test->assert($val);
assertType('int', $val);
};

function (DateTimeInterfaceWrapper $test, $val) {
if ($test->supports($val)) {
assertType('DateTimeInterface', $val);
} else {
assertType('mixed~DateTimeInterface', $val);
}

if ($test->notSupports($val)) {
assertType('mixed~DateTimeInterface', $val);
} else {
assertType('DateTimeInterface', $val);
}

assertType('mixed', $val);
$test->assert($val);
assertType('DateTimeInterface', $val);
};
96 changes: 96 additions & 0 deletions tests/PHPStan/Analyser/data/bug-10037.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php declare(strict_types = 1);

namespace Bug10037;

interface Identifier
{}

interface Document
{}

/** @template T of Identifier */
interface Fetcher
{
/** @phpstan-assert-if-true T $identifier */
public function supports(Identifier $identifier): bool;

/** @param T $identifier */
public function fetch(Identifier $identifier): Document;
}

/** @implements Fetcher<PostIdentifier> */
final readonly class PostFetcher implements Fetcher
{
public function supports(Identifier $identifier): bool
{
return $identifier instanceof PostIdentifier;
}

public function fetch(Identifier $identifier): Document
{
// SA knows $identifier is instance of PostIdentifier here
return $identifier->foo();
}
}

class PostIdentifier implements Identifier
{
public function foo(): Document
{
return new class implements Document{};
}
}

function (Identifier $i): void {
$fetcher = new PostFetcher();
\PHPStan\Testing\assertType('Bug10037\Identifier', $i);
if ($fetcher->supports($i)) {
\PHPStan\Testing\assertType('Bug10037\PostIdentifier', $i);
$fetcher->fetch($i);
} else {
$fetcher->fetch($i);
}
};

class Post
{
}

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

/** @phpstan-assert-if-true T $subject */
abstract function supports(string $attribute, mixed $subject): bool;

/** @param T $subject */
abstract function voteOnAttribute(string $attribute, mixed $subject): bool;

}

/** @extends Voter<Post> */
class PostVoter extends Voter
{

/** @phpstan-assert-if-true Post $subject */
function supports(string $attribute, mixed $subject): bool
{

}

function voteOnAttribute(string $attribute, mixed $subject): bool
{
\PHPStan\Testing\assertType('Bug10037\Post', $subject);
}
}

function ($subject): void {
$voter = new PostVoter();
\PHPStan\Testing\assertType('mixed', $subject);
if ($voter->supports('aaa', $subject)) {
\PHPStan\Testing\assertType('Bug10037\Post', $subject);
$voter->voteOnAttribute('aaa', $subject);
} else {
$voter->voteOnAttribute('aaa', $subject);
}
};
53 changes: 53 additions & 0 deletions tests/PHPStan/Analyser/data/bug-9123.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php declare(strict_types=1);

namespace Bug9123;

interface Event {}

class MyEvent implements Event {}

/** @template T of Event */
interface EventListener
{
/** @phpstan-assert-if-true T $event */
public function canBeListen(Event $event): bool;

public function listen(Event $event): void;
}

/** @implements EventListener<MyEvent> */
final class Implementation implements EventListener
{
public function canBeListen(Event $event): bool
{
return $event instanceof MyEvent;
}

public function listen(Event $event): void
{
if (! $this->canBeListen($event)) {
return;
}

\PHPStan\Testing\assertType('Bug9123\MyEvent', $event);
}
}

/** @implements EventListener<MyEvent> */
final class Implementation2 implements EventListener
{
/** @phpstan-assert-if-true MyEvent $event */
public function canBeListen(Event $event): bool
{
return $event instanceof MyEvent;
}

public function listen(Event $event): void
{
if (! $this->canBeListen($event)) {
return;
}

\PHPStan\Testing\assertType('Bug9123\MyEvent', $event);
}
}
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/data/self-out.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,10 @@ function () {

$i->addData(321);
assertType('SelfOut\\a<int>', $i);

$i->addData(random_bytes(3));
assertType('SelfOut\\a<int|non-empty-string>', $i);

$i->setData(true);
assertType('SelfOut\\a<true>', $i);
};

0 comments on commit 813d15e

Please sign in to comment.