decopule new VendorLock package

This commit is contained in:
TomasVotruba 2020-02-10 00:06:17 +01:00
parent 59f82bf804
commit aaa5ffa8f4
7 changed files with 331 additions and 318 deletions

View File

@ -0,0 +1,79 @@
<?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 Rector\Core\NodeContainer\NodeCollector\ParsedNodeCollector;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\NodeNameResolver\NodeNameResolver;
abstract class AbstractNodeVendorLockResolver
{
/**
* @var ParsedNodeCollector
*/
protected $parsedNodeCollector;
/**
* @var ClassManipulator
*/
protected $classManipulator;
/**
* @var NodeNameResolver
*/
protected $nodeNameResolver;
/**
* @required
*/
public function autowireAbstractNodeVendorLockResolver(
ParsedNodeCollector $parsedNodeCollector,
ClassManipulator $classManipulator,
NodeNameResolver $nodeNameResolver
): void {
$this->parsedNodeCollector = $parsedNodeCollector;
$this->classManipulator = $classManipulator;
$this->nodeNameResolver = $nodeNameResolver;
}
protected function hasParentClassOrImplementsInterface(ClassLike $classLike): bool
{
if (($classLike instanceof Class_ || $classLike instanceof Interface_) && $classLike->extends) {
return true;
}
if ($classLike instanceof Class_) {
return (bool) $classLike->implements;
}
return false;
}
/**
* @param Class_|Interface_ $classLike
*/
protected function isMethodVendorLockedByInterface(ClassLike $classLike, string $methodName): bool
{
$interfaceNames = $this->classManipulator->getClassLikeNodeParentInterfaceNames($classLike);
foreach ($interfaceNames as $interfaceName) {
if (! interface_exists($interfaceName)) {
continue;
}
$interfaceMethods = get_class_methods($interfaceName);
if (! in_array($methodName, $interfaceMethods, true)) {
continue;
}
return true;
}
return false;
}
}

View File

