-
-
Notifications
You must be signed in to change notification settings - Fork 699
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Updated Rector to commit d7bfc24bedc11374868f5a7ff3066eb847e80a8c
rectorphp/rector-src@d7bfc24 [CodeQuality] Add DynamicDocBlockPropertyToNativePropertyRector (#5691)
- Loading branch information
1 parent
b862c5c
commit 8defa24
Showing
5 changed files
with
265 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
<?php | ||
|
||
declare (strict_types=1); | ||
namespace Rector\CodeQuality\NodeFactory; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\ComplexType; | ||
use PhpParser\Node\Identifier; | ||
use PhpParser\Node\Name; | ||
use PhpParser\Node\NullableType; | ||
use PhpParser\Node\Stmt\Class_; | ||
use PhpParser\Node\Stmt\Property; | ||
use PhpParser\Node\Stmt\PropertyProperty; | ||
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; | ||
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind; | ||
use Rector\StaticTypeMapper\StaticTypeMapper; | ||
final class TypedPropertyFactory | ||
{ | ||
/** | ||
* @readonly | ||
* @var \Rector\StaticTypeMapper\StaticTypeMapper | ||
*/ | ||
private $staticTypeMapper; | ||
public function __construct(StaticTypeMapper $staticTypeMapper) | ||
{ | ||
$this->staticTypeMapper = $staticTypeMapper; | ||
} | ||
public function createFromPropertyTagValueNode(PropertyTagValueNode $propertyTagValueNode, Class_ $class, string $propertyName) : Property | ||
{ | ||
$propertyProperty = new PropertyProperty($propertyName); | ||
$propertyTypeNode = $this->createPropertyTypeNode($propertyTagValueNode, $class); | ||
return new Property(Class_::MODIFIER_PRIVATE, [$propertyProperty], [], $propertyTypeNode); | ||
} | ||
/** | ||
* @return \PhpParser\Node\Name|\PhpParser\Node\ComplexType|\PhpParser\Node\Identifier|null | ||
*/ | ||
public function createPropertyTypeNode(PropertyTagValueNode $propertyTagValueNode, Class_ $class, bool $isNullable = \true) | ||
{ | ||
$propertyType = $this->staticTypeMapper->mapPHPStanPhpDocTypeNodeToPHPStanType($propertyTagValueNode->type, $class); | ||
$typeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType, TypeKind::PROPERTY); | ||
if ($isNullable && !$typeNode instanceof NullableType && !$typeNode instanceof ComplexType && $typeNode instanceof Node) { | ||
return new NullableType($typeNode); | ||
} | ||
return $typeNode; | ||
} | ||
} |
213 changes: 213 additions & 0 deletions
213
rules/CodeQuality/Rector/Class_/DynamicDocBlockPropertyToNativePropertyRector.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
<?php | ||
|
||
declare (strict_types=1); | ||
namespace Rector\CodeQuality\Rector\Class_; | ||
|
||
use PhpParser\Node; | ||
use PhpParser\Node\Attribute; | ||
use PhpParser\Node\Expr; | ||
use PhpParser\Node\Stmt\Class_; | ||
use PhpParser\Node\Stmt\ClassMethod; | ||
use PhpParser\Node\Stmt\Property; | ||
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; | ||
use PHPStan\PhpDocParser\Ast\PhpDoc\PropertyTagValueNode; | ||
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; | ||
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; | ||
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover; | ||
use Rector\CodeQuality\NodeFactory\TypedPropertyFactory; | ||
use Rector\Comments\NodeDocBlock\DocBlockUpdater; | ||
use Rector\Doctrine\NodeAnalyzer\AttributeFinder; | ||
use Rector\PhpParser\Node\Value\ValueResolver; | ||
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer; | ||
use Rector\Rector\AbstractRector; | ||
use Rector\ValueObject\MethodName; | ||
use Rector\ValueObject\PhpVersionFeature; | ||
use Rector\VersionBonding\Contract\MinPhpVersionInterface; | ||
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | ||
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | ||
/** | ||
* @see \Rector\Tests\CodeQuality\Rector\Class_\DynamicDocBlockPropertyToNativePropertyRector\DynamicDocBlockPropertyToNativePropertyRectorTest | ||
*/ | ||
final class DynamicDocBlockPropertyToNativePropertyRector extends AbstractRector implements MinPhpVersionInterface | ||
{ | ||
/** | ||
* @readonly | ||
* @var \Rector\Doctrine\NodeAnalyzer\AttributeFinder | ||
*/ | ||
private $attributeFinder; | ||
/** | ||
* @readonly | ||
* @var \Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory | ||
*/ | ||
private $phpDocInfoFactory; | ||
/** | ||
* @readonly | ||
* @var \Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTagRemover | ||
*/ | ||
private $phpDocTagRemover; | ||
/** | ||
* @readonly | ||
* @var \Rector\Comments\NodeDocBlock\DocBlockUpdater | ||
*/ | ||
private $docBlockUpdater; | ||
/** | ||
* @readonly | ||
* @var \Rector\CodeQuality\NodeFactory\TypedPropertyFactory | ||
*/ | ||
private $typedPropertyFactory; | ||
/** | ||
* @readonly | ||
* @var \Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer | ||
*/ | ||
private $testsNodeAnalyzer; | ||
/** | ||
* @readonly | ||
* @var \Rector\PhpParser\Node\Value\ValueResolver | ||
*/ | ||
private $valueResolver; | ||
public function __construct(AttributeFinder $attributeFinder, PhpDocInfoFactory $phpDocInfoFactory, PhpDocTagRemover $phpDocTagRemover, DocBlockUpdater $docBlockUpdater, TypedPropertyFactory $typedPropertyFactory, TestsNodeAnalyzer $testsNodeAnalyzer, ValueResolver $valueResolver) | ||
{ | ||
$this->attributeFinder = $attributeFinder; | ||
$this->phpDocInfoFactory = $phpDocInfoFactory; | ||
$this->phpDocTagRemover = $phpDocTagRemover; | ||
$this->docBlockUpdater = $docBlockUpdater; | ||
$this->typedPropertyFactory = $typedPropertyFactory; | ||
$this->testsNodeAnalyzer = $testsNodeAnalyzer; | ||
$this->valueResolver = $valueResolver; | ||
} | ||
public function getRuleDefinition() : RuleDefinition | ||
{ | ||
return new RuleDefinition('Turn dynamic docblock properties on class with no parents to explicit ones', [new CodeSample(<<<'CODE_SAMPLE' | ||
/** | ||
* @property SomeDependency $someDependency | ||
*/ | ||
#[\AllowDynamicProperties] | ||
final class SomeClass | ||
{ | ||
public function __construct() | ||
{ | ||
$this->someDependency = new SomeDependency(); | ||
} | ||
} | ||
CODE_SAMPLE | ||
, <<<'CODE_SAMPLE' | ||
final class SomeClass | ||
{ | ||
private SomeDependency $someDependency; | ||
public function __construct() | ||
{ | ||
$this->someDependency = new SomeDependency(); | ||
} | ||
} | ||
CODE_SAMPLE | ||
)]); | ||
} | ||
/** | ||
* @return array<class-string<Node>> | ||
*/ | ||
public function getNodeTypes() : array | ||
{ | ||
return [Class_::class]; | ||
} | ||
/** | ||
* @param Class_ $node | ||
*/ | ||
public function refactor(Node $node) : ?Node | ||
{ | ||
$allowDynamicPropertiesAttribute = $this->attributeFinder->findAttributeByClass($node, 'AllowDynamicProperties'); | ||
if (!$allowDynamicPropertiesAttribute instanceof Attribute) { | ||
return null; | ||
} | ||
if ($this->shouldSkipClass($node)) { | ||
return null; | ||
} | ||
// 1. remove dynamic attribute, most likely any | ||
$node->attrGroups = []; | ||
// 2. add defined @property explicitly | ||
$classPhpDocInfo = $this->phpDocInfoFactory->createFromNode($node); | ||
if ($classPhpDocInfo instanceof PhpDocInfo) { | ||
$propertyPhpDocTagNodes = $classPhpDocInfo->getTagsByName('property'); | ||
$newProperties = $this->createNewPropertyFromPropertyTagValueNodes($propertyPhpDocTagNodes, $node); | ||
// remove property tags | ||
foreach ($propertyPhpDocTagNodes as $propertyPhpDocTagNode) { | ||
// remove from docblock | ||
$this->phpDocTagRemover->removeTagValueFromNode($classPhpDocInfo, $propertyPhpDocTagNode); | ||
} | ||
// merge new properties to start of the file | ||
$node->stmts = \array_merge($newProperties, $node->stmts); | ||
// update doc info | ||
$this->docBlockUpdater->updateRefactoredNodeWithPhpDocInfo($node); | ||
} | ||
return $node; | ||
} | ||
public function provideMinPhpVersion() : int | ||
{ | ||
return PhpVersionFeature::DEPRECATE_DYNAMIC_PROPERTIES; | ||
} | ||
/** | ||
* @param PhpDocTagNode[] $propertyPhpDocTagNodes | ||
* @return Property[] | ||
*/ | ||
private function createNewPropertyFromPropertyTagValueNodes(array $propertyPhpDocTagNodes, Class_ $class) : array | ||
{ | ||
$newProperties = []; | ||
foreach ($propertyPhpDocTagNodes as $propertyPhpDocTagNode) { | ||
// add explicit native property | ||
$propertyTagValueNode = $propertyPhpDocTagNode->value; | ||
if (!$propertyTagValueNode instanceof PropertyTagValueNode) { | ||
continue; | ||
} | ||
$propertyName = \ltrim($propertyTagValueNode->propertyName, '$'); | ||
if ($this->isPromotedProperty($class, $propertyName)) { | ||
continue; | ||
} | ||
// is property already defined? | ||
if ($class->getProperty($propertyName)) { | ||
// improve exising one type if needed | ||
$existingProperty = $class->getProperty($propertyName); | ||
if ($existingProperty->type !== null) { | ||
continue; | ||
} | ||
$defaultValue = $existingProperty->props[0]->default; | ||
$isNullable = $defaultValue instanceof Expr && $this->valueResolver->isNull($defaultValue); | ||
$existingProperty->type = $this->typedPropertyFactory->createPropertyTypeNode($propertyTagValueNode, $class, $isNullable); | ||
continue; | ||
} | ||
$newProperties[] = $this->typedPropertyFactory->createFromPropertyTagValueNode($propertyTagValueNode, $class, $propertyName); | ||
} | ||
return $newProperties; | ||
} | ||
private function shouldSkipClass(Class_ $class) : bool | ||
{ | ||
// skip magic | ||
$getClassMethod = $class->getMethod('__get'); | ||
if ($getClassMethod instanceof ClassMethod) { | ||
return \true; | ||
} | ||
$setClassMethod = $class->getMethod('__set'); | ||
if ($setClassMethod instanceof ClassMethod) { | ||
return \true; | ||
} | ||
if (!$class->extends instanceof Node) { | ||
return \false; | ||
} | ||
return !$this->testsNodeAnalyzer->isInTestClass($class); | ||
} | ||
private function isPromotedProperty(Class_ $class, string $propertyName) : bool | ||
{ | ||
$constructClassMethod = $class->getMethod(MethodName::CONSTRUCT); | ||
if ($constructClassMethod instanceof ClassMethod) { | ||
foreach ($constructClassMethod->params as $param) { | ||
if (!$param->flags) { | ||
continue; | ||
} | ||
$paramName = $this->getName($param->var); | ||
if ($paramName === $propertyName) { | ||
return \true; | ||
} | ||
} | ||
} | ||
return \false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters