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
53 changes: 43 additions & 10 deletions src/Reflection/Type/IntersectionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_map;
use function array_merge;
use function count;
use function implode;
use function is_bool;
Expand Down Expand Up @@ -80,20 +82,26 @@ public function getPrototype(): ClassMemberReflection

public function getVariants(): array
{
$allVariants = array_merge(...array_map(
static fn (MethodReflection $method) => $method->getVariants(),
$this->methods,
));
$combined = ParametersAcceptorSelector::combineAcceptors($allVariants);

$returnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getReturnType(), $method->getVariants())), $this->methods));
$phpDocReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getPhpDocReturnType(), $method->getVariants())), $this->methods));
$nativeReturnType = TypeCombinator::intersect(...array_map(static fn (MethodReflection $method): Type => TypeCombinator::intersect(...array_map(static fn (ParametersAcceptor $acceptor): Type => $acceptor->getNativeReturnType(), $method->getVariants())), $this->methods));

return array_map(static fn (ExtendedParametersAcceptor $acceptor): ExtendedParametersAcceptor => new ExtendedFunctionVariant(
$acceptor->getTemplateTypeMap(),
$acceptor->getResolvedTemplateTypeMap(),
$acceptor->getParameters(),
$acceptor->isVariadic(),
return [new ExtendedFunctionVariant(
$combined->getTemplateTypeMap(),
$combined->getResolvedTemplateTypeMap(),
$combined->getParameters(),
$combined->isVariadic(),
$returnType,
$phpDocReturnType,
$nativeReturnType,
$acceptor->getCallSiteVarianceMap(),
), $this->methods[0]->getVariants());
$combined->getCallSiteVarianceMap(),
)];
}

public function getOnlyVariant(): ExtendedParametersAcceptor
Expand Down Expand Up @@ -211,7 +219,20 @@ public function acceptsNamedArguments(): TrinaryLogic

public function getSelfOutType(): ?Type
{
return null;
$types = [];
foreach ($this->methods as $method) {
$selfOutType = $method->getSelfOutType();
if ($selfOutType === null) {
return null;
}
$types[] = $selfOutType;
}

if (count($types) === 0) {
return null;
}

return TypeCombinator::intersect(...$types);
}

public function returnsByReference(): TrinaryLogic
Expand All @@ -226,7 +247,19 @@ public function isAbstract(): TrinaryLogic

public function getAttributes(): array
{
return $this->methods[0]->getAttributes();
$result = [];
$seen = [];
foreach ($this->methods as $method) {
foreach ($method->getAttributes() as $attribute) {
if (isset($seen[$attribute->getName()])) {
continue;
}
$seen[$attribute->getName()] = true;
$result[] = $attribute;
}
}

return $result;
}

public function mustUseReturnValue(): TrinaryLogic
Expand All @@ -236,7 +269,7 @@ public function mustUseReturnValue(): TrinaryLogic

public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
{
return $this->methods[0]->getResolvedPhpDoc();
return null;
}

}
14 changes: 13 additions & 1 deletion src/Reflection/Type/IntersectionTypePropertyReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,19 @@ public function isPrivateSet(): bool

public function getAttributes(): array
{
return $this->properties[0]->getAttributes();
$result = [];
$seen = [];
foreach ($this->properties as $property) {
foreach ($property->getAttributes() as $attribute) {
if (isset($seen[$attribute->getName()])) {
continue;
}
$seen[$attribute->getName()] = true;
$result[] = $attribute;
}
}

return $result;
}

public function isDummy(): TrinaryLogic
Expand Down
118 changes: 113 additions & 5 deletions src/Reflection/Type/UnionTypeMethodReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,32 @@

use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\Reflection\Assertions;
use PHPStan\Reflection\AttributeReflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ExtendedFunctionVariant;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ExtendedParametersAcceptor;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\ExtendedDummyParameter;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_filter;
use function array_map;
use function array_merge;
use function array_values;
use function count;
use function implode;
use function is_bool;

