Skip to content

Commit

Permalink
[TypeDeclaration] Fix TypedPropertyFromStrictGetterMethodReturnTypeRe…
Browse files Browse the repository at this point in the history
…ctor for default type (#3251)
  • Loading branch information
TomasVotruba committed Dec 24, 2022
1 parent bebea61 commit bce8153
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\TypeDeclaration\Rector\Property\TypedPropertyFromStrictGetterMethodReturnTypeRector\Fixture;

final class SkipDefaultValue
{
public $name = false;

public function getName(): string|null
{
return $this->name;
}
}
44 changes: 27 additions & 17 deletions rules/Privatization/Guard/ParentPropertyLookupGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ public function __construct(
) {
}

public function isLegal(Property $property): bool
public function isLegal(Property $property, ?Class_ $class = null): bool
{
$class = $this->betterNodeFinder->findParentType($property, Class_::class);

if (! $class instanceof Class_) {
return false;
// @todo optimize
$class = $this->betterNodeFinder->findParentType($property, Class_::class);
if (! $class instanceof Class_) {
return false;
}
}

$classReflection = $this->reflectionResolver->resolveClassReflection($property);
Expand All @@ -55,24 +57,14 @@ public function isLegal(Property $property): bool
}

$className = $classReflection->getName();
$parents = $classReflection->getParents();
$parentClassReflections = $classReflection->getParents();

// parent class not autoloaded
if ($parents === []) {
if ($parentClassReflections === []) {
return false;
}

foreach ($parents as $parent) {
if ($parent->hasProperty($propertyName)) {
return false;
}

if ($this->isFoundInParentClassMethods($parent, $propertyName, $className)) {
return false;
}
}

return true;
return $this->isGuardedByParents($parentClassReflections, $propertyName, $className);
}

private function isFoundInParentClassMethods(
Expand Down Expand Up @@ -132,4 +124,22 @@ private function isFoundInMethodStmts(array $stmts, string $propertyName, string
return $this->nodeNameResolver->isName($subNode->name, $propertyName);
});
}

/**
* @param ClassReflection[] $parentClassReflections
*/
private function isGuardedByParents(array $parentClassReflections, string $propertyName, string $className): bool
{
foreach ($parentClassReflections as $parentClassReflection) {
if ($parentClassReflection->hasProperty($propertyName)) {
return false;
}

if ($this->isFoundInParentClassMethods($parentClassReflection, $propertyName, $className)) {
return false;
}
}

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

use PhpParser\Node;
use PhpParser\Node\Expr;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Core\Php\PhpVersionProvider;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\DeadCode\PhpDoc\TagRemover\VarTagRemover;
Expand All @@ -29,10 +29,8 @@ final class TypedPropertyFromStrictGetterMethodReturnTypeRector extends Abstract
{
public function __construct(
private readonly GetterTypeDeclarationPropertyTypeInferer $getterTypeDeclarationPropertyTypeInferer,
private readonly PhpDocTypeChanger $phpDocTypeChanger,
private readonly VarTagRemover $varTagRemover,
private readonly ParentPropertyLookupGuard $parentPropertyLookupGuard,
private readonly PhpVersionProvider $phpVersionProvider,
) {
}

Expand Down Expand Up @@ -74,54 +72,61 @@ public function getName(): string|null
*/
public function getNodeTypes(): array
{
return [Property::class];
return [Class_::class];
}

/**
* @param Property $node
* @param Class_ $node
*/
public function refactor(Node $node): ?Property
public function refactor(Node $node): null|Class_
{
if ($node->type !== null) {
return null;
$hasChanged = false;

foreach ($node->getProperties() as $property) {
if ($this->shouldSkipProperty($property, $node)) {
continue;
}

$getterReturnType = $this->getterTypeDeclarationPropertyTypeInferer->inferProperty($property);
if (! $getterReturnType instanceof Type) {
continue;
}

if ($getterReturnType instanceof MixedType) {
continue;
}

// if property is public, it should be nullable
if ($property->isPublic() && ! TypeCombinator::containsNull($getterReturnType)) {
$getterReturnType = TypeCombinator::addNull($getterReturnType);
}

$propertyTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode(
$getterReturnType,
TypeKind::PROPERTY
);
if (! $propertyTypeNode instanceof Node) {
continue;
}

// include fault value in the type
if ($this->isConclictingDefaultExprType($property, $getterReturnType)) {
continue;
}

$property->type = $propertyTypeNode;
$this->decorateDefaultNull($getterReturnType, $property);

$this->refactorPhpDoc($property);

$hasChanged = true;
}

if (! $this->parentPropertyLookupGuard->isLegal($node)) {
return null;
}

$getterReturnType = $this->getterTypeDeclarationPropertyTypeInferer->inferProperty($node);
if (! $getterReturnType instanceof Type) {
return null;
}

if ($getterReturnType instanceof MixedType) {
return null;
}

if (! $this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::TYPED_PROPERTIES)) {
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
$this->phpDocTypeChanger->changeVarType($phpDocInfo, $getterReturnType);
if ($hasChanged) {
return $node;
}

// if property is public, it should be nullable
if ($node->isPublic() && ! TypeCombinator::containsNull($getterReturnType)) {
$getterReturnType = TypeCombinator::addNull($getterReturnType);
}

$propertyType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($getterReturnType, TypeKind::PROPERTY);
if (! $propertyType instanceof Node) {
return null;
}

$node->type = $propertyType;
$this->decorateDefaultNull($getterReturnType, $node);

$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($node);
$this->varTagRemover->removeVarTagIfUseless($phpDocInfo, $node);

return $node;
return null;
}

public function provideMinPhpVersion(): int
Expand All @@ -142,4 +147,44 @@ private function decorateDefaultNull(Type $propertyType, Property $property): vo

$propertyProperty->default = $this->nodeFactory->createNull();
}

private function isConclictingDefaultExprType(Property $property, Type $getterReturnType): bool
{
$onlyPropertyProperty = $property->props[0];
if (! $onlyPropertyProperty->default instanceof Expr) {
return false;
}

$defaultType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($onlyPropertyProperty->default);

// does default type match the getter one?
return ! $defaultType->isSuperTypeOf($getterReturnType)
->yes();
}

private function refactorPhpDoc(Property $property): void
{
$phpDocInfo = $this->phpDocInfoFactory->createFromNode($property);
if (! $phpDocInfo instanceof PhpDocInfo) {
return;
}

$this->varTagRemover->removeVarTagIfUseless($phpDocInfo, $property);
}

private function shouldSkipProperty(Property $property, Class_ $class): bool
{
if ($property->type instanceof Node) {
// already has type
return true;
}

// skip non-single property
if (count($property->props) !== 1) {
// has too many properties
return true;
}

return ! $this->parentPropertyLookupGuard->isLegal($property, $class);
}
}

0 comments on commit bce8153

Please sign in to comment.