[PHP 7.4] Prevent child typed property override by abstract

This commit is contained in:
TomasVotruba 2020-06-07 00:12:54 +02:00
parent 2f5440760d
commit 53e9cc6c06
10 changed files with 179 additions and 80 deletions

View File

@ -10,6 +10,7 @@ use PhpParser\Node\Stmt\Interface_;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\NodeCollector\NodeCollector\ParsedNodeCollector;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
abstract class AbstractNodeVendorLockResolver
{
@ -41,14 +42,19 @@ abstract class AbstractNodeVendorLockResolver
$this->nodeNameResolver = $nodeNameResolver;
}
protected function hasParentClassOrImplementsInterface(ClassLike $classLike): bool
protected function hasParentClassChildrenClassesOrImplementsInterface(ClassLike $classLike): bool
{
if (($classLike instanceof Class_ || $classLike instanceof Interface_) && $classLike->extends) {
return true;
}
if ($classLike instanceof Class_) {
return (bool) $classLike->implements;
if ((bool) $classLike->implements) {
return true;
}
/** @var Class_ $classLike */
return $this->getChildrenClassesByClass($classLike) !== [];
}
return false;
@ -72,6 +78,32 @@ abstract class AbstractNodeVendorLockResolver
return false;
}
/**
* @return string[]
*/
protected function getChildrenClassesByClass(Class_ $class): array
{
$desiredClassName = $class->getAttribute(AttributeKey::CLASS_NAME);
if ($desiredClassName === null) {
return [];
}
$childrenClasses = [];
foreach (get_declared_classes() as $declaredClass) {
if ($declaredClass === $desiredClassName) {
continue;
}
if (! is_a($declaredClass, $desiredClassName, true)) {
continue;
}
$childrenClasses[] = $declaredClass;
}
return $childrenClasses;
}
private function hasInterfaceMethod(string $methodName, string $interfaceName): bool
{
if (! interface_exists($interfaceName)) {

View File

@ -19,7 +19,7 @@ final class ClassMethodParamVendorLockResolver extends AbstractNodeVendorLockRes
return false;
}
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
if (! $this->hasParentClassChildrenClassesOrImplementsInterface($classNode)) {
return false;
}

View File

@ -18,7 +18,7 @@ final class ClassMethodReturnVendorLockResolver extends AbstractNodeVendorLockRe
return false;
}
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
if (! $this->hasParentClassChildrenClassesOrImplementsInterface($classNode)) {
return false;
}

View File

@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Rector\VendorLocker\NodeVendorLocker;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Property;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class PropertyTypeVendorLockResolver extends AbstractNodeVendorLockResolver
{
public function isVendorLocked(Property $property): bool
{
/** @var Class_|null $classNode */
$classNode = $property->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return false;
}
/** @var Class_|Interface_ $classNode */
if (! $this->hasParentClassChildrenClassesOrImplementsInterface($classNode)) {
return false;
}
/** @var string|null $propertyName */
$propertyName = $this->nodeNameResolver->getName($property);
if (! is_string($propertyName)) {
throw new ShouldNotHappenException();
}
if ($this->isParentClassLocked($classNode, $propertyName)) {
return true;
}
return $this->isChildClassLocked($property, $classNode, $propertyName);
}
/**
* @param Class_|Interface_ $classLike
*/
private function isParentClassLocked(ClassLike $classLike, string $propertyName): bool
{
if (! $classLike instanceof Class_) {
return false;
}
// extract to some "inherited parent method" service
/** @var string|null $parentClassName */
$parentClassName = $classLike->getAttribute(AttributeKey::PARENT_CLASS_NAME);
if ($parentClassName === null) {
return false;
}
// if not, look for it's parent parent - recursion
if (property_exists($parentClassName, $propertyName)) {
// validate type is conflicting
// parent class property in external scope → it's not ok
return true;
// if not, look for it's parent parent
}
return false;
}
/**
* @param Class_|Interface_ $classLike
*/
private function isChildClassLocked(Property $property, ClassLike $classLike, string $propertyName): bool
{
if (! $classLike instanceof Class_) {
return false;
}
// is child class locker
if ($property->isPrivate()) {
return false;
}
$childrenClassNames = $this->getChildrenClassesByClass($classLike);
foreach ($childrenClassNames as $childClassName) {
if (property_exists($childClassName, $propertyName)) {
return true;
}
}
return false;
}
}