final class UnionTypeMethodReflection implements ExtendedMethodReflection
{

/** @var list<ExtendedParametersAcceptor>|null */
private ?array $cachedVariants = null;

/**
* @param ExtendedMethodReflection[] $methods
*/
Expand Down Expand Up @@ -79,9 +87,82 @@

public function getVariants(): array
{
$variants = array_merge(...array_map(static fn (MethodReflection $method) => $method->getVariants(), $this->methods));
if ($this->cachedVariants !== null) {
return $this->cachedVariants;
}

$allVariants = array_merge(...array_map(static fn (MethodReflection $method) => $method->getVariants(), $this->methods));
$combined = ParametersAcceptorSelector::combineAcceptors($allVariants);

// Fast path: when all methods come from the same class (e.g. enum cases,
// or multiple subtypes of the same base), params are identical — skip
// the expensive per-parameter intersection.
$declaringClasses = [];
foreach ($this->methods as $method) {
$declaringClasses[$method->getDeclaringClass()->getName()] = true;
}

if (count($declaringClasses) <= 1) {
return $this->cachedVariants = [$combined];
}

return [ParametersAcceptorSelector::combineAcceptors($variants)];
// combineAcceptors unions parameter types, but for union types we need
// to intersect them: the argument must be valid for ALL possible methods
// since we don't know which runtime type the object is.
$intersectedParams = [];
foreach ($combined->getParameters() as $i => $param) {
$types = [];
$nativeTypes = [];
$phpDocTypes = [];
foreach ($this->methods as $method) {

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.3)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.2)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.4)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan with result cache (8.5)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, ubuntu-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.3, windows-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.2, windows-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.5, windows-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.4, windows-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.0, windows-latest)

Foreach overwrites $method with its value variable.

Check failure on line 117 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (8.1, windows-latest)

Foreach overwrites $method with its value variable.
$variantTypes = [];
$variantNativeTypes = [];
$variantPhpDocTypes = [];
foreach ($method->getVariants() as $variant) {
$variantParams = $variant->getParameters();
if (!isset($variantParams[$i])) {
continue;

Check failure on line 124 in src/Reflection/Type/UnionTypeMethodReflection.php

View workflow job for this annotation

GitHub Actions / PHPStan (7.4, ubuntu-latest)

Foreach overwrites $method with its value variable.
}
$variantTypes[] = $variantParams[$i]->getType();
$variantNativeTypes[] = $variantParams[$i]->getNativeType();
$variantPhpDocTypes[] = $variantParams[$i]->getPhpDocType();
}
if ($variantTypes !== []) {
$types[] = count($variantTypes) === 1 ? $variantTypes[0] : TypeCombinator::union(...$variantTypes);
}
if ($variantNativeTypes !== []) {
$nativeTypes[] = count($variantNativeTypes) === 1 ? $variantNativeTypes[0] : TypeCombinator::union(...$variantNativeTypes);
}
if ($variantPhpDocTypes !== []) {
$phpDocTypes[] = count($variantPhpDocTypes) === 1 ? $variantPhpDocTypes[0] : TypeCombinator::union(...$variantPhpDocTypes);
}
}

$intersectedParams[] = new ExtendedDummyParameter(
$param->getName(),
count($types) > 1 ? TypeCombinator::intersect(...$types) : ($types[0] ?? $param->getType()),
$param->isOptional(),
$param->passedByReference(),
$param->isVariadic(),
$param->getDefaultValue(),
count($nativeTypes) > 1 ? TypeCombinator::intersect(...$nativeTypes) : ($nativeTypes[0] ?? $param->getNativeType()),
count($phpDocTypes) > 1 ? TypeCombinator::intersect(...$phpDocTypes) : ($phpDocTypes[0] ?? $param->getPhpDocType()),
$param->getOutType(),
$param->isImmediatelyInvokedCallable(),
$param->getClosureThisType(),
$param->getAttributes(),
);
}

return $this->cachedVariants = [new ExtendedFunctionVariant(
$combined->getTemplateTypeMap(),
$combined->getResolvedTemplateTypeMap(),
$intersectedParams,
$combined->isVariadic(),
$combined->getReturnType(),
$combined->getPhpDocReturnType(),
$combined->getNativeReturnType(),
)];
}

