From 5f874721616b39a13686d885578211ef06725420 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Thu, 2 Jun 2022 10:22:27 +0200 Subject: [PATCH] add ConstExprClassNameDecorator, add PhpDocNodeDecoratorInterface --- easy-ci.php | 2 + .../PhpDocNodeDecoratorInterface.php | 13 +++++ .../PhpDocInfo/PhpDocInfoFactory.php | 2 +- .../PhpDocParser/BetterPhpDocParser.php | 29 +++++++--- .../ConstExprClassNameDecorator.php | 24 +++----- .../DoctrineAnnotationDecorator.php | 15 ++--- .../Fixture/change_param_type.php.inc | 3 +- .../Fixture/multiple_params_change.php.inc | 38 +++++++++++++ .../Fixture/skip_unknown_class.php.inc | 13 +++++ .../EnumConstListClassDetector.php | 24 ++++++-- .../Php80/NodeAnalyzer/EnumParamAnalyzer.php | 56 +++++++++++++++++++ .../Class_/ConstantListClassToEnumRector.php | 56 +++++++++++++------ 12 files changed, 214 insertions(+), 61 deletions(-) create mode 100644 packages/BetterPhpDocParser/Contract/PhpDocParser/PhpDocNodeDecoratorInterface.php create mode 100644 rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/multiple_params_change.php.inc create mode 100644 rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/skip_unknown_class.php.inc create mode 100644 rules/Php80/NodeAnalyzer/EnumParamAnalyzer.php diff --git a/easy-ci.php b/easy-ci.php index 976ccb06afc..acad2306ddf 100644 --- a/easy-ci.php +++ b/easy-ci.php @@ -3,6 +3,7 @@ declare(strict_types=1); use PHPStan\PhpDocParser\Parser\TypeParser; +use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface; use Rector\CodingStyle\Contract\ClassNameImport\ClassNameImportSkipVoterInterface; use Rector\Core\Contract\Console\OutputStyleInterface; use Rector\Core\Contract\PhpParser\Node\StmtsAwareInterface; @@ -40,6 +41,7 @@ return static function (ContainerConfigurator $containerConfigurator): void { $parameters = $containerConfigurator->parameters(); $parameters->set(Option::TYPES_TO_SKIP, [ + PhpDocNodeDecoratorInterface::class, Command::class, Application::class, RectorInterface::class, diff --git a/packages/BetterPhpDocParser/Contract/PhpDocParser/PhpDocNodeDecoratorInterface.php b/packages/BetterPhpDocParser/Contract/PhpDocParser/PhpDocNodeDecoratorInterface.php new file mode 100644 index 00000000000..7d556956015 --- /dev/null +++ b/packages/BetterPhpDocParser/Contract/PhpDocParser/PhpDocNodeDecoratorInterface.php @@ -0,0 +1,13 @@ +privatesCaller = new PrivatesCaller(); } public function parse(TokenIterator $tokenIterator): PhpDocNode @@ -60,15 +63,23 @@ public function parse(TokenIterator $tokenIterator): PhpDocNode $tokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC); $phpDocNode = new PhpDocNode($children); - // replace generic nodes with DoctrineAnnotations - $this->doctrineAnnotationDecorator->decorate($phpDocNode); - $this->constExprClassNameDecorator->decorate($phpDocNode); + + // decorate FQN classes etc. + $node = $this->currentNodeProvider->getNode(); + if (! $node instanceof Node) { + throw new ShouldNotHappenException(); + } + + foreach ($this->phpDocNodeDecorators as $phpDocNodeDecorator) { + $phpDocNodeDecorator->decorate($phpDocNode, $node); + } return $phpDocNode; } public function parseTag(TokenIterator $tokenIterator): PhpDocTagNode { + // replace generic nodes with DoctrineAnnotations if (! $tokenIterator instanceof BetterTokenIterator) { throw new ShouldNotHappenException(); } diff --git a/packages/BetterPhpDocParser/PhpDocParser/ConstExprClassNameDecorator.php b/packages/BetterPhpDocParser/PhpDocParser/ConstExprClassNameDecorator.php index 61585f00704..5cf032bf39c 100644 --- a/packages/BetterPhpDocParser/PhpDocParser/ConstExprClassNameDecorator.php +++ b/packages/BetterPhpDocParser/PhpDocParser/ConstExprClassNameDecorator.php @@ -9,9 +9,8 @@ use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; use PHPStan\PhpDocParser\Ast\Node; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode; +use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface; use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey; -use Rector\Core\Configuration\CurrentNodeProvider; -use Rector\Core\Exception\ShouldNotHappenException; use Rector\StaticTypeMapper\Naming\NameScopeFactory; use Symplify\Astral\PhpDocParser\PhpDocNodeTraverser; @@ -19,26 +18,19 @@ * Decorate node with fully qualified class name for const epxr, * e.g. Direction::* */ -final class ConstExprClassNameDecorator +final class ConstExprClassNameDecorator implements PhpDocNodeDecoratorInterface { public function __construct( - private CurrentNodeProvider $currentNodeProvider, - private NameScopeFactory $nameScopeFactory, - private PhpDocNodeTraverser $phpDocNodeTraverser + private readonly NameScopeFactory $nameScopeFactory, + private readonly PhpDocNodeTraverser $phpDocNodeTraverser ) { } - public function decorate(PhpDocNode $phpDocNode): void + public function decorate(PhpDocNode $phpDocNode, PhpNode $phpNode): void { - $phpNode = $this->currentNodeProvider->getNode(); - - if (! $phpNode instanceof PhpNode) { - throw new ShouldNotHappenException(); - } - $this->phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', function (Node $node) use ( $phpNode - ): int|Node|null { + ): Node|null { if (! $node instanceof ConstExprNode) { return null; } @@ -53,13 +45,13 @@ public function decorate(PhpDocNode $phpDocNode): void }); } - private function resolveFullyQualifiedClass(ConstExprNode $constExprNode, PhpNode $node): ?string + private function resolveFullyQualifiedClass(ConstExprNode $constExprNode, PhpNode $phpNode): ?string { if (! $constExprNode instanceof ConstFetchNode) { return null; } - $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($node); + $nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($phpNode); return $nameScope->resolveStringName($constExprNode->className); } } diff --git a/packages/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php b/packages/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php index e015e3a1ce2..0d1aa7ed393 100644 --- a/packages/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php +++ b/packages/BetterPhpDocParser/PhpDocParser/DoctrineAnnotationDecorator.php @@ -14,17 +14,16 @@ use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use PHPStan\PhpDocParser\Lexer\Lexer; use Rector\BetterPhpDocParser\Attributes\AttributeMirrorer; +use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface; use Rector\BetterPhpDocParser\PhpDoc\DoctrineAnnotationTagValueNode; use Rector\BetterPhpDocParser\PhpDoc\SpacelessPhpDocTagNode; use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory; use Rector\BetterPhpDocParser\ValueObject\DoctrineAnnotation\SilentKeyMap; use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey; use Rector\BetterPhpDocParser\ValueObject\StartAndEnd; -use Rector\Core\Configuration\CurrentNodeProvider; -use Rector\Core\Exception\ShouldNotHappenException; use Rector\Core\Util\StringUtils; -final class DoctrineAnnotationDecorator +final class DoctrineAnnotationDecorator implements PhpDocNodeDecoratorInterface { /** * Special short annotations, that are resolved as FQN by Doctrine annotation parser @@ -45,7 +44,6 @@ final class DoctrineAnnotationDecorator private const NESTED_ANNOTATION_END_REGEX = '#(\s+)?\}\)(\s+)?#'; public function __construct( - private readonly CurrentNodeProvider $currentNodeProvider, private readonly ClassAnnotationMatcher $classAnnotationMatcher, private readonly StaticDoctrineAnnotationParser $staticDoctrineAnnotationParser, private readonly TokenIteratorFactory $tokenIteratorFactory, @@ -53,16 +51,11 @@ public function __construct( ) { } - public function decorate(PhpDocNode $phpDocNode): void + public function decorate(PhpDocNode $phpDocNode, Node $phpNode): void { - $currentPhpNode = $this->currentNodeProvider->getNode(); - if (! $currentPhpNode instanceof Node) { - throw new ShouldNotHappenException(); - } - // merge split doctrine nested tags $this->mergeNestedDoctrineAnnotations($phpDocNode); - $this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $currentPhpNode); + $this->transformGenericTagValueNodesToDoctrineAnnotationTagValueNodes($phpDocNode, $phpNode); } /** diff --git a/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/change_param_type.php.inc b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/change_param_type.php.inc index 33669e3c40f..25d7c35f651 100644 --- a/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/change_param_type.php.inc +++ b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/change_param_type.php.inc @@ -24,10 +24,9 @@ use Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear; final class ChangeParamType { - public function changeGear(Gear $gear) + public function changeGear(\Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear $gear) { } } ?> - diff --git a/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/multiple_params_change.php.inc b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/multiple_params_change.php.inc new file mode 100644 index 00000000000..2630ecd0963 --- /dev/null +++ b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/multiple_params_change.php.inc @@ -0,0 +1,38 @@ + +----- + diff --git a/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/skip_unknown_class.php.inc b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/skip_unknown_class.php.inc new file mode 100644 index 00000000000..e340565fa3b --- /dev/null +++ b/rules-tests/Php80/Rector/Class_/ConstantListClassToEnumRector/Fixture/skip_unknown_class.php.inc @@ -0,0 +1,13 @@ +isPublic()) { - return false; - } + if (! $this->hasExclusivelyPublicClassConsts($classConstants)) { + return false; } // all constants must have exactly 1 value @@ -61,7 +59,7 @@ public function detect(Class_ $class): bool /** * @param ClassConst[] $classConsts - * @return string[] + * @return array> */ private function resolveClassConstTypes(array $classConsts): array { @@ -71,9 +69,23 @@ private function resolveClassConstTypes(array $classConsts): array foreach ($classConsts as $classConst) { $const = $classConst->consts[0]; $type = $this->nodeTypeResolver->getType($const->value); - $typeClasses[] = get_class($type); + $typeClasses[] = $type::class; } return array_unique($typeClasses); } + + /** + * @param ClassConst[] $classConsts + */ + private function hasExclusivelyPublicClassConsts(array $classConsts): bool + { + foreach ($classConsts as $classConst) { + if (! $classConst->isPublic()) { + return false; + } + } + + return true; + } } diff --git a/rules/Php80/NodeAnalyzer/EnumParamAnalyzer.php b/rules/Php80/NodeAnalyzer/EnumParamAnalyzer.php new file mode 100644 index 00000000000..91783b73cc0 --- /dev/null +++ b/rules/Php80/NodeAnalyzer/EnumParamAnalyzer.php @@ -0,0 +1,56 @@ +getParamTagValueByName($parameterReflection->getName()); + if (! $paramTagValueNode instanceof ParamTagValueNode) { + return null; + } + + if (! $paramTagValueNode->type instanceof ConstTypeNode) { + return null; + } + + $constTypeNode = $paramTagValueNode->type; + if (! $constTypeNode->constExpr instanceof ConstFetchNode) { + return null; + } + + $constExpr = $constTypeNode->constExpr; + $className = $constExpr->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS); + + if (! $this->reflectionProvider->hasClass($className)) { + return null; + } + + return $className; + } +} diff --git a/rules/Php80/Rector/Class_/ConstantListClassToEnumRector.php b/rules/Php80/Rector/Class_/ConstantListClassToEnumRector.php index 2235e2555e4..73d3106c4e6 100644 --- a/rules/Php80/Rector/Class_/ConstantListClassToEnumRector.php +++ b/rules/Php80/Rector/Class_/ConstantListClassToEnumRector.php @@ -5,18 +5,19 @@ namespace Rector\Php80\Rector\Class_; use PhpParser\Node; +use PhpParser\Node\Name\FullyQualified; +use PhpParser\Node\Param; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\ClassMethod; -use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode; -use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode; +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; +use PHPStan\Reflection\MethodReflection; use PHPStan\Reflection\ParametersAcceptorSelector; -use PHPStan\Reflection\Php\PhpParameterReflection; -use PHPStan\Type\UnionType; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; -use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey; +use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; use Rector\Core\Rector\AbstractRector; use Rector\Core\Reflection\ReflectionResolver; use Rector\Php80\NodeAnalyzer\EnumConstListClassDetector; +use Rector\Php80\NodeAnalyzer\EnumParamAnalyzer; use Rector\Php81\NodeFactory\EnumFactory; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -29,7 +30,9 @@ final class ConstantListClassToEnumRector extends AbstractRector public function __construct( private readonly EnumConstListClassDetector $enumConstListClassDetector, private readonly EnumFactory $enumFactory, + private readonly EnumParamAnalyzer $enumParamAnalyzer, private readonly ReflectionResolver $reflectionResolver, + private readonly PhpDocTagRemover $phpDocTagRemover ) { } @@ -96,28 +99,49 @@ private function refactorClassMethod(ClassMethod $classMethod): ?ClassMethod } $methodReflection = $this->reflectionResolver->resolveMethodReflectionFromClassMethod($classMethod); + if (! $methodReflection instanceof MethodReflection) { + return null; + } + + $hasNodeChanged = false; $parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants()); + foreach ($parametersAcceptor->getParameters() as $parameterReflection) { - if (! $parameterReflection instanceof PhpParameterReflection) { + $enumLikeClass = $this->enumParamAnalyzer->matchClassName($parameterReflection, $phpDocInfo); + if ($enumLikeClass === null) { continue; } - // should be union, that is how PHPStan resolves it - if (! $parameterReflection->getType() instanceof UnionType) { + $param = $this->getParamByName($classMethod, $parameterReflection->getName()); + if (! $param instanceof Param) { continue; } + // change and remove + $param->type = new FullyQualified($enumLikeClass); + $hasNodeChanged = true; + + /** @var ParamTagValueNode $paramTagValueNode */ $paramTagValueNode = $phpDocInfo->getParamTagValueByName($parameterReflection->getName()); - if ($paramTagValueNode->type instanceof ConstTypeNode) { - $constTypeNode = $paramTagValueNode->type; - if ($constTypeNode->constExpr instanceof ConstFetchNode) { - $constExpr = $constTypeNode->constExpr; - dump($constExpr->getAttribute(PhpDocAttributeKey::RESOLVED_CLASS)); - dump('___'); - die; - } + $this->phpDocTagRemover->removeTagValueFromNode($phpDocInfo, $paramTagValueNode); + } + + if ($hasNodeChanged) { + return $classMethod; + } + + return null; + } + + private function getParamByName(ClassMethod $classMethod, string $desiredParamName): ?Param + { + foreach ($classMethod->params as $param) { + if (! $this->nodeNameResolver->isName($param, $desiredParamName)) { + continue; } + + return $param; } return null;