@ -2,49 +2,19 @@
declare(strict_types=1);
namespace Rector\VendorLocker;
namespace Rector\VendorLocker\NodeVendorLocker;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Interface_;
use Rector\Core\NodeContainer\NodeCollector\ParsedNodeCollector;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* @todo decouple to standalone package "packages/vendor-locker"
*/
final class ReturnNodeVendorLockResolver
final class ClassMethodParamVendorLockResolver extends AbstractNodeVendorLockResolver
{
/**
* @var NodeNameResolver
*/
private $nodeNameResolver;
/**
* @var ClassManipulator
*/
private $classManipulator;
/**
* @var ParsedNodeCollector
*/
private $parsedNodeCollector;
public function __construct(
NodeNameResolver $nodeNameResolver,
ParsedNodeCollector $parsedNodeCollector,
ClassManipulator $classManipulator
) {
$this->nodeNameResolver = $nodeNameResolver;
$this->parsedNodeCollector = $parsedNodeCollector;
$this->classManipulator = $classManipulator;
}
public function isVendorLocked(ClassMethod $classMethod): bool
public function isVendorLocked(ClassMethod $classMethod, int $paramPosition): bool
{
/** @var Class_|null $classNode */
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return false;
@ -54,15 +24,20 @@ final class ReturnNodeVendorLockResolver
return false;
}
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($classMethod);
if (! is_string($methodName)) {
throw new ShouldNotHappenException();
}
// @todo extract to some "inherited parent method" service
/** @var string|null $parentClassName */
$parentClassName = $classMethod->getAttribute(AttributeKey::PARENT_CLASS_NAME);
if ($parentClassName !== null) {
return $this->isVendorLockedByParentClass($parentClassName, $methodName);
$vendorLock = $this->isParentClassVendorLocking($paramPosition, $parentClassName, $methodName);
if ($vendorLock !== null) {
return $vendorLock;
}
}
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
@ -71,24 +46,10 @@ final class ReturnNodeVendorLockResolver
}
$interfaceNames = $this->classManipulator->getClassLikeNodeParentInterfaceNames($classNode);
return $this->isVendorLockedByInterface($interfaceNames, $methodName);
return $this->isInterfaceParamVendorLockin($interfaceNames, $methodName);
}
private function hasParentClassOrImplementsInterface(ClassLike $classLike): bool
{
if (($classLike instanceof Class_ || $classLike instanceof Interface_) && $classLike->extends) {
return true;
}
if ($classLike instanceof Class_) {
return (bool) $classLike->implements;
}
return false;
}
private function isVendorLockedByParentClass(string $parentClassName, string $methodName): bool
private function isParentClassVendorLocking(int $paramPosition, string $parentClassName, string $methodName): ?bool
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode !== null) {
@ -96,12 +57,13 @@ final class ReturnNodeVendorLockResolver
// @todo validate type is conflicting
// parent class method in local scope → it's ok
if ($parentMethodNode !== null) {
return $parentMethodNode->returnType !== null;
// parent method has no type → we cannot change it here
return isset($parentMethodNode->params[$paramPosition]) && $parentMethodNode->params[$paramPosition]->type === null;
}
// if not, look for it's parent parent - @todo recursion
}
// if not, look for it's parent parent - @todo recursion
if (method_exists($parentClassName, $methodName)) {
// @todo validate type is conflicting
// parent class method in external scope → it's not ok
@ -110,13 +72,10 @@ final class ReturnNodeVendorLockResolver
// if not, look for it's parent parent - @todo recursion
}
return false;
return null;
}
/**
* @param string[] $interfaceNames
*/
private function isVendorLockedByInterface(array $interfaceNames, string $methodName): bool
private function isInterfaceParamVendorLockin(array $interfaceNames, string $methodName): bool
{
foreach ($interfaceNames as $interfaceName) {
$interface = $this->parsedNodeCollector->findInterface($interfaceName);

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Rector\VendorLocker\NodeVendorLocker;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Interface_;
use Rector\NodeTypeResolver\Node\AttributeKey;
final class ClassMethodReturnVendorLockResolver extends AbstractNodeVendorLockResolver
{
public function isVendorLocked(ClassMethod $classMethod): bool
{
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return false;
}
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
return false;
}
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($classMethod);
// @todo extract to some "inherited parent method" service
/** @var string|null $parentClassName */
$parentClassName = $classMethod->getAttribute(AttributeKey::PARENT_CLASS_NAME);
if ($parentClassName !== null) {
return $this->isVendorLockedByParentClass($parentClassName, $methodName);
}
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if (! $classNode instanceof Class_ && ! $classNode instanceof Interface_) {
return false;
}
return $this->isMethodVendorLockedByInterface($classNode, $methodName);
}
private function isVendorLockedByParentClass(string $parentClassName, string $methodName): bool
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode !== null) {
$parentMethodNode = $parentClassNode->getMethod($methodName);
// @todo validate type is conflicting
// parent class method in local scope → it's ok
if ($parentMethodNode !== null) {
return $parentMethodNode->returnType !== null;
}
// if not, look for it's parent parent - @todo recursion
}
if (method_exists($parentClassName, $methodName)) {
// @todo validate type is conflicting
// parent class method in external scope → it's not ok
return true;
// if not, look for it's parent parent - @todo recursion
}
return false;
}
}

View File

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Rector\VendorLocker\NodeVendorLocker;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use ReflectionClass;
final class ClassMethodVendorLockResolver extends AbstractNodeVendorLockResolver
{
/**
* Checks for:
* - interface required methods
* - abstract classes reqired method
*
* Prevent:
* - removing class methods, that breaks the code
*/
public function isRemovalVendorLocked(ClassMethod $classMethod): bool
{
$classMethodName = $this->nodeNameResolver->getName($classMethod);
if (! is_string($classMethodName)) {
throw new ShouldNotHappenException();
}
/** @var Class_|null $class */
$class = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($class === null) {
return false;
}
if ($this->isMethodVendorLockedByInterface($class, $classMethodName)) {
return true;
}
if ($class->extends === null) {
return false;
}
/** @var string $className */
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
$classParents = class_parents($className);
foreach ($classParents as $classParent) {
if (! class_exists($classParent)) {
continue;
}
$parentClassReflection = new ReflectionClass($classParent);
if (! $parentClassReflection->hasMethod($classMethodName)) {
continue;
}
$methodReflection = $parentClassReflection->getMethod($classMethodName);
if (! $methodReflection->isAbstract()) {
continue;
}
return true;
}
return false;
}
}

View File

