From 08cf654a9693d553d19169d4ac85db3474ce072d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 17:31:04 +0200 Subject: [PATCH 1/2] [TypeDeclaration] Add NarrowBoolDocblockReturnTypeRector to narrow @return bool to false/true based on native return type --- .../Fixture/narrow_bool_to_false.php.inc | 33 ++++ .../Fixture/narrow_bool_to_true.php.inc | 33 ++++ .../Fixture/narrow_function.php.inc | 27 +++ .../Fixture/skip_native_bool.php.inc | 14 ++ .../skip_native_false_and_true.php.inc | 14 ++ .../Fixture/skip_no_bool_in_docblock.php.inc | 14 ++ .../Fixture/skip_no_docblock.php.inc | 11 ++ ...NarrowBoolDocblockReturnTypeRectorTest.php | 28 +++ .../config/configured_rule.php | 10 ++ .../NarrowBoolDocblockReturnTypeRector.php | 164 ++++++++++++++++++ .../StaticDoctrineAnnotationParser.php | 4 - .../PlainValueParser.php | 3 - .../Storage/MemoryCacheStorage.php | 3 - .../AnnotationToAttributeMapper.php | 3 - src/PhpParser/Node/Value/ValueResolver.php | 3 - 15 files changed, 348 insertions(+), 16 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_false.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_true.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_function.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_native_bool.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_native_false_and_true.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_no_bool_in_docblock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_no_docblock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/NarrowBoolDocblockReturnTypeRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/config/configured_rule.php create mode 100644 rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_false.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_false.php.inc new file mode 100644 index 00000000000..3b904560b93 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_false.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_true.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_true.php.inc new file mode 100644 index 00000000000..35760c376f6 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_bool_to_true.php.inc @@ -0,0 +1,33 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_function.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_function.php.inc new file mode 100644 index 00000000000..a600e879cf6 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/narrow_function.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_native_bool.php.inc b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_native_bool.php.inc new file mode 100644 index 00000000000..cd722a3e041 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_native_bool.php.inc @@ -0,0 +1,14 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/config/configured_rule.php b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/config/configured_rule.php new file mode 100644 index 00000000000..b736f9f0624 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/config/configured_rule.php @@ -0,0 +1,10 @@ +rule(NarrowBoolDocblockReturnTypeRector::class); +}; diff --git a/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php b/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php new file mode 100644 index 00000000000..ed2a4fb9690 --- /dev/null +++ b/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php @@ -0,0 +1,164 @@ +matchNativeConstantBoolName($node); + if ($constantBoolName === null) { + return null; + } + + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $returnTagValueNode = $phpDocInfo->getReturnTagValue(); + if (! $returnTagValueNode instanceof ReturnTagValueNode) { + return null; + } + + if (! $returnTagValueNode->type instanceof UnionTypeNode) { + return null; + } + + if (! $this->narrowBoolInUnion($returnTagValueNode->type, $constantBoolName)) { + return null; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + /** + * Returns "false" or "true" when the native return type allows exactly one of them, null otherwise. + */ + private function matchNativeConstantBoolName(ClassMethod|Function_ $node): ?string + { + $returnType = $node->returnType; + if (! $returnType instanceof UnionType) { + return null; + } + + $hasFalse = false; + $hasTrue = false; + + foreach ($returnType->types as $type) { + if (! $type instanceof Identifier) { + continue; + } + + $lowerName = $type->toLowerString(); + if ($lowerName === 'bool') { + // ambiguous, both values possible + return null; + } + + if ($lowerName === 'false') { + $hasFalse = true; + } + + if ($lowerName === 'true') { + $hasTrue = true; + } + } + + if ($hasFalse && ! $hasTrue) { + return 'false'; + } + + if ($hasTrue && ! $hasFalse) { + return 'true'; + } + + return null; + } + + private function narrowBoolInUnion(UnionTypeNode $unionTypeNode, string $constantBoolName): bool + { + $hasChanged = false; + + foreach ($unionTypeNode->types as $key => $typeNode) { + if ($typeNode instanceof IdentifierTypeNode && $typeNode->name === 'bool') { + $unionTypeNode->types[$key] = new IdentifierTypeNode($constantBoolName); + $hasChanged = true; + } + } + + return $hasChanged; + } +} diff --git a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php index 79fbfe00660..e5a1b61180c 100644 --- a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php +++ b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php @@ -51,7 +51,6 @@ public function resolveAnnotationMethodCall(BetterTokenIterator $tokenIterator, /** * @api tests * @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1215-L1224 - * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode */ public function resolveAnnotationValue( BetterTokenIterator $tokenIterator, @@ -120,9 +119,6 @@ private function resolveAnnotationValues(BetterTokenIterator $tokenIterator, Nod return $this->arrayParser->createArrayFromValues($values); } - /** - * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode - */ private function parseValue( BetterTokenIterator $tokenIterator, Node $currentPhpNode diff --git a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php index c6d7e9da51e..d4b6c68e9d4 100644 --- a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php +++ b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php @@ -37,9 +37,6 @@ public function autowire( $this->arrayParser = $arrayParser; } - /** - * @return string|mixed[]|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode - */ public function parseValue( BetterTokenIterator $tokenIterator, Node $currentPhpNode diff --git a/src/Caching/ValueObject/Storage/MemoryCacheStorage.php b/src/Caching/ValueObject/Storage/MemoryCacheStorage.php index 65e24c37f13..c3e4ecf1a31 100644 --- a/src/Caching/ValueObject/Storage/MemoryCacheStorage.php +++ b/src/Caching/ValueObject/Storage/MemoryCacheStorage.php @@ -17,9 +17,6 @@ final class MemoryCacheStorage implements CacheStorageInterface */ private array $storage = []; - /** - * @return null|mixed - */ public function load(string $key, string $variableKey): mixed { if (! isset($this->storage[$key])) { diff --git a/src/PhpAttribute/AnnotationToAttributeMapper.php b/src/PhpAttribute/AnnotationToAttributeMapper.php index ac8cbad06e1..26825226509 100644 --- a/src/PhpAttribute/AnnotationToAttributeMapper.php +++ b/src/PhpAttribute/AnnotationToAttributeMapper.php @@ -29,9 +29,6 @@ public function __construct( Assert::notEmpty($annotationToAttributeMappers); } - /** - * @return mixed|DocTagNodeState::REMOVE_ARRAY - */ public function map(mixed $value): mixed { foreach ($this->annotationToAttributeMappers as $annotationToAttributeMapper) { diff --git a/src/PhpParser/Node/Value/ValueResolver.php b/src/PhpParser/Node/Value/ValueResolver.php index 77a9b830dec..208373b1720 100644 --- a/src/PhpParser/Node/Value/ValueResolver.php +++ b/src/PhpParser/Node/Value/ValueResolver.php @@ -240,9 +240,6 @@ private function resolveFileConstant(File $file): string return $file->getFilePath(); } - /** - * @return mixed[]|null - */ private function extractConstantArrayTypeValue(ConstantArrayType $constantArrayType): ?array { $keys = []; From 3b31e5689f2c6b36fb1562b0e6bfef29d73569ad Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 30 Jun 2026 17:37:26 +0200 Subject: [PATCH 2/2] Add duplicate-constant guard and skip-case fixtures --- .../Fixture/skip_nullable_array_native.php.inc | 13 +++++++++++++ .../Fixture/skip_union_with_array_native.php.inc | 13 +++++++++++++ .../Fixture/skip_already_has_constant.php.inc | 14 ++++++++++++++ .../Fixture/skip_nullable_array_no_bool.php.inc | 14 ++++++++++++++ .../Fixture/skip_union_without_bool.php.inc | 14 ++++++++++++++ .../RemoveUselessUnionReturnDocblockRector.php | 4 ++-- .../NarrowBoolDocblockReturnTypeRector.php | 7 +++++++ .../StaticDoctrineAnnotationParser.php | 4 ++++ .../PlainValueParser.php | 3 +++ src/PhpParser/Node/Value/ValueResolver.php | 3 +++ 10 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector/Fixture/skip_nullable_array_native.php.inc create mode 100644 rules-tests/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector/Fixture/skip_union_with_array_native.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_already_has_constant.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_nullable_array_no_bool.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector/Fixture/skip_union_without_bool.php.inc diff --git a/rules-tests/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector/Fixture/skip_nullable_array_native.php.inc b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector/Fixture/skip_nullable_array_native.php.inc new file mode 100644 index 00000000000..bf53ea289de --- /dev/null +++ b/rules-tests/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector/Fixture/skip_nullable_array_native.php.inc @@ -0,0 +1,13 @@ +|int + */ + private function parseValue(string $value): string|array|int + { + return $value; + } +} diff --git a/rules/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector.php b/rules/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector.php index d6d713e751a..561a2689a75 100644 --- a/rules/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector.php +++ b/rules/DeadCode/Rector/ClassMethod/RemoveUselessUnionReturnDocblockRector.php @@ -106,8 +106,8 @@ public function refactor(Node $node): ?Node $nativeReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($node->returnType); - // keep array native type, the docblock may carry element types - if ($nativeReturnType->isArray()->yes()) { + // keep array-involving native type (array, ?array, union with array), the docblock may carry element types + if (! $nativeReturnType->isArray()->no()) { return null; } diff --git a/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php b/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php index ed2a4fb9690..ad9a39a75ba 100644 --- a/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php +++ b/rules/TypeDeclaration/Rector/ClassMethod/NarrowBoolDocblockReturnTypeRector.php @@ -150,6 +150,13 @@ private function matchNativeConstantBoolName(ClassMethod|Function_ $node): ?stri private function narrowBoolInUnion(UnionTypeNode $unionTypeNode, string $constantBoolName): bool { + // already contains the constant? replacing "bool" would create a duplicate + foreach ($unionTypeNode->types as $typeNode) { + if ($typeNode instanceof IdentifierTypeNode && $typeNode->name === $constantBoolName) { + return false; + } + } + $hasChanged = false; foreach ($unionTypeNode->types as $key => $typeNode) { diff --git a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php index e5a1b61180c..79fbfe00660 100644 --- a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php +++ b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser.php @@ -51,6 +51,7 @@ public function resolveAnnotationMethodCall(BetterTokenIterator $tokenIterator, /** * @api tests * @see https://github.com/doctrine/annotations/blob/c66f06b7c83e9a2a7523351a9d5a4b55f885e574/lib/Doctrine/Common/Annotations/DocParser.php#L1215-L1224 + * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode */ public function resolveAnnotationValue( BetterTokenIterator $tokenIterator, @@ -119,6 +120,9 @@ private function resolveAnnotationValues(BetterTokenIterator $tokenIterator, Nod return $this->arrayParser->createArrayFromValues($values); } + /** + * @return CurlyListNode|string|array|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode + */ private function parseValue( BetterTokenIterator $tokenIterator, Node $currentPhpNode diff --git a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php index d4b6c68e9d4..c6d7e9da51e 100644 --- a/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php +++ b/src/BetterPhpDocParser/PhpDocParser/StaticDoctrineAnnotationParser/PlainValueParser.php @@ -37,6 +37,9 @@ public function autowire( $this->arrayParser = $arrayParser; } + /** + * @return string|mixed[]|ConstExprNode|DoctrineAnnotationTagValueNode|StringNode + */ public function parseValue( BetterTokenIterator $tokenIterator, Node $currentPhpNode diff --git a/src/PhpParser/Node/Value/ValueResolver.php b/src/PhpParser/Node/Value/ValueResolver.php index 208373b1720..77a9b830dec 100644 --- a/src/PhpParser/Node/Value/ValueResolver.php +++ b/src/PhpParser/Node/Value/ValueResolver.php @@ -240,6 +240,9 @@ private function resolveFileConstant(File $file): string return $file->getFilePath(); } + /** + * @return mixed[]|null + */ private function extractConstantArrayTypeValue(ConstantArrayType $constantArrayType): ?array { $keys = [];