From d0501fd8eee60caaea5051134e6c99ad2711680d Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 11 Aug 2025 09:44:26 +0200 Subject: [PATCH 1/2] [type-declarations] Add TypedPropertyFromDocblockSetUpDefinedRector --- bin/check-before-after-same-fixtures.php | 2 +- config/set/php85.php | 4 +- .../Fixture/magic_setup_test_assign.php.inc | 40 +++++ .../Source/SomeDocblockType.php | 9 + ...ertyFromDocblockSetUpDefinedRectorTest.php | 28 +++ .../config/rule_config.php | 11 ++ ...PropertyFromDocblockSetUpDefinedRector.php | 168 ++++++++++++++++++ src/Config/Level/TypeDeclarationLevel.php | 3 + src/ValueObject/PhpVersionFeature.php | 4 +- 9 files changed, 265 insertions(+), 4 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Source/SomeDocblockType.php create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/TypedPropertyFromDocblockSetUpDefinedRectorTest.php create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/config/rule_config.php create mode 100644 rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php diff --git a/bin/check-before-after-same-fixtures.php b/bin/check-before-after-same-fixtures.php index 5b4822a6bce..7eb7bb257d9 100644 --- a/bin/check-before-after-same-fixtures.php +++ b/bin/check-before-after-same-fixtures.php @@ -41,7 +41,7 @@ public function run(array $testDirectories): int } if ($invalidFixturePaths === []) { - $this->symfonyStyle->success('All fixtures are valid'); + $this->symfonyStyle->success(sprintf('All %d fixtures are valid', count($fixtureFiles))); return Command::SUCCESS; } diff --git a/config/set/php85.php b/config/set/php85.php index a3125b37f88..37449b68b95 100644 --- a/config/set/php85.php +++ b/config/set/php85.php @@ -15,7 +15,9 @@ use Rector\Renaming\ValueObject\RenameClassAndConstFetch; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rules([ArrayFirstLastRector::class, RemoveFinfoBufferContextArgRector::class, NullDebugInfoReturnRector::class]); + $rectorConfig->rules( + [ArrayFirstLastRector::class, RemoveFinfoBufferContextArgRector::class, NullDebugInfoReturnRector::class] + ); $rectorConfig->ruleWithConfiguration( RemoveFuncCallArgRector::class, diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc new file mode 100644 index 00000000000..307f8c6ce32 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc @@ -0,0 +1,40 @@ +someType = $this->create('string'); + } +} + +?> +----- +someType = $this->create('string'); + } +} + +?> diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Source/SomeDocblockType.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Source/SomeDocblockType.php new file mode 100644 index 00000000000..411046e54a9 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Source/SomeDocblockType.php @@ -0,0 +1,9 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/rule_config.php'; + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/config/rule_config.php b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/config/rule_config.php new file mode 100644 index 00000000000..88c338e2c6a --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/config/rule_config.php @@ -0,0 +1,11 @@ +withRules([TypedPropertyFromDocblockSetUpDefinedRector::class]) + ->withPhpVersion(PhpVersionFeature::TYPED_PROPERTIES); diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php new file mode 100644 index 00000000000..8e75484c213 --- /dev/null +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php @@ -0,0 +1,168 @@ +doctrine = $this->container('doctrine.orm.entity_manager'); + } +} +CODE_SAMPLE + + , + <<<'CODE_SAMPLE' +use PHPUnit\Framework\TestCase; + +class SomeClass extends TestCase +{ + private \Doctrine\ORM\EntityManagerInterface $doctrine; + + protected function setUp(): void + { + $this->doctrine = $this->container('doctrine.orm.entity_manager'); + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->testsNodeAnalyzer->isInTestClass($node)) { + return null; + } + + // nothing useful here + $setUpClassMethod = $node->getMethod(MethodName::SET_UP); + if (! $setUpClassMethod instanceof ClassMethod) { + return null; + } + + $hasChanged = false; + + foreach ($node->getProperties() as $property) { + // already known type + if ($property->type instanceof \PhpParser\Node) { + continue; + } + + // some magic might be going on + if ($property->isStatic() || ! $property->isPrivate()) { + continue; + } + + // exactly one property + if (count($property->props) !== 1) { + continue; + } + + $propertyName = $property->props[0]->name->toString(); + if (! $this->constructorAssignDetector->isPropertyAssigned($node, $propertyName)) { + continue; + } + + $propertyPhpDocInfo = $this->phpDocInfoFactory->createFromNode($property); + if (! $propertyPhpDocInfo instanceof PhpDocInfo) { + continue; + } + + $varType = $propertyPhpDocInfo->getVarType(); + if ($varType instanceof MixedType) { + continue; + } + + $nativePropertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode( + $varType, + TypeKind::PROPERTY + ); + if (! $nativePropertyTypeNode instanceof \PhpParser\Node) { + continue; + } + + // remove var tag + $this->removeVarTag($propertyPhpDocInfo, $property); + + $property->type = $nativePropertyTypeNode; + $hasChanged = true; + } + + if ($hasChanged) { + return $node; + } + + return null; + } + + public function provideMinPhpVersion(): int + { + return PhpVersionFeature::TYPED_PROPERTIES; + } + + private function removeVarTag(PhpDocInfo $propertyPhpDocInfo, Property $property): void + { + $propertyPhpDocInfo->removeByType(VarTagValueNode::class); + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($property); + } +} diff --git a/src/Config/Level/TypeDeclarationLevel.php b/src/Config/Level/TypeDeclarationLevel.php index 1ac9994995b..d1f27f9c087 100644 --- a/src/Config/Level/TypeDeclarationLevel.php +++ b/src/Config/Level/TypeDeclarationLevel.php @@ -13,6 +13,7 @@ use Rector\TypeDeclaration\Rector\Class_\PropertyTypeFromStrictSetterGetterRector; use Rector\TypeDeclaration\Rector\Class_\ReturnTypeFromStrictTernaryRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromCreateMockAssignRector; +use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromDocblockSetUpDefinedRector; use Rector\TypeDeclaration\Rector\Class_\TypedPropertyFromJMSSerializerAttributeTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddMethodCallBasedStrictParamTypeRector; use Rector\TypeDeclaration\Rector\ClassMethod\AddParamTypeBasedOnPHPUnitDataProviderRector; @@ -142,5 +143,7 @@ final class TypeDeclarationLevel // possibly based on docblocks, but also helpful, intentionally last AddArrayFunctionClosureParamTypeRector::class, + + TypedPropertyFromDocblockSetUpDefinedRector::class, ]; } diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index dcdbe245a2f..ec67f7703e1 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -768,12 +768,12 @@ final class PhpVersionFeature * @var int */ public const ARRAY_ANY = PhpVersion::PHP_84; - + /** * @see https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_the_context_parameter_for_finfo_buffer * @var int */ - public const DEPRECATE_FINFO_BUFFER_CONTEXT = PhpVersion::PHP_85; + public const DEPRECATE_FINFO_BUFFER_CONTEXT = PhpVersion::PHP_85; /** * @see https://wiki.php.net/rfc/deprecations_php_8_5#deprecate_debuginfo_returning_null From bdc1fe26c2e2c8c994e384395f82869b55443b64 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Mon, 11 Aug 2025 12:31:14 +0200 Subject: [PATCH 2/2] more fixtures --- .../Fixture/magic_setup_test_assign.php.inc | 2 +- .../Fixture/skip_no_docblock.php.inc | 16 ++++++++++++++++ .../Fixture/skip_public_property.php.inc | 19 +++++++++++++++++++ .../ClassMethod/NullDebugInfoReturnRector.php | 5 ++++- ...PropertyFromDocblockSetUpDefinedRector.php | 10 +++++++--- 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_no_docblock.php.inc create mode 100644 rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_public_property.php.inc diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc index 307f8c6ce32..b168c2536e0 100644 --- a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/magic_setup_test_assign.php.inc @@ -29,7 +29,7 @@ use Rector\Tests\TypeDeclaration\Rector\Class_\TypedPropertyFromDocblockSetUpDef final class MagicSetupTestAssign extends TestCase { - private SomeDocblockType $someType; + private \Rector\Tests\TypeDeclaration\Rector\Class_\TypedPropertyFromDocblockSetUpDefinedRector\Source\SomeDocblockType $someType; protected function setUp(): void { diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_no_docblock.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_no_docblock.php.inc new file mode 100644 index 00000000000..7e9a8fc62e5 --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_no_docblock.php.inc @@ -0,0 +1,16 @@ +someType = $this->create('string'); + } +} diff --git a/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_public_property.php.inc b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_public_property.php.inc new file mode 100644 index 00000000000..744ed5fd27d --- /dev/null +++ b/rules-tests/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector/Fixture/skip_public_property.php.inc @@ -0,0 +1,19 @@ +someType = $this->create('string'); + } +} diff --git a/rules/Php85/Rector/ClassMethod/NullDebugInfoReturnRector.php b/rules/Php85/Rector/ClassMethod/NullDebugInfoReturnRector.php index e5ed9d260fa..7f42e4d59d8 100644 --- a/rules/Php85/Rector/ClassMethod/NullDebugInfoReturnRector.php +++ b/rules/Php85/Rector/ClassMethod/NullDebugInfoReturnRector.php @@ -75,7 +75,10 @@ public function refactor(Node $node): ?Node } $hasChanged = \false; - $this->traverseNodesWithCallable((array) $node->stmts, function (Node $node) use (&$hasChanged): int|Return_|null { + + $this->traverseNodesWithCallable((array) $node->stmts, function (Node $node) use ( + &$hasChanged + ): int|Return_|null { if ($node instanceof Class_ || $node instanceof Function_ || $node instanceof Closure) { return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } diff --git a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php index 8e75484c213..8e50b591ff6 100644 --- a/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php +++ b/rules/TypeDeclaration/Rector/Class_/TypedPropertyFromDocblockSetUpDefinedRector.php @@ -104,12 +104,16 @@ public function refactor(Node $node): ?Node foreach ($node->getProperties() as $property) { // already known type - if ($property->type instanceof \PhpParser\Node) { + if ($property->type instanceof Node) { continue; } // some magic might be going on - if ($property->isStatic() || ! $property->isPrivate()) { + if ($property->isStatic()) { + continue; + } + + if (! $property->isPrivate()) { continue; } @@ -137,7 +141,7 @@ public function refactor(Node $node): ?Node $varType, TypeKind::PROPERTY ); - if (! $nativePropertyTypeNode instanceof \PhpParser\Node) { + if (! $nativePropertyTypeNode instanceof Node) { continue; }