From 6ad966b2d1efd70547bc623e7b3c95d2cc0abda7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 11 Jul 2026 09:27:27 +0200 Subject: [PATCH] Add MergePhpstanDocTagIntoNativeRector to merge @phpstan-* doc tags into native @var/@param/@return --- .../Fixture/method_param_and_return.php.inc | 39 ++++ .../Fixture/phpstan_only_var.php.inc | 27 +++ .../Fixture/property_var.php.inc | 29 +++ .../Fixture/skip_no_phpstan_tag.php.inc | 11 + ...MergePhpstanDocTagIntoNativeRectorTest.php | 28 +++ .../config/configured_rule.php | 9 + .../MergePhpstanDocTagIntoNativeRector.php | 192 ++++++++++++++++++ scoper.php | 6 +- tests/Scoper/ScoperPatchersTest.php | 2 +- 9 files changed, 341 insertions(+), 2 deletions(-) create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/method_param_and_return.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/phpstan_only_var.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/property_var.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/skip_no_phpstan_tag.php.inc create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/MergePhpstanDocTagIntoNativeRectorTest.php create mode 100644 rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/config/configured_rule.php create mode 100644 rules/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector.php diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/method_param_and_return.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/method_param_and_return.php.inc new file mode 100644 index 00000000000..12654e63416 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/method_param_and_return.php.inc @@ -0,0 +1,39 @@ + $values + * + * @return array + * @phpstan-return array + */ + public function process($values) + { + return $values; + } +} + +?> +----- + $values + * + * @return array + */ + public function process($values) + { + return $values; + } +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/phpstan_only_var.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/phpstan_only_var.php.inc new file mode 100644 index 00000000000..fdbb8ee58d8 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/phpstan_only_var.php.inc @@ -0,0 +1,27 @@ + + */ + private $items; +} + +?> +----- + + */ + private $items; +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/property_var.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/property_var.php.inc new file mode 100644 index 00000000000..710c735f135 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/property_var.php.inc @@ -0,0 +1,29 @@ + + */ + private $items; +} + +?> +----- + + */ + private $items; +} + +?> diff --git a/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/skip_no_phpstan_tag.php.inc b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/skip_no_phpstan_tag.php.inc new file mode 100644 index 00000000000..878f4c3f4d4 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/Fixture/skip_no_phpstan_tag.php.inc @@ -0,0 +1,11 @@ +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/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/config/configured_rule.php b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/config/configured_rule.php new file mode 100644 index 00000000000..4481f085ac4 --- /dev/null +++ b/rules-tests/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector/config/configured_rule.php @@ -0,0 +1,9 @@ +withRules([MergePhpstanDocTagIntoNativeRector::class]); diff --git a/rules/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector.php b/rules/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector.php new file mode 100644 index 00000000000..230aba71da5 --- /dev/null +++ b/rules/TypeDeclarationDocblocks/Rector/Property/MergePhpstanDocTagIntoNativeRector.php @@ -0,0 +1,192 @@ + + */ + private const array PHPSTAN_TAG_TO_NATIVE_TAG = [ + '@phpstan-var' => '@var', + '@phpstan-param' => '@param', + '@phpstan-return' => '@return', + ]; + + public function __construct( + private readonly DocBlockUpdater $docBlockUpdater, + private readonly PhpDocInfoFactory $phpDocInfoFactory, + ) { + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition( + 'Merge more precise @phpstan-var/@phpstan-param/@phpstan-return docblock tag into its native @var/@param/@return tag', + [ + new CodeSample( + <<<'CODE_SAMPLE' +final class SomeClass +{ + /** + * @var Collection + * + * @phpstan-var Collection + */ + private $items; +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +final class SomeClass +{ + /** + * @var Collection + */ + private $items; +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Property::class, Param::class, ClassConst::class, ClassMethod::class, Function_::class]; + } + + /** + * @param Property|Param|ClassConst|ClassMethod|Function_ $node + */ + public function refactor(Node $node): ?Node + { + $phpDocInfo = $this->phpDocInfoFactory->createFromNode($node); + if (! $phpDocInfo instanceof PhpDocInfo) { + return null; + } + + $phpDocNode = $phpDocInfo->getPhpDocNode(); + + $phpstanParamNames = []; + $phpstanVarNames = []; + $hasPhpstanReturn = false; + + foreach ($phpDocNode->children as $phpDocChildNode) { + if (! $phpDocChildNode instanceof PhpDocTagNode) { + continue; + } + + if ($phpDocChildNode->name === '@phpstan-param' && $phpDocChildNode->value instanceof ParamTagValueNode) { + $phpstanParamNames[] = $phpDocChildNode->value->parameterName; + } elseif ($phpDocChildNode->name === '@phpstan-var' && $phpDocChildNode->value instanceof VarTagValueNode) { + $phpstanVarNames[] = $phpDocChildNode->value->variableName; + } elseif ($phpDocChildNode->name === '@phpstan-return' && $phpDocChildNode->value instanceof ReturnTagValueNode) { + $hasPhpstanReturn = true; + } + } + + if ($phpstanParamNames === [] && $phpstanVarNames === [] && ! $hasPhpstanReturn) { + return null; + } + + $hasChanged = false; + + foreach ($phpDocNode->children as $key => $phpDocChildNode) { + if (! $phpDocChildNode instanceof PhpDocTagNode) { + continue; + } + + // drop the weaker native tag that the @phpstan-* variant overrules + if ($this->isOverruledNativeTag( + $phpDocChildNode, + $phpstanParamNames, + $phpstanVarNames, + $hasPhpstanReturn + )) { + unset($phpDocNode->children[$key]); + $hasChanged = true; + continue; + } + + // promote the @phpstan-* tag to its native counterpart + $nativeTagName = self::PHPSTAN_TAG_TO_NATIVE_TAG[$phpDocChildNode->name] ?? null; + if ($nativeTagName !== null) { + $phpDocNode->children[$key] = new PhpDocTagNode($nativeTagName, $phpDocChildNode->value); + $hasChanged = true; + } + } + + if (! $hasChanged) { + return null; + } + + $phpDocNode->children = array_values($phpDocNode->children); + + // drop a leading empty line left behind by a removed native tag + while ($phpDocNode->children !== []) { + $firstChildNode = $phpDocNode->children[0]; + if ($firstChildNode instanceof PhpDocTextNode && trim($firstChildNode->text) === '') { + array_shift($phpDocNode->children); + continue; + } + + break; + } + + $this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); + + return $node; + } + + /** + * @param string[] $phpstanParamNames + * @param string[] $phpstanVarNames + */ + private function isOverruledNativeTag( + PhpDocTagNode $phpDocTagNode, + array $phpstanParamNames, + array $phpstanVarNames, + bool $hasPhpstanReturn + ): bool { + if ($phpDocTagNode->name === '@param' && $phpDocTagNode->value instanceof ParamTagValueNode) { + return in_array($phpDocTagNode->value->parameterName, $phpstanParamNames, true); + } + + if ($phpDocTagNode->name === '@var' && $phpDocTagNode->value instanceof VarTagValueNode) { + return in_array($phpDocTagNode->value->variableName, $phpstanVarNames, true); + } + + if ($phpDocTagNode->name === '@return' && $phpDocTagNode->value instanceof ReturnTagValueNode) { + return $hasPhpstanReturn; + } + + return false; + } +} diff --git a/scoper.php b/scoper.php index a744e529014..4bbfc32d8e7 100644 --- a/scoper.php +++ b/scoper.php @@ -126,7 +126,11 @@ static function (array $ruleDefinitionMatch) use ($prefix): string { "#(<<<'?(?