[PHP 8.0] Add method param to ConstantListClassToEnumRector (#2415)

* add skipped fixtures

* skip non-scalar types

* prepare parameter update

* add ConstExprClassNameDecorator, add PhpDocNodeDecoratorInterface
This commit is contained in:
Tomas Votruba 2022-06-02 10:58:19 +02:00 committed by GitHub
parent ba0869a9a5
commit a73dafd30a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 407 additions and 35 deletions

View File

@ -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 @@ use Symplify\EasyCI\ValueObject\Option;
return static function (ContainerConfigurator $containerConfigurator): void {
$parameters = $containerConfigurator->parameters();
$parameters->set(Option::TYPES_TO_SKIP, [
PhpDocNodeDecoratorInterface::class,
Command::class,
Application::class,
RectorInterface::class,

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Rector\BetterPhpDocParser\Contract\PhpDocParser;
use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
interface PhpDocNodeDecoratorInterface
{
public function decorate(PhpDocNode $phpDocNode, Node $phpNode): void;
}

View File

@ -35,7 +35,7 @@ final class PhpDocInfoFactory
private readonly StaticTypeMapper $staticTypeMapper,
private readonly AnnotationNaming $annotationNaming,
private readonly RectorChangeCollector $rectorChangeCollector,
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder
private readonly PhpDocNodeByTypeFinder $phpDocNodeByTypeFinder,
) {
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Rector\BetterPhpDocParser\PhpDocParser;
use PhpParser\Node;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
@ -14,10 +15,12 @@ use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
use PHPStan\PhpDocParser\Parser\TokenIterator;
use PHPStan\PhpDocParser\Parser\TypeParser;
use Rector\BetterPhpDocParser\Contract\PhpDocParser\PhpDocNodeDecoratorInterface;
use Rector\BetterPhpDocParser\PhpDocInfo\TokenIteratorFactory;
use Rector\BetterPhpDocParser\ValueObject\Parser\BetterTokenIterator;
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
use Rector\BetterPhpDocParser\ValueObject\StartAndEnd;
use Rector\Core\Configuration\CurrentNodeProvider;
use Rector\Core\Exception\ShouldNotHappenException;
use Symplify\PackageBuilder\Reflection\PrivatesCaller;
@ -26,17 +29,18 @@ use Symplify\PackageBuilder\Reflection\PrivatesCaller;
*/
final class BetterPhpDocParser extends PhpDocParser
{
private readonly PrivatesCaller $privatesCaller;
/**
* @param PhpDocNodeDecoratorInterface[] $phpDocNodeDecorators
*/
public function __construct(
TypeParser $typeParser,
ConstExprParser $constExprParser,
private readonly CurrentNodeProvider $currentNodeProvider,
private readonly TokenIteratorFactory $tokenIteratorFactory,
private readonly DoctrineAnnotationDecorator $doctrineAnnotationDecorator
private readonly array $phpDocNodeDecorators,
private readonly PrivatesCaller $privatesCaller = new PrivatesCaller(),
) {
parent::__construct($typeParser, $constExprParser);
$this->privatesCaller = new PrivatesCaller();
}
public function parse(TokenIterator $tokenIterator): PhpDocNode
@ -59,14 +63,23 @@ final class BetterPhpDocParser extends PhpDocParser
$tokenIterator->tryConsumeTokenType(Lexer::TOKEN_CLOSE_PHPDOC);
$phpDocNode = new PhpDocNode($children);
// replace generic nodes with DoctrineAnnotations
$this->doctrineAnnotationDecorator->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();
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Rector\BetterPhpDocParser\PhpDocParser;
use PhpParser\Node as PhpNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNode;
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\StaticTypeMapper\Naming\NameScopeFactory;
use Symplify\Astral\PhpDocParser\PhpDocNodeTraverser;
/**
* Decorate node with fully qualified class name for const epxr,
* e.g. Direction::*
*/
final class ConstExprClassNameDecorator implements PhpDocNodeDecoratorInterface
{
public function __construct(
private readonly NameScopeFactory $nameScopeFactory,
private readonly PhpDocNodeTraverser $phpDocNodeTraverser
) {
}
public function decorate(PhpDocNode $phpDocNode, PhpNode $phpNode): void
{
$this->phpDocNodeTraverser->traverseWithCallable($phpDocNode, '', function (Node $node) use (
$phpNode
): Node|null {
if (! $node instanceof ConstExprNode) {
return null;
}
$className = $this->resolveFullyQualifiedClass($node, $phpNode);
if ($className === null) {
return null;
}
$node->setAttribute(PhpDocAttributeKey::RESOLVED_CLASS, $className);
return $node;
});
}
private function resolveFullyQualifiedClass(ConstExprNode $constExprNode, PhpNode $phpNode): ?string
{
if (! $constExprNode instanceof ConstFetchNode) {
return null;
}
$nameScope = $this->nameScopeFactory->createNameScopeFromNodeWithoutTemplateTypes($phpNode);
return $nameScope->resolveStringName($constExprNode->className);
}
}

View File

@ -14,17 +14,16 @@ use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTextNode;
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 @@ final class DoctrineAnnotationDecorator
) {
}
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);
}
/**

View File

@ -0,0 +1,32 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear;
final class ChangeParamType
{
/**
* @param Gear::* $gear
*/
public function changeGear(string $gear)
{
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear;
final class ChangeParamType
{
public function changeGear(\Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear $gear)
{
}
}
?>

View File

@ -0,0 +1,38 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear;
final class MultipleParamsChange
{
/**
* @param string $carType
* @param Gear::* $gear
* @param int $speed
*/
public function changeGear($carType, string $gear, $speed)
{
}
}
?>
-----
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
use Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear;
final class MultipleParamsChange
{
/**
* @param string $carType
* @param int $speed
*/
public function changeGear($carType, \Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source\Gear $gear, $speed)
{
}
}
?>

View File

@ -0,0 +1,12 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
final class SkipAlsoOtherElements
{
public const LEFT = 'left';
public const RIGHT = 'right';
protected $value;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
final class SkipDifferentType
{
public const LEFT = 'left';
public const RIGHT = 5;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
final class SkipNonPublicConst
{
public const LEFT = 'left';
protected const RIGHT = 5;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
final class SkipNonScalarTypes
{
public const LEFT = self::class;
public const RIGHT = self::class;
}

View File

@ -0,0 +1,13 @@
<?php
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Fixture;
final class SkipUnknownClass
{
/**
* @param AnythingNonExisting::* $gear
*/
public function changeGear(string $gear)
{
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Rector\Tests\Php80\Rector\Class_\ConstantListClassToEnumRector\Source;
final class Gear
{
public const FIRST = 'first';
public const SECOND = 'second';
}

View File

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Rector\Php80\NodeAnalyzer;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\NodeTypeResolver;
final class EnumConstListClassDetector
@ -30,10 +32,8 @@ final class EnumConstListClassDetector
}
// all constant must be public
foreach ($classConstants as $classConstant) {
if (! $classConstant->isPublic()) {
return false;
}
if (! $this->hasExclusivelyPublicClassConsts($classConstants)) {
return false;
}
// all constants must have exactly 1 value
@ -43,26 +43,49 @@ final class EnumConstListClassDetector
}
}
$constantUniqueTypeCount = $this->resolveConstantUniqueTypeCount($classConstants);
// only scalar values are allowed
foreach ($classConstants as $classConstant) {
$onlyConstConst = $classConstant->consts[0];
if (! $onlyConstConst->value instanceof Scalar) {
return false;
}
}
$uniqueTypeClasses = $this->resolveClassConstTypes($classConstants);
// must be exactly 1 type
return $constantUniqueTypeCount === 1;
return count($uniqueTypeClasses) === 1;
}
/**
* @param ClassConst[] $classConsts
* @return array<class-string<Type>>
*/
private function resolveConstantUniqueTypeCount(array $classConsts): int
private function resolveClassConstTypes(array $classConsts): array
{
$typeClasses = [];
// all constants must have same type
foreach ($classConsts as $classConst) {
$const = $classConst->consts[0];
$constantType = $this->nodeTypeResolver->getType($const->value);
$typeClasses[] = $constantType::class;
$type = $this->nodeTypeResolver->getType($const->value);
$typeClasses[] = $type::class;
}
$uniqueTypeClasses = array_unique($typeClasses);
return count($uniqueTypeClasses);
return array_unique($typeClasses);
}
/**
* @param ClassConst[] $classConsts
*/
private function hasExclusivelyPublicClassConsts(array $classConsts): bool
{
foreach ($classConsts as $classConst) {
if (! $classConst->isPublic()) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Rector\Php80\NodeAnalyzer;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstFetchNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\ConstTypeNode;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\Php\PhpParameterReflection;
use PHPStan\Reflection\ReflectionProvider;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\ValueObject\PhpDocAttributeKey;
/**
* Detects enum-like params, e.g.
* Direction::*
*/
final class EnumParamAnalyzer
{
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
) {
}
public function matchClassName(ParameterReflection $parameterReflection, PhpDocInfo $phpDocInfo): ?string
{
if (! $parameterReflection instanceof PhpParameterReflection) {
return null;
}
$paramTagValueNode = $phpDocInfo->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;
}
}

View File

@ -5,9 +5,19 @@ declare(strict_types=1);
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\PhpDoc\ParamTagValueNode;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
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;
@ -19,7 +29,10 @@ final class ConstantListClassToEnumRector extends AbstractRector
{
public function __construct(
private readonly EnumConstListClassDetector $enumConstListClassDetector,
private readonly EnumFactory $enumFactory
private readonly EnumFactory $enumFactory,
private readonly EnumParamAnalyzer $enumParamAnalyzer,
private readonly ReflectionResolver $reflectionResolver,
private readonly PhpDocTagRemover $phpDocTagRemover
) {
}
@ -54,18 +67,83 @@ CODE_SAMPLE
*/
public function getNodeTypes(): array
{
return [Class_::class];
return [Class_::class, ClassMethod::class];
}
/**
* @param Class_ $node
* @param Class_|ClassMethod $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->enumConstListClassDetector->detect($node)) {
if ($node instanceof Class_) {
if (! $this->enumConstListClassDetector->detect($node)) {
return null;
}
return $this->enumFactory->createFromClass($node);
}
return $this->refactorClassMethod($node);
}
private function refactorClassMethod(ClassMethod $classMethod): ?ClassMethod
{
if ($classMethod->params === []) {
return null;
}
return $this->enumFactory->createFromClass($node);
// enum param types doc requires a docblock
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($classMethod);
if (! $phpDocInfo instanceof PhpDocInfo) {
return null;
}
$methodReflection = $this->reflectionResolver->resolveMethodReflectionFromClassMethod($classMethod);
if (! $methodReflection instanceof MethodReflection) {
return null;
}
$hasNodeChanged = false;
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
foreach ($parametersAcceptor->getParameters() as $parameterReflection) {
$enumLikeClass = $this->enumParamAnalyzer->matchClassName($parameterReflection, $phpDocInfo);
if ($enumLikeClass === null) {
continue;
}
$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());
$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;
}
}