2019-05-26 19:21:31 +00:00
|
|
|
<?php declare(strict_types=1);
|
|
|
|
|
|
|
|
namespace Rector\CodeQuality\Rector\Class_;
|
|
|
|
|
|
|
|
use PhpParser\Node;
|
|
|
|
use PhpParser\Node\Expr\Assign;
|
|
|
|
use PhpParser\Node\Expr\PropertyFetch;
|
2019-07-10 07:23:37 +00:00
|
|
|
use PhpParser\Node\Expr\StaticCall;
|
2019-07-19 13:59:41 +00:00
|
|
|
use PhpParser\Node\Expr\Variable;
|
2019-05-26 19:21:31 +00:00
|
|
|
use PhpParser\Node\Stmt\Class_;
|
2019-07-10 07:23:37 +00:00
|
|
|
use PhpParser\Node\Stmt\ClassMethod;
|
2019-05-26 19:21:31 +00:00
|
|
|
use PhpParser\Node\Stmt\Property;
|
2019-09-06 10:30:58 +00:00
|
|
|
use PHPStan\Type\MixedType;
|
|
|
|
use PHPStan\Type\Type;
|
2019-05-26 19:21:31 +00:00
|
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
2019-09-06 10:30:58 +00:00
|
|
|
use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory;
|
2019-05-26 19:21:31 +00:00
|
|
|
use Rector\Rector\AbstractRector;
|
|
|
|
use Rector\RectorDefinition\CodeSample;
|
|
|
|
use Rector\RectorDefinition\RectorDefinition;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see https://3v4l.org/GL6II
|
|
|
|
* @see https://3v4l.org/eTrhZ
|
|
|
|
* @see https://3v4l.org/C554W
|
2019-09-06 10:30:58 +00:00
|
|
|
*
|
2019-09-03 09:11:45 +00:00
|
|
|
* @see \Rector\CodeQuality\Tests\Rector\Class_\CompleteDynamicPropertiesRector\CompleteDynamicPropertiesRectorTest
|
2019-05-26 19:21:31 +00:00
|
|
|
*/
|
|
|
|
final class CompleteDynamicPropertiesRector extends AbstractRector
|
|
|
|
{
|
2019-07-10 07:23:37 +00:00
|
|
|
/**
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private const LARAVEL_COLLECTION_CLASS = 'Illuminate\Support\Collection';
|
|
|
|
|
2019-05-26 19:21:31 +00:00
|
|
|
/**
|
2019-09-06 10:30:58 +00:00
|
|
|
* @var TypeFactory
|
2019-05-26 19:21:31 +00:00
|
|
|
*/
|
2019-09-06 10:30:58 +00:00
|
|
|
private $typeFactory;
|
2019-05-26 19:21:31 +00:00
|
|
|
|
2019-09-19 15:29:30 +00:00
|
|
|
public function __construct(TypeFactory $typeFactory)
|
2019-09-04 09:49:25 +00:00
|
|
|
{
|
2019-09-06 10:30:58 +00:00
|
|
|
$this->typeFactory = $typeFactory;
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public function getDefinition(): RectorDefinition
|
|
|
|
{
|
|
|
|
return new RectorDefinition('Add missing dynamic properties', [
|
|
|
|
new CodeSample(
|
2019-09-18 06:14:35 +00:00
|
|
|
<<<'PHP'
|
2019-05-26 19:21:31 +00:00
|
|
|
class SomeClass
|
|
|
|
{
|
|
|
|
public function set()
|
|
|
|
{
|
|
|
|
$this->value = 5;
|
|
|
|
}
|
|
|
|
}
|
2019-09-18 06:14:35 +00:00
|
|
|
PHP
|
2019-05-26 19:21:31 +00:00
|
|
|
,
|
2019-09-18 06:14:35 +00:00
|
|
|
<<<'PHP'
|
2019-05-26 19:21:31 +00:00
|
|
|
class SomeClass
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var int
|
|
|
|
*/
|
|
|
|
public $value;
|
|
|
|
public function set()
|
|
|
|
{
|
|
|
|
$this->value = 5;
|
|
|
|
}
|
|
|
|
}
|
2019-09-18 06:14:35 +00:00
|
|
|
PHP
|
2019-05-26 19:21:31 +00:00
|
|
|
),
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string[]
|
|
|
|
*/
|
|
|
|
public function getNodeTypes(): array
|
|
|
|
{
|
|
|
|
return [Class_::class];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param Class_ $node
|
|
|
|
*/
|
|
|
|
public function refactor(Node $node): ?Node
|
|
|
|
{
|
2019-07-10 07:23:37 +00:00
|
|
|
if (! $this->isNonAnonymousClass($node)) {
|
2019-05-31 06:08:39 +00:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-06-15 23:15:35 +00:00
|
|
|
/** @var string $class */
|
|
|
|
$class = $this->getName($node);
|
2019-07-10 07:23:37 +00:00
|
|
|
// properties are accessed via magic, nothing we can do
|
2019-06-15 23:15:35 +00:00
|
|
|
if (method_exists($class, '__set') || method_exists($class, '__get')) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-07-10 07:23:37 +00:00
|
|
|
// special case for Laravel Collection macro magic
|
2019-09-06 10:30:58 +00:00
|
|
|
$fetchedLocalPropertyNameToTypes = $this->resolveFetchedLocalPropertyNameToType($node);
|
2019-05-26 19:21:31 +00:00
|
|
|
|
2019-07-10 07:23:37 +00:00
|
|
|
$propertyNames = $this->getClassPropertyNames($node);
|
2019-05-26 19:21:31 +00:00
|
|
|
|
|
|
|
$fetchedLocalPropertyNames = array_keys($fetchedLocalPropertyNameToTypes);
|
|
|
|
$propertiesToComplete = array_diff($fetchedLocalPropertyNames, $propertyNames);
|
|
|
|
|
2019-05-31 06:08:39 +00:00
|
|
|
// remove other properties that are accessible from this scope
|
|
|
|
/** @var string $class */
|
|
|
|
$class = $this->getName($node);
|
|
|
|
foreach ($propertiesToComplete as $key => $propertyToComplete) {
|
|
|
|
if (! property_exists($class, $propertyToComplete)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($propertiesToComplete[$key]);
|
|
|
|
}
|
|
|
|
|
2019-05-26 19:21:31 +00:00
|
|
|
$newProperties = $this->createNewProperties($fetchedLocalPropertyNameToTypes, $propertiesToComplete);
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
$node->stmts = array_merge($newProperties, $node->stmts);
|
2019-05-26 19:21:31 +00:00
|
|
|
|
|
|
|
return $node;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-06 10:30:58 +00:00
|
|
|
* @param Type[] $fetchedLocalPropertyNameToTypes
|
2019-05-26 19:21:31 +00:00
|
|
|
* @param string[] $propertiesToComplete
|
|
|
|
* @return Property[]
|
|
|
|
*/
|
|
|
|
private function createNewProperties(array $fetchedLocalPropertyNameToTypes, array $propertiesToComplete): array
|
|
|
|
{
|
|
|
|
$newProperties = [];
|
2019-09-06 10:30:58 +00:00
|
|
|
foreach ($fetchedLocalPropertyNameToTypes as $propertyName => $propertyType) {
|
2019-05-26 19:21:31 +00:00
|
|
|
if (! in_array($propertyName, $propertiesToComplete, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2019-09-04 12:10:29 +00:00
|
|
|
$propertyBuilder = $this->builderFactory->property($propertyName);
|
|
|
|
$propertyBuilder->makePublic();
|
2019-09-06 10:30:58 +00:00
|
|
|
$property = $propertyBuilder->getNode();
|
|
|
|
|
|
|
|
if ($this->isAtLeastPhpVersion('7.4')) {
|
|
|
|
$phpStanNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($propertyType);
|
|
|
|
if ($phpStanNode) {
|
|
|
|
$property->type = $phpStanNode;
|
|
|
|
} else {
|
|
|
|
// fallback to doc type in PHP 7.4
|
|
|
|
$this->docBlockManipulator->changeVarTag($property, $propertyType);
|
2019-07-10 07:02:31 +00:00
|
|
|
}
|
2019-09-06 10:30:58 +00:00
|
|
|
} else {
|
|
|
|
$this->docBlockManipulator->changeVarTag($property, $propertyType);
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
$newProperties[] = $property;
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|
2019-09-06 10:30:58 +00:00
|
|
|
|
2019-05-26 19:21:31 +00:00
|
|
|
return $newProperties;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-09-06 10:30:58 +00:00
|
|
|
* @return Type[]
|
2019-05-26 19:21:31 +00:00
|
|
|
*/
|
2019-09-06 10:30:58 +00:00
|
|
|
private function resolveFetchedLocalPropertyNameToType(Class_ $class): array
|
2019-05-26 19:21:31 +00:00
|
|
|
{
|
|
|
|
$fetchedLocalPropertyNameToTypes = [];
|
|
|
|
|
2019-06-04 20:34:59 +00:00
|
|
|
$this->traverseNodesWithCallable($class->stmts, function (Node $node) use (
|
2019-05-26 19:21:31 +00:00
|
|
|
&$fetchedLocalPropertyNameToTypes
|
|
|
|
) {
|
|
|
|
if (! $node instanceof PropertyFetch) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! $this->isName($node->var, 'this')) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-07-10 07:23:37 +00:00
|
|
|
// special Laravel collection scope
|
|
|
|
if ($this->shouldSkipForLaravelCollection($node)) {
|
|
|
|
return null;
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|
|
|
|
|
2019-07-19 13:59:41 +00:00
|
|
|
if ($node->name instanceof Variable) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-05-26 19:21:31 +00:00
|
|
|
$propertyName = $this->getName($node->name);
|
2019-07-19 13:59:41 +00:00
|
|
|
if ($propertyName === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-07-10 07:23:37 +00:00
|
|
|
$propertyFetchType = $this->resolvePropertyFetchType($node);
|
2019-05-26 19:21:31 +00:00
|
|
|
|
|
|
|
$fetchedLocalPropertyNameToTypes[$propertyName][] = $propertyFetchType;
|
|
|
|
});
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
// normalize types to union
|
|
|
|
$fetchedLocalPropertyNameToType = [];
|
|
|
|
foreach ($fetchedLocalPropertyNameToTypes as $name => $types) {
|
|
|
|
$fetchedLocalPropertyNameToType[$name] = $this->typeFactory->createMixedPassedOrUnionType($types);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $fetchedLocalPropertyNameToType;
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|
2019-07-10 07:23:37 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @return string[]
|
|
|
|
*/
|
|
|
|
private function getClassPropertyNames(Class_ $class): array
|
|
|
|
{
|
|
|
|
$propertyNames = [];
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
foreach ($class->getProperties() as $property) {
|
|
|
|
$propertyNames[] = $this->getName($property);
|
|
|
|
}
|
2019-07-10 07:23:37 +00:00
|
|
|
|
|
|
|
return $propertyNames;
|
|
|
|
}
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
private function resolvePropertyFetchType(Node $node): Type
|
2019-07-10 07:23:37 +00:00
|
|
|
{
|
|
|
|
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
|
|
|
|
|
|
|
|
// possible get type
|
|
|
|
if ($parentNode instanceof Assign) {
|
2019-09-06 10:30:58 +00:00
|
|
|
return $this->getStaticType($parentNode->expr);
|
2019-07-10 07:23:37 +00:00
|
|
|
}
|
|
|
|
|
2019-09-06 10:30:58 +00:00
|
|
|
return new MixedType();
|
2019-07-10 07:23:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private function shouldSkipForLaravelCollection(Node $node): bool
|
|
|
|
{
|
|
|
|
$staticCallOrClassMethod = $this->betterNodeFinder->findFirstAncestorInstancesOf(
|
|
|
|
$node,
|
|
|
|
[ClassMethod::class, StaticCall::class]
|
|
|
|
);
|
|
|
|
|
|
|
|
if (! $staticCallOrClassMethod instanceof StaticCall) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->isName($staticCallOrClassMethod->class, self::LARAVEL_COLLECTION_CLASS);
|
|
|
|
}
|
2019-05-26 19:21:31 +00:00
|
|
|
}
|