Skip to content
Closed
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
12 changes: 12 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1437,12 +1437,24 @@ parameters:
count: 1
path: src/Type/IntersectionType.php

-
rawMessage: Doing instanceof PHPStan\Type\Generic\GenericObjectType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 5
path: src/Type/IntersectionType.php

-
rawMessage: Doing instanceof PHPStan\Type\IntersectionType is error-prone and deprecated.
identifier: phpstanApi.instanceofType
count: 3
path: src/Type/IntersectionType.php

-
rawMessage: 'Doing instanceof PHPStan\Type\ObjectType is error-prone and deprecated. Use Type::isObject() or Type::getObjectClassNames() instead.'
identifier: phpstanApi.instanceofType
count: 3
path: src/Type/IntersectionType.php

-
rawMessage: 'Method PHPStan\Type\IntersectionType::getConstantArrays() should return list<PHPStan\Type\Constant\ConstantArrayType> but returns array{PHPStan\Type\Type}.'
identifier: return.type
Expand Down
69 changes: 69 additions & 0 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Enum\EnumCaseObjectType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateArrayType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
Expand Down Expand Up @@ -669,6 +670,10 @@
continue;
}

if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithPropertyInIntersection($type, $propertyName, false)) {
continue;
}

$propertyPrototypes[] = $type->getUnresolvedInstancePropertyPrototype($propertyName, $scope)->withFechedOnType($this);
}

Expand Down Expand Up @@ -702,6 +707,10 @@
continue;
}

if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithPropertyInIntersection($type, $propertyName, true)) {
continue;
}

$propertyPrototypes[] = $type->getUnresolvedStaticPropertyPrototype($propertyName, $scope)->withFechedOnType($this);
}

Expand Down Expand Up @@ -740,6 +749,10 @@
continue;
}

if ($type instanceof ObjectType && !$type instanceof GenericObjectType && $this->hasGenericAncestorWithMethodInIntersection($type, $methodName)) {
continue;
}

$methodPrototypes[] = $type->getUnresolvedMethodPrototype($methodName, $scope)->withCalledOnType($this);
}

Expand All @@ -755,6 +768,62 @@
return new IntersectionTypeUnresolvedMethodPrototypeReflection($methodName, $methodPrototypes);
}

private function hasGenericAncestorWithMethodInIntersection(ObjectType $type, string $methodName): bool
{
$nakedReflection = $type->getNakedClassReflection();
if ($nakedReflection === null || !$nakedReflection->isGeneric()) {
return false;
}

foreach ($this->types as $otherType) {
if ($otherType === $type) {
continue;
}
if (!$otherType instanceof GenericObjectType) {
continue;
}
if ($type->getAncestorWithClassName($otherType->getClassName()) === null) {
continue;
}
if ($otherType->hasMethod($methodName)->yes()) {

Check warning on line 788 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type->getAncestorWithClassName($otherType->getClassName()) === null) { continue; } - if ($otherType->hasMethod($methodName)->yes()) { + if (!$otherType->hasMethod($methodName)->no()) { return true; } }

Check warning on line 788 in src/Type/IntersectionType.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($type->getAncestorWithClassName($otherType->getClassName()) === null) { continue; } - if ($otherType->hasMethod($methodName)->yes()) { + if (!$otherType->hasMethod($methodName)->no()) { return true; } }
return true;
}
}

return false;
}

private function hasGenericAncestorWithPropertyInIntersection(ObjectType $type, string $propertyName, bool $static): bool
{
$nakedReflection = $type->getNakedClassReflection();
if ($nakedReflection === null || !$nakedReflection->isGeneric()) {
return false;
}

foreach ($this->types as $otherType) {
if ($otherType === $type) {
continue;
}
if (!$otherType instanceof GenericObjectType) {
continue;
}
if ($type->getAncestorWithClassName($otherType->getClassName()) === null) {
continue;
}
if ($static) {
if ($otherType->hasStaticProperty($propertyName)->yes()) {
return true;
}
} else {
if ($otherType->hasInstanceProperty($propertyName)->yes()) {
return true;
}
}
}

return false;
}

public function canAccessConstants(): TrinaryLogic
{
return $this->intersectResults(static fn (Type $type): TrinaryLogic => $type->canAccessConstants());
Expand Down
172 changes: 172 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-5357.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php declare(strict_types = 1);

namespace Bug5357;

use function PHPStan\Testing\assertType;

/**
* @phpstan-template T of object
*/
interface AdminInterface {}

/**
* @phpstan-template T of ProxyQueryInterface
*/
interface PagerInterface {}

/**
* @phpstan-template T of ProxyQueryInterface
* @phpstan-implements PagerInterface<T>
*/
class Pager implements PagerInterface
{}

/**
* @phpstan-template T of ProxyQueryInterface
*/
interface DatagridInterface
{
/**
* @phpstan-return PagerInterface<T>
*/
public function getPager(): PagerInterface;
}

/**
* @phpstan-template T of ProxyQueryInterface
* @phpstan-implements DatagridInterface<T>
*/
class Datagrid implements DatagridInterface
{
/**
* @phpstan-return PagerInterface<T>
*/
public function getPager(): PagerInterface
{
throw new \Exception();
}

/**
* Method unique to Datagrid (not on DatagridInterface)
* @phpstan-return T
*/
public function getQuery(): ProxyQueryInterface
{
throw new \Exception();
}
}

interface ProxyQueryInterface {}

class Proxy implements ProxyQueryInterface {}

class MockObject {}

/**
* @phpstan-template T of ProxyQueryInterface
* @phpstan-extends Datagrid<T>
*/
class ChildDatagrid extends Datagrid
{
}

/**
* @phpstan-template T of ProxyQueryInterface
*/
interface DatagridBuilderInterface
{
/**
* @param AdminInterface<object> $admin
* @param array<string, mixed> $values
*
* @phpstan-return DatagridInterface<T>
*/
public function getBaseDatagrid(AdminInterface $admin, array $values = []): DatagridInterface;
}

/**
* @phpstan-implements DatagridBuilderInterface<Proxy>
*/
class DatagridBuilder implements DatagridBuilderInterface
{
/**
* @param AdminInterface<object> $admin
* @param array<string, mixed> $values
* @phpstan-return DatagridInterface<Proxy>
*/
public function getBaseDatagrid(AdminInterface $admin, array $values = []): DatagridInterface
{
throw new \Exception();
}
}

class HelloWorld
{
/**
* @var MockObject&AdminInterface<object>
*/
public $admin;

public DatagridBuilder $datagridBuilder;

public function sayHello(): void
{
$datagrid = $this->datagridBuilder->getBaseDatagrid($this->admin);
assert($datagrid instanceof Datagrid);
assertType('Bug5357\Datagrid&Bug5357\DatagridInterface<Bug5357\Proxy>', $datagrid);
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* @param Datagrid&DatagridInterface<Proxy> $datagrid
*/
public function testDirect($datagrid): void
{
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* @param Datagrid<Proxy> $datagrid
*/
public function testGeneric($datagrid): void
{
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* Test with parent class generic intersection (not interface)
* @param ChildDatagrid&Datagrid<Proxy> $datagrid
*/
public function testWithGenericParentClass($datagrid): void
{
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* Test with multiple generic levels in hierarchy
* @param ChildDatagrid&DatagridInterface<Proxy> $datagrid
*/
public function testWithGrandparentInterface($datagrid): void
{
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* When both types already have explicit generics, existing intersection behavior should be preserved
* @param Datagrid<Proxy>&DatagridInterface<Proxy> $datagrid
*/
public function testBothGeneric($datagrid): void
{
assertType('Bug5357\PagerInterface<Bug5357\Proxy>', $datagrid->getPager());
}

/**
* Method unique to Datagrid should still resolve to bounds when raw type is skipped
* (it won't be skipped because DatagridInterface doesn't have getQuery)
* @param Datagrid&DatagridInterface<Proxy> $datagrid
*/
public function testUniqueMethod($datagrid): void
{
assertType('Bug5357\ProxyQueryInterface', $datagrid->getQuery());
}
}
Loading