View File

@ -1,68 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\VendorLocker\NodeVendorLocker;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class PropertyVendorLockResolver extends AbstractNodeVendorLockResolver
{
public function isVendorLocked(Property $property): bool
{
/** @var Class_|null $classNode */
$classNode = $property->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return false;
}
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
return false;
}
/** @var string|null $propertyName */
$propertyName = $this->nodeNameResolver->getName($property);
if (! is_string($propertyName)) {
throw new ShouldNotHappenException();
}
// extract to some "inherited parent method" service
/** @var string|null $parentClassName */
$parentClassName = $classNode->getAttribute(AttributeKey::PARENT_CLASS_NAME);
if ($parentClassName !== null) {
$parentClassProperty = $this->findParentProperty($parentClassName, $propertyName);
// validate type is conflicting
// parent class property in local scope → it's ok
if ($parentClassProperty !== null) {
return $parentClassProperty->type !== null;
}
// if not, look for it's parent parent - recursion
if (property_exists($parentClassName, $propertyName)) {
// validate type is conflicting
// parent class property in external scope → it's not ok
return true;
// if not, look for it's parent parent
}
}
return false;
}
private function findParentProperty(string $parentClassName, string $propertyName): ?Property
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode === null) {
return null;
}
return $parentClassNode->getProperty($propertyName);
}
}

View File

@ -10,7 +10,7 @@ use PhpParser\Node\Stmt\Property;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodParamVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodReturnVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\PropertyVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\PropertyTypeVendorLockResolver;
final class VendorLockResolver
{
@ -25,9 +25,9 @@ final class VendorLockResolver
private $classMethodParamVendorLockResolver;
/**
* @var PropertyVendorLockResolver
* @var PropertyTypeVendorLockResolver
*/
private $propertyVendorLockResolver;
private $propertyTypeVendorLockResolver;
/**
* @var ClassMethodVendorLockResolver
@ -37,12 +37,12 @@ final class VendorLockResolver
public function __construct(
ClassMethodReturnVendorLockResolver $classMethodReturnVendorLockResolver,
ClassMethodParamVendorLockResolver $classMethodParamVendorLockResolver,
PropertyVendorLockResolver $propertyVendorLockResolver,
PropertyTypeVendorLockResolver $propertyTypeVendorLockResolver,
ClassMethodVendorLockResolver $classMethodVendorLockResolver
) {
$this->classMethodReturnVendorLockResolver = $classMethodReturnVendorLockResolver;
$this->classMethodParamVendorLockResolver = $classMethodParamVendorLockResolver;
$this->propertyVendorLockResolver = $propertyVendorLockResolver;
$this->propertyTypeVendorLockResolver = $propertyTypeVendorLockResolver;
$this->classMethodVendorLockResolver = $classMethodVendorLockResolver;
}
@ -60,9 +60,9 @@ final class VendorLockResolver
return $this->classMethodReturnVendorLockResolver->isVendorLocked($classMethod);
}
public function isPropertyChangeVendorLockedIn(Property $property): bool
public function isPropertyTypeChangeVendorLockedIn(Property $property): bool
{
return $this->propertyVendorLockResolver->isVendorLocked($property);
return $this->propertyTypeVendorLockResolver->isVendorLocked($property);
}
public function isClassMethodRemovalVendorLocked(ClassMethod $classMethod): bool

View File

@ -124,7 +124,7 @@ PHP
return null;
}
if ($this->vendorLockResolver->isPropertyChangeVendorLockedIn($node)) {
if ($this->vendorLockResolver->isPropertyTypeChangeVendorLockedIn($node)) {
return null;
}

View File

@ -0,0 +1,23 @@
<?php
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Fixture;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\AbstractSomeParent;
use Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source\SomeChildOfSomeParent;
use Rector\Privatization\Tests\Rector\MethodCall\PrivatizeLocalGetterToPropertyRector\Fixture\SkipParentClass;
abstract class SkipParentConflicting
{
/**
* @var AbstractSomeParent
*/
protected $repository;
}
final class SkipParentConflictingChild extends SkipParentConflicting
{
/**
* @var SomeChildOfSomeParent
*/
protected $repository;
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source;
abstract class AbstractSomeParent
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Rector\Php74\Tests\Rector\Property\TypedPropertyRector\Source;
final class SomeChildOfSomeParent extends AbstractSomeParent
{
}