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
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnUnionTypeRector\FixtureUnionIntersection;

/**
* "\Iterator|(\Iterator&\Countable)" is a compile error in PHP: the intersection is redundant
*/
final class RedundantIntersection
{
public function run(\Iterator $obj)
{
if (! $obj instanceof \Countable) {
return $obj;
}

if (rand(0, 1)) {
return $obj;
}

return '10';
}
}

?>
-----
<?php

namespace Rector\Tests\TypeDeclaration\Rector\ClassMethod\ReturnUnionTypeRector\FixtureUnionIntersection;

/**
* "\Iterator|(\Iterator&\Countable)" is a compile error in PHP: the intersection is redundant
*/
final class RedundantIntersection
{
public function run(\Iterator $obj): \Iterator|string
{
if (! $obj instanceof \Countable) {
return $obj;
}

if (rand(0, 1)) {
return $obj;
}

return '10';
}
}

?>
67 changes: 67 additions & 0 deletions src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ private function matchPhpParserUnionType(UnionType $unionType, string $typeKind)

/** @var Identifier[]|Name[] $phpParserUnionedTypes */
$phpParserUnionedTypes = array_unique($phpParserUnionedTypes, SORT_REGULAR);
$phpParserUnionedTypes = $this->removeRedundantIntersectionTypes($phpParserUnionedTypes);

$countPhpParserUnionedTypes = count($phpParserUnionedTypes);
if ($countPhpParserUnionedTypes === 1) {
Expand All @@ -234,6 +235,72 @@ private function matchPhpParserUnionType(UnionType $unionType, string $typeKind)
return $this->resolveTypeWithNullablePHPParserUnionType(new PhpParserUnionType($phpParserUnionedTypes));
}

/**
* PHP rejects e.g. "A|(A&B)" with "Type A&B is redundant as it is more restrictive than type A",
* so drop intersections that contain all parts of another member of the union
*
* @param array<Identifier|Name|PHPParserNodeIntersectionType> $phpParserUnionedTypes
* @return list<Identifier|Name|PHPParserNodeIntersectionType>
*/
private function removeRedundantIntersectionTypes(array $phpParserUnionedTypes): array
{
$phpParserUnionedTypes = array_values($phpParserUnionedTypes);

$typeNames = [];
foreach ($phpParserUnionedTypes as $key => $phpParserUnionedType) {
$typeNames[$key] = $this->resolveIntersectionPartNames($phpParserUnionedType);
}

foreach ($phpParserUnionedTypes as $key => $phpParserUnionedType) {
if (! $phpParserUnionedType instanceof PHPParserNodeIntersectionType) {
continue;
}

foreach ($typeNames as $otherKey => $otherTypeNames) {
if ($otherKey === $key || $otherTypeNames === []) {
continue;
}

// already removed
if (! isset($phpParserUnionedTypes[$otherKey])) {
continue;
}

if (array_diff($otherTypeNames, $typeNames[$key]) === []) {
unset($phpParserUnionedTypes[$key]);
continue 2;
}
}
}

return array_values($phpParserUnionedTypes);
}

/**
* @return string[]
*/
private function resolveIntersectionPartNames(Node $node): array
{
if ($node instanceof Name) {
return [$node->toString()];
}

if (! $node instanceof PHPParserNodeIntersectionType) {
return [];
}

$names = [];
foreach ($node->types as $type) {
if (! $type instanceof Name) {
return [];
}

$names[] = $type->toString();
}

return $names;
}

private function resolveUnionTypeNode(PhpParserUnionType $phpParserUnionType): ?PhpParserUnionType
{
if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) {
Expand Down
Loading