From 9be95f24ddcd15d1cfd12fa5cb0da9fec0a0dd5c Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 5 Oct 2026 10:11:59 +0900 Subject: [PATCH 1/2] [TypeDeclaration] Skip redundant intersection types in union return types --- .../redundant_intersection.php.inc | 49 ++++++++++++++ .../TypeMapper/UnionTypeMapper.php | 67 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/ReturnUnionTypeRector/FixtureUnionIntersection/redundant_intersection.php.inc 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..89d9d0c752b 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 [strtolower($node->toString())]; + } + + if (! $node instanceof PHPParserNodeIntersectionType) { + return []; + } + + $names = []; + foreach ($node->types as $type) { + if (! $type instanceof Name) { + return []; + } + + $names[] = strtolower($type->toString()); + } + + return $names; + } + private function resolveUnionTypeNode(PhpParserUnionType $phpParserUnionType): ?PhpParserUnionType { if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { From 2489e65003866a12dc6d9c26756eb7d194fa7727 Mon Sep 17 00:00:00 2001 From: kevin9327 <5299031+kevin9327@users.noreply.github.com> Date: Mon, 5 Oct 2026 11:31:01 +0900 Subject: [PATCH 2/2] Drop unneeded strtolower in intersection part names --- src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php index 89d9d0c752b..c80df7cafdc 100644 --- a/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php +++ b/src/PHPStanStaticTypeMapper/TypeMapper/UnionTypeMapper.php @@ -282,7 +282,7 @@ private function removeRedundantIntersectionTypes(array $phpParserUnionedTypes): private function resolveIntersectionPartNames(Node $node): array { if ($node instanceof Name) { - return [strtolower($node->toString())]; + return [$node->toString()]; } if (! $node instanceof PHPParserNodeIntersectionType) { @@ -295,7 +295,7 @@ private function resolveIntersectionPartNames(Node $node): array return []; } - $names[] = strtolower($type->toString()); + $names[] = $type->toString(); } return $names;