public function getOnlyVariant(): ExtendedParametersAcceptor
Expand Down Expand Up @@ -194,7 +275,20 @@

public function getSelfOutType(): ?Type
{
return null;
$types = [];
foreach ($this->methods as $method) {
$selfOutType = $method->getSelfOutType();
if ($selfOutType === null) {
return null;
}
$types[] = $selfOutType;
}

if (count($types) === 0) {
return null;
}

return TypeCombinator::union(...$types);
}

public function returnsByReference(): TrinaryLogic
Expand All @@ -209,7 +303,21 @@

public function getAttributes(): array
{
return $this->methods[0]->getAttributes();
$result = null;
foreach ($this->methods as $method) {
$methodAttributes = $method->getAttributes();
if ($result === null) {
$result = $methodAttributes;
continue;
}
$methodAttributeNames = [];
foreach ($methodAttributes as $attribute) {
$methodAttributeNames[$attribute->getName()] = true;
}
$result = array_filter($result, static fn (AttributeReflection $a) => isset($methodAttributeNames[$a->getName()]));
}

return array_values($result ?? []);
}

public function mustUseReturnValue(): TrinaryLogic
Expand All @@ -219,7 +327,7 @@

public function getResolvedPhpDoc(): ?ResolvedPhpDocBlock
{
return $this->methods[0]->getResolvedPhpDoc();
return null;
}

}
19 changes: 18 additions & 1 deletion src/Reflection/Type/UnionTypePropertyReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@

namespace PHPStan\Reflection\Type;

use PHPStan\Reflection\AttributeReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ExtendedMethodReflection;
use PHPStan\Reflection\ExtendedPropertyReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function array_filter;
use function array_map;
use function array_values;
use function count;
use function implode;

Expand Down Expand Up @@ -202,7 +205,21 @@ public function isPrivateSet(): bool

public function getAttributes(): array
{
return $this->properties[0]->getAttributes();
$result = null;
foreach ($this->properties as $property) {
$propertyAttributes = $property->getAttributes();
if ($result === null) {
$result = $propertyAttributes;
continue;
}
$propertyAttributeNames = [];
foreach ($propertyAttributes as $attribute) {
$propertyAttributeNames[$attribute->getName()] = true;
}
$result = array_filter($result, static fn (AttributeReflection $a) => isset($propertyAttributeNames[$a->getName()]));
}

return array_values($result ?? []);
}

public function isDummy(): TrinaryLogic
Expand Down
46 changes: 46 additions & 0 deletions tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3853,4 +3853,50 @@ public function testBug13805(): void
$this->analyse([__DIR__ . '/data/bug-13805.php'], []);
}

public function testUnionIntersectionMethodVariants(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/union-intersection-method-variants.php'], [
[
'Parameter #1 $x of method UnionIntersectionMethodVariants\AcceptsInt::process() expects int|string, true given.',
38,
],
[
'Method UnionIntersectionMethodVariants\TwoParams::transform() invoked with 0 parameters, 1-2 required.',
52,
],
[
'Parameter #1 $x of method UnionIntersectionMethodVariants\AcceptsString::process() expects string, null given.',
68,
],
[
'Parameter #1 $x of method UnionIntersectionMethodVariants\AcceptsString::process() expects string, int given.',
69,
],
[
'Method UnionIntersectionMethodVariants\TwoParams::transform() invoked with 0 parameters, 1-2 required.',
96,
],
]);
}

public function testBug9664(): void
{
$this->checkThisOnly = false;
$this->checkNullables = true;
$this->checkUnionTypes = true;
$this->analyse([__DIR__ . '/data/bug-9664.php'], [
[
'Parameter #1 $foo of method Bug9664\Entity1::setFoo() expects string, null given.',
17,
],
[
'Parameter #1 $foo of method Bug9664\Entity1::setFoo() expects string, null given.',
22,
],
]);
}

}
Loading
Loading