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: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,10 @@ 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
8 changes: 7 additions & 1 deletion src/Reflection/Type/UnionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,13 @@ public function getDocComment(): ?string

public function getAsserts(): Assertions
{
return Assertions::createEmpty();
$assertions = Assertions::createEmpty();

foreach ($this->methods as $method) {
$assertions = $assertions->intersectWith($method->getAsserts());
Copy link
Contributor

Choose a reason for hiding this comment

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

looking at this implementation and the tests, I think we miss test coverage for the case when asserts do not intersect.

maybe thats someting we could enhance the prompt about so more test cases are generated

Copy link
Contributor

Choose a reason for hiding this comment

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

alternatively thats something which could be detected by infection and claude could run mutation tests (which could make the whole process slow though)

}

return $assertions;
}

public function acceptsNamedArguments(): TrinaryLogic
Expand Down
58 changes: 58 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-11441.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php // lint >= 8.0

namespace Bug11441;

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 checkNotNull(): void
{
if ($this->getParam() === null) {
throw new \Exception();
}
}
}

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

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

/**
* @phpstan-assert !null $this->getParam()
*/
public function checkNotNull(): void
{
if ($this->getParam() === null) {
throw new \Exception();
}
}
}

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

$fooOrBar->checkNotNull();

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

namespace Bug13358;

use function PHPStan\Testing\assertType;

/**
* @phpstan-sealed SystemActor|AnonymousVisitorActor
*/
abstract class Actor
{
/**
* @phpstan-assert-if-true SystemActor $this
*/
public function isSystem() : bool
{
return $this instanceof SystemActor;
}

/**
* @phpstan-assert-if-true AnonymousVisitorActor $this
*/
public function isAnonymousVisitor() : bool
{
return $this instanceof AnonymousVisitorActor;
}
}

class SystemActor extends Actor
{
}

class AnonymousVisitorActor extends Actor
{
}

function (AnonymousVisitorActor|SystemActor $actor): void {
assertType('Bug13358\AnonymousVisitorActor|Bug13358\SystemActor', $actor);

if ($actor->isSystem()) {
assertType('Bug13358\SystemActor', $actor);
} else {
assertType('Bug13358\AnonymousVisitorActor', $actor);
}
};
Loading