@ -0,0 +1,68 @@
<?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();
}
// @todo 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);
// @todo 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 - @todo recursion
if (property_exists($parentClassName, $propertyName)) {
// @todo validate type is conflicting
// parent class property in external scope → it's not ok
return true;
// if not, look for it's parent parent - @todo recursion
}
}
return false;
}
private function findParentProperty(string $parentClassName, string $propertyName): ?Property
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode === null) {
return null;
}
return $this->classManipulator->getProperty($parentClassNode, $propertyName);
}
}

View File

@ -4,141 +4,60 @@ declare(strict_types=1);
namespace Rector\VendorLocker;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Property;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\NodeContainer\NodeCollector\ParsedNodeCollector;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use ReflectionClass;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodParamVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodReturnVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\ClassMethodVendorLockResolver;
use Rector\VendorLocker\NodeVendorLocker\PropertyVendorLockResolver;
/**
* @todo decouple to standalone package "packages/vendor-locker"
*/
final class VendorLockResolver
{
/**
* @var NodeNameResolver
* @var ClassMethodReturnVendorLockResolver
*/
private $nodeNameResolver;
private $classMethodReturnVendorLockResolver;
/**
* @var ClassManipulator
* @var ClassMethodParamVendorLockResolver
*/
private $classManipulator;
private $classMethodParamVendorLockResolver;
/**
* @var ParsedNodeCollector
* @var PropertyVendorLockResolver
*/
private $parsedNodeCollector;
private $propertyVendorLockResolver;
/**
* @var ReturnNodeVendorLockResolver
* @var ClassMethodVendorLockResolver
*/
private $returnNodeVendorLockResolver;
private $classMethodVendorLockResolver;
public function __construct(
NodeNameResolver $nodeNameResolver,
ParsedNodeCollector $parsedNodeCollector,
ClassManipulator $classManipulator,
ReturnNodeVendorLockResolver $returnNodeVendorLockResolver
ClassMethodReturnVendorLockResolver $classMethodReturnVendorLockResolver,
ClassMethodParamVendorLockResolver $classMethodParamVendorLockResolver,
PropertyVendorLockResolver $propertyVendorLockResolver,
ClassMethodVendorLockResolver $classMethodVendorLockResolver
) {
$this->nodeNameResolver = $nodeNameResolver;
$this->parsedNodeCollector = $parsedNodeCollector;
$this->classManipulator = $classManipulator;
$this->returnNodeVendorLockResolver = $returnNodeVendorLockResolver;
$this->classMethodReturnVendorLockResolver = $classMethodReturnVendorLockResolver;
$this->classMethodParamVendorLockResolver = $classMethodParamVendorLockResolver;
$this->propertyVendorLockResolver = $propertyVendorLockResolver;
$this->classMethodVendorLockResolver = $classMethodVendorLockResolver;
}
public function isParamChangeVendorLockedIn(ClassMethod $classMethod, int $paramPosition): bool
{
/** @var Class_|null $classNode */
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode === null) {
return false;
}
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
return false;
}
$methodName = $this->nodeNameResolver->getName($classMethod);
if (! is_string($methodName)) {
throw new ShouldNotHappenException();
}
// @todo extract to some "inherited parent method" service
/** @var string|null $parentClassName */
$parentClassName = $classMethod->getAttribute(AttributeKey::PARENT_CLASS_NAME);
if ($parentClassName !== null) {
$vendorLock = $this->isParentClassVendorLocking($paramPosition, $parentClassName, $methodName);
if ($vendorLock !== null) {
return $vendorLock;
}
}
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if (! $classNode instanceof Class_ && ! $classNode instanceof Interface_) {
return false;
}
$interfaceNames = $this->classManipulator->getClassLikeNodeParentInterfaceNames($classNode);
return $this->isInterfaceParamVendorLockin($interfaceNames, $methodName);
return $this->classMethodParamVendorLockResolver->isVendorLocked($classMethod, $paramPosition);
}
public function isReturnChangeVendorLockedIn(ClassMethod $classMethod): bool
{
return $this->returnNodeVendorLockResolver->isVendorLocked($classMethod);
return $this->classMethodReturnVendorLockResolver->isVendorLocked($classMethod);
}
public function isPropertyChangeVendorLockedIn(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();
}
// @todo 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);
// @todo 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 - @todo recursion
if (property_exists($parentClassName, $propertyName)) {
// @todo validate type is conflicting
// parent class property in external scope → it's not ok
return true;
// if not, look for it's parent parent - @todo recursion
}
}
return false;
return $this->propertyVendorLockResolver->isVendorLocked($property);
}
/**
@ -151,158 +70,6 @@ final class VendorLockResolver
*/
public function isClassMethodRemovalVendorLocked(ClassMethod $classMethod): bool
{
$classMethodName = $this->nodeNameResolver->getName($classMethod);
if (! is_string($classMethodName)) {
throw new ShouldNotHappenException();
}
/** @var Class_|null $class */
$class = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
if ($class === null) {
return false;
}
if ($this->isVendorLockedByInterface($class, $classMethodName)) {
return true;
}
if ($class->extends === null) {
return false;
}
/** @var string $className */
$className = $classMethod->getAttribute(AttributeKey::CLASS_NAME);
$classParents = class_parents($className);
foreach ($classParents as $classParent) {
if (! class_exists($classParent)) {
continue;
}
$parentClassReflection = new ReflectionClass($classParent);
if (! $parentClassReflection->hasMethod($classMethodName)) {
continue;
}
$methodReflection = $parentClassReflection->getMethod($classMethodName);
if (! $methodReflection->isAbstract()) {
continue;
}
return true;
}
return false;
}
private function hasParentClassOrImplementsInterface(Node $classNode): bool
{
if (($classNode instanceof Class_ || $classNode instanceof Interface_) && $classNode->extends) {
return true;
}
if ($classNode instanceof Class_) {
return (bool) $classNode->implements;
}
return false;
}
// Until we have getProperty (https://github.com/nikic/PHP-Parser/pull/646)
private function getProperty(ClassLike $classLike, string $name)
{
$lowerName = strtolower($name);
foreach ($classLike->getProperties() as $property) {
foreach ($property->props as $propertyProperty) {
if ($lowerName !== $propertyProperty->name->toLowerString()) {
continue;
}
return $property;
}
}
return null;
}
private function findParentProperty(string $parentClassName, string $propertyName): ?Property
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode === null) {
return null;
}
return $this->getProperty($parentClassNode, $propertyName);
}
private function isVendorLockedByInterface(Class_ $class, string $classMethodName): bool
{
// required by interface?
foreach ($class->implements as $implement) {
$implementedInterfaceName = $this->nodeNameResolver->getName($implement);
if (! is_string($implementedInterfaceName)) {
throw new ShouldNotHappenException();
}
if (! interface_exists($implementedInterfaceName)) {
continue;
}
$interfaceMethods = get_class_methods($implementedInterfaceName);
if (! in_array($classMethodName, $interfaceMethods, true)) {
continue;
}
return true;
}
return false;
}
private function isParentClassVendorLocking(int $paramPosition, string $parentClassName, string $methodName): ?bool
{
$parentClassNode = $this->parsedNodeCollector->findClass($parentClassName);
if ($parentClassNode !== null) {
$parentMethodNode = $parentClassNode->getMethod($methodName);
// @todo validate type is conflicting
// parent class method in local scope → it's ok
if ($parentMethodNode !== null) {
// parent method has no type → we cannot change it here
return isset($parentMethodNode->params[$paramPosition]) && $parentMethodNode->params[$paramPosition]->type === null;
}
}
// if not, look for it's parent parent - @todo recursion
if (method_exists($parentClassName, $methodName)) {
// @todo validate type is conflicting
// parent class method in external scope → it's not ok
return true;
// if not, look for it's parent parent - @todo recursion
}
return null;
}
private function isInterfaceParamVendorLockin(array $interfaceNames, string $methodName): bool
{
foreach ($interfaceNames as $interfaceName) {
$interface = $this->parsedNodeCollector->findInterface($interfaceName);
// parent class method in local scope → it's ok
// @todo validate type is conflicting
if ($interface !== null && $interface->getMethod($methodName) !== null) {
return false;
}
if (method_exists($interfaceName, $methodName)) {
// parent class method in external scope → it's not ok
// @todo validate type is conflicting
return true;
}
}
return false;
return $this->classMethodVendorLockResolver->isRemovalVendorLocked($classMethod);
}
}

View File

@ -200,6 +200,10 @@ final class ClassManipulator
return $usedTraits;
}
/**
* @todo
* Waits on https://github.com/nikic/PHP-Parser/pull/646 to be relased
*/
public function getProperty(ClassLike $classLike, string $name): ?Property
{
foreach ($classLike->getProperties() as $property) {