diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureUnionIntersection/redundant_intersection.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureUnionIntersection/redundant_intersection.php.inc new file mode 100644 index 00000000000..0154fd51214 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureUnionIntersection/redundant_intersection.php.inc @@ -0,0 +1,49 @@ + +----- + diff --git a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php index a610e4a441b..c80df7cafdc 100644 --- a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php +++ b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php @@ -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) { @@ -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 $phpParserUnionedTypes + * @return list + */ + 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)) {