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
4 changes: 0 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,10 +399,6 @@ When adding or editing PHPDoc comments in this codebase, follow these guidelines
- Use imperative voice without "Returns the..." preambles when a brief note suffices. Prefer `/** Replaces unresolved TemplateTypes with their bounds. */` over a multi-line block.
- Preserve `@api` and type tags on their own lines, with no redundant description alongside them.

### UnionTypeMethodReflection and IntersectionTypeMethodReflection parity

When methods are called on union types (`Foo|Bar`), the resolved method reflection is a `UnionTypeMethodReflection` that wraps the individual method reflections. Similarly, `IntersectionTypeMethodReflection` handles intersection types. These two classes must maintain feature parity for things like `getAsserts()`, `getSelfOutType()`, etc. When one class correctly combines member data (e.g. `IntersectionTypeMethodReflection::getAsserts()` iterating over methods and calling `intersectWith()`), the other should do the same rather than returning empty/null. The `Assertions::intersectWith()` method merges assertion tag lists from multiple sources.

## Important dependencies

- `nikic/php-parser` ^5.7.0 - PHP AST parsing
Expand Down
75 changes: 69 additions & 6 deletions src/Reflection/Assertions.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDoc\Tag\AssertTag;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_filter;
use function array_map;
use function array_merge;
use function count;
use function sprintf;

/**
* Collection of @phpstan-assert annotations on a function or method.
Expand Down Expand Up @@ -91,12 +93,76 @@ public function mapTypes(callable $callable): self
{
$assertTagsCallback = static fn (AssertTag $tag): AssertTag => $tag->withType($callable($tag->getType()));

return new self(array_map($assertTagsCallback, $this->asserts));
return self::create(array_map($assertTagsCallback, $this->asserts));
}

/**
* @deprecated use union() or intersect() instead
*/
public function intersectWith(Assertions $other): self
{
return new self(array_merge($this->getAll(), $other->getAll()));
return $this->union($other);
}

public function union(Assertions $other): self
{
if ($this === self::$empty) {
return $other;
}
if ($other === self::$empty) {
return $this;
}

return self::create(array_merge($this->getAll(), $other->getAll()));
}

public function intersect(Assertions $other): self
{
if ($this === self::$empty) {
return $other;
}
if ($other === self::$empty) {
return $this;
}

$otherAsserts = $other->getAll();
$thisAsserts = $this->getAll();

$merged = [];
foreach ($thisAsserts as $thisAssert) {
$key = self::getAssertKey($thisAssert);

foreach ($otherAsserts as $otherAssert) {
if (self::getAssertKey($otherAssert) !== $key) {
continue;
}

$merged[] = $thisAssert->withType(TypeCombinator::union($thisAssert->getType(), $otherAssert->getType()));
}
}

return self::create($merged);
}

private static function getAssertKey(AssertTag $assert): string
{
return sprintf(
'%s-%s-%s',
$assert->getParameter()->describe(),
$assert->getIf(),
$assert->isNegated() ? '1' : '0',
);
}

/**
* @param AssertTag[] $asserts
*/
private static function create(array $asserts): self
{
if (count($asserts) === 0) {
return self::createEmpty();
}
return new self($asserts);
}

public static function createEmpty(): self
Expand All @@ -116,11 +182,8 @@ public static function createEmpty(): self
public static function createFromResolvedPhpDocBlock(ResolvedPhpDocBlock $phpDocBlock): self
{
$tags = $phpDocBlock->getAssertTags();
if (count($tags) === 0) {
return self::createEmpty();
}

return new self($tags);
return self::create($tags);
}

}
2 changes: 1 addition & 1 deletion src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ public function getAsserts(): Assertions
$assertions = Assertions::createEmpty();

foreach ($this->methods as $method) {
$assertions = $assertions->intersectWith($method->getAsserts());
$assertions = $assertions->union($method->getAsserts());
}

return $assertions;
Expand Down
2 changes: 1 addition & 1 deletion src/Reflection/Type/UnionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ public function getAsserts(): Assertions
$assertions = Assertions::createEmpty();

foreach ($this->methods as $method) {
$assertions = $assertions->intersectWith($method->getAsserts());
$assertions = $assertions->intersect($method->getAsserts());
}

return $assertions;
Expand Down
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14108.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php // lint >= 8.0

namespace Bug14108;

use function PHPStan\Testing\assertType;

class Foo
{
public function __construct(private ?string $param)
{
}

public function getParam(): ?string
{
return $this->param;
}

/**
* @phpstan-assert string $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

class Bar
{
public function __construct(private ?int $param)
{
}

public function getParam(): ?int
{
return $this->param;
}

/**
* @phpstan-assert int $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

function test(Foo|Bar $fooOrBar): void
{
assertType('int|string|null', $fooOrBar->getParam());

$fooOrBar->narrowGetParam();

assertType('int|string', $fooOrBar->getParam());
}
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14108b.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php // lint >= 8.0

namespace Bug14108b;

use function PHPStan\Testing\assertType;

class Foo
{
public function __construct(private ?string $param)
{
}

public function getParam(): ?string
{
return $this->param;
}

/**
* @phpstan-assert !null $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

class Bar
{
public function __construct(private ?int $param)
{
}

public function getParam(): ?int
{
return $this->param;
}

/**
* @phpstan-assert int $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

function test(Foo|Bar $fooOrBar): void
{
assertType('int|string|null', $fooOrBar->getParam());

$fooOrBar->narrowGetParam();

assertType('int|string|null', $fooOrBar->getParam()); // could be 'int|string'
}
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14108c.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php // lint >= 8.0

namespace Bug14108c;

use function PHPStan\Testing\assertType;

class Foo
{
public function __construct(private ?string $param)
{
}

public function getParam(): ?string
{
return $this->param;
}

/**
* @phpstan-assert =string $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

class Bar
{
public function __construct(private ?int $param)
{
}

public function getParam(): ?int
{
return $this->param;
}

/**
* @phpstan-assert int $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

function test(Foo|Bar $fooOrBar): void
{
assertType('int|string|null', $fooOrBar->getParam());

$fooOrBar->narrowGetParam();

assertType('int|string', $fooOrBar->getParam());
}
59 changes: 59 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14108d.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php // lint >= 8.0

namespace Bug14108d;

use function PHPStan\Testing\assertType;

class Foo
{
public function __construct(private ?string $param)
{
}

public function getParam(): ?string
{
return $this->param;
}

/**
* @phpstan-assert-if-true string $this->getParam()
*/
public function narrowGetParam(): bool
{
}
}

class Bar
{
public function __construct(private ?int $param)
{
}

public function getParam(): ?int
{
return $this->param;
}

/**
* @phpstan-assert int $this->getParam()
*/
public function narrowGetParam(): void
{
}
}

function test(Foo|Bar $fooOrBar): void
{
assertType('int|string|null', $fooOrBar->getParam());

$fooOrBar->narrowGetParam();

assertType('int|string|null', $fooOrBar->getParam());

if ($fooOrBar->narrowGetParam()) {
assertType('int|string|null', $fooOrBar->getParam()); // could be 'int|string'
} else {
assertType('int|string|null', $fooOrBar->getParam());
}
assertType('int|string|null', $fooOrBar->getParam());
}
Loading