2019-12-26 22:58:40 +01:00
|
|
|
<?php
|
|
|
|
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
namespace Rector\VendorLocker;
|
2019-12-26 22:58:40 +01:00
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
use PhpParser\Node;
|
2019-12-26 22:58:40 +01:00
|
|
|
use PhpParser\Node\Stmt\Class_;
|
2019-12-27 16:46:52 +01:00
|
|
|
use PhpParser\Node\Stmt\ClassLike;
|
2019-12-26 22:58:40 +01:00
|
|
|
use PhpParser\Node\Stmt\ClassMethod;
|
|
|
|
use PhpParser\Node\Stmt\Interface_;
|
2019-12-27 16:46:52 +01:00
|
|
|
use PhpParser\Node\Stmt\Property;
|
2020-02-06 22:48:18 +01:00
|
|
|
use Rector\Core\Exception\ShouldNotHappenException;
|
2020-02-09 11:05:37 +01:00
|
|
|
use Rector\Core\NodeContainer\NodeCollector\ParsedNodeCollector;
|
2020-02-06 22:48:18 +01:00
|
|
|
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
|
2020-02-09 23:47:00 +01:00
|
|
|
use Rector\NodeNameResolver\NodeNameResolver;
|
2019-12-26 22:58:40 +01:00
|
|
|
use Rector\NodeTypeResolver\Node\AttributeKey;
|
2020-02-01 10:31:25 +01:00
|
|
|
use ReflectionClass;
|
2019-12-26 22:58:40 +01:00
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
/**
|
|
|
|
* @todo decouple to standalone package "packages/vendor-locker"
|
|
|
|
*/
|
2019-12-26 22:58:40 +01:00
|
|
|
final class VendorLockResolver
|
|
|
|
{
|
|
|
|
/**
|
2020-02-09 12:31:31 +01:00
|
|
|
* @var NodeNameResolver
|
2019-12-26 22:58:40 +01:00
|
|
|
*/
|
2020-02-09 12:31:31 +01:00
|
|
|
private $nodeNameResolver;
|
2019-12-26 22:58:40 +01:00
|
|
|
|
|
|
|
/**
|
2020-02-09 11:05:37 +01:00
|
|
|
* @var ClassManipulator
|
2019-12-26 22:58:40 +01:00
|
|
|
*/
|
2020-02-09 11:05:37 +01:00
|
|
|
private $classManipulator;
|
2019-12-26 22:58:40 +01:00
|
|
|
|
|
|
|
/**
|
2020-02-09 11:05:37 +01:00
|
|
|
* @var ParsedNodeCollector
|
2019-12-26 22:58:40 +01:00
|
|
|
*/
|
2020-02-09 11:05:37 +01:00
|
|
|
private $parsedNodeCollector;
|
2019-12-26 22:58:40 +01:00
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
/**
|
|
|
|
* @var ReturnNodeVendorLockResolver
|
|
|
|
*/
|
|
|
|
private $returnNodeVendorLockResolver;
|
|
|
|
|
2019-12-26 22:58:40 +01:00
|
|
|
public function __construct(
|
2020-02-09 12:31:31 +01:00
|
|
|
NodeNameResolver $nodeNameResolver,
|
2020-02-09 11:05:37 +01:00
|
|
|
ParsedNodeCollector $parsedNodeCollector,
|
2020-02-09 12:31:31 +01:00
|
|
|
ClassManipulator $classManipulator,
|
|
|
|
ReturnNodeVendorLockResolver $returnNodeVendorLockResolver
|
2019-12-26 22:58:40 +01:00
|
|
|
) {
|
2020-02-09 12:31:31 +01:00
|
|
|
$this->nodeNameResolver = $nodeNameResolver;
|
2020-02-09 11:05:37 +01:00
|
|
|
$this->parsedNodeCollector = $parsedNodeCollector;
|
2019-12-26 22:58:40 +01:00
|
|
|
$this->classManipulator = $classManipulator;
|
2020-02-09 12:31:31 +01:00
|
|
|
$this->returnNodeVendorLockResolver = $returnNodeVendorLockResolver;
|
2019-12-26 22:58:40 +01:00
|
|
|
}
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
public function isParamChangeVendorLockedIn(ClassMethod $classMethod, int $paramPosition): bool
|
2019-12-26 22:58:40 +01:00
|
|
|
{
|
2020-02-09 12:31:31 +01:00
|
|
|
/** @var Class_|null $classNode */
|
2019-12-27 16:46:52 +01:00
|
|
|
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
|
|
|
|
if ($classNode === null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
|
2019-12-26 22:58:40 +01:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
$methodName = $this->nodeNameResolver->getName($classMethod);
|
2020-02-06 22:48:18 +01:00
|
|
|
if (! is_string($methodName)) {
|
|
|
|
throw new ShouldNotHappenException();
|
|
|
|
}
|
2019-12-26 22:58:40 +01:00
|
|
|
|
|
|
|
// @todo extract to some "inherited parent method" service
|
|
|
|
/** @var string|null $parentClassName */
|
|
|
|
$parentClassName = $classMethod->getAttribute(AttributeKey::PARENT_CLASS_NAME);
|
|
|
|
|
|
|
|
if ($parentClassName !== null) {
|
2020-02-09 12:31:31 +01:00
|
|
|
$vendorLock = $this->isParentClassVendorLocking($paramPosition, $parentClassName, $methodName);
|
|
|
|
if ($vendorLock !== null) {
|
|
|
|
return $vendorLock;
|
2019-12-26 22:58:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$classNode = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
|
|
|
|
if (! $classNode instanceof Class_ && ! $classNode instanceof Interface_) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$interfaceNames = $this->classManipulator->getClassLikeNodeParentInterfaceNames($classNode);
|
2020-02-09 12:31:31 +01:00
|
|
|
return $this->isInterfaceParamVendorLockin($interfaceNames, $methodName);
|
2019-12-26 23:07:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
public function isReturnChangeVendorLockedIn(ClassMethod $classMethod): bool
|
|
|
|
{
|
2020-02-09 12:31:31 +01:00
|
|
|
return $this->returnNodeVendorLockResolver->isVendorLocked($classMethod);
|
2019-12-26 22:58:40 +01:00
|
|
|
}
|
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
public function isPropertyChangeVendorLockedIn(Property $property): bool
|
2019-12-26 22:58:40 +01:00
|
|
|
{
|
2020-02-09 12:31:31 +01:00
|
|
|
/** @var Class_|null $classNode */
|
2019-12-27 16:46:52 +01:00
|
|
|
$classNode = $property->getAttribute(AttributeKey::CLASS_NODE);
|
2019-12-26 22:58:40 +01:00
|
|
|
if ($classNode === null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
if (! $this->hasParentClassOrImplementsInterface($classNode)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
/** @var string|null $propertyName */
|
|
|
|
$propertyName = $this->nodeNameResolver->getName($property);
|
2020-02-06 22:48:18 +01:00
|
|
|
if (! is_string($propertyName)) {
|
|
|
|
throw new ShouldNotHappenException();
|
|
|
|
}
|
2019-12-27 16:46:52 +01:00
|
|
|
|
|
|
|
// @todo extract to some "inherited parent method" service
|
|
|
|
/** @var string|null $parentClassName */
|
|
|
|
$parentClassName = $classNode->getAttribute(AttributeKey::PARENT_CLASS_NAME);
|
|
|
|
|
|
|
|
if ($parentClassName !== null) {
|
2020-02-09 12:31:31 +01:00
|
|
|
$parentClassProperty = $this->findParentProperty($parentClassName, $propertyName);
|
2019-12-27 16:46:52 +01:00
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
// @todo validate type is conflicting
|
|
|
|
// parent class property in local scope → it's ok
|
|
|
|
if ($parentClassProperty !== null) {
|
|
|
|
return $parentClassProperty->type !== null;
|
2019-12-27 16:46:52 +01:00
|
|
|
}
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
// if not, look for it's parent parent - @todo recursion
|
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-02-01 10:31:25 +01:00
|
|
|
/**
|
|
|
|
* Checks for:
|
|
|
|
* - interface required methods
|
|
|
|
* - abstract classes reqired method
|
|
|
|
*
|
|
|
|
* Prevent:
|
|
|
|
* - removing class methods, that breaks the code
|
|
|
|
*/
|
|
|
|
public function isClassMethodRemovalVendorLocked(ClassMethod $classMethod): bool
|
|
|
|
{
|
2020-02-09 12:31:31 +01:00
|
|
|
$classMethodName = $this->nodeNameResolver->getName($classMethod);
|
2020-02-06 22:48:18 +01:00
|
|
|
if (! is_string($classMethodName)) {
|
|
|
|
throw new ShouldNotHappenException();
|
|
|
|
}
|
2020-02-01 10:31:25 +01:00
|
|
|
|
|
|
|
/** @var Class_|null $class */
|
|
|
|
$class = $classMethod->getAttribute(AttributeKey::CLASS_NODE);
|
|
|
|
if ($class === null) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-02-09 12:31:31 +01:00
|
|
|
if ($this->isVendorLockedByInterface($class, $classMethodName)) {
|
2020-02-06 22:48:18 +01:00
|
|
|
return true;
|
2020-02-01 10:31:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2020-02-01 17:04:38 +01:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
// Until we have getProperty (https://github.com/nikic/PHP-Parser/pull/646)
|
|
|
|
private function getProperty(ClassLike $classLike, string $name)
|
|
|
|
{
|
|
|
|
$lowerName = strtolower($name);
|
2020-02-09 12:31:31 +01:00
|
|
|
|
|
|
|
foreach ($classLike->getProperties() as $property) {
|
|
|
|
foreach ($property->props as $propertyProperty) {
|
|
|
|
if ($lowerName !== $propertyProperty->name->toLowerString()) {
|
|
|
|
continue;
|
2019-12-27 16:46:52 +01:00
|
|
|
}
|
2020-02-09 12:31:31 +01:00
|
|
|
|
|
|
|
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;
|
2019-12-27 16:46:52 +01:00
|
|
|
}
|
2020-02-09 12:31:31 +01:00
|
|
|
|
|
|
|
$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
|
2019-12-27 16:46:52 +01:00
|
|
|
}
|
2020-02-09 12:31:31 +01:00
|
|
|
|
2019-12-27 16:46:52 +01:00
|
|
|
return null;
|
|
|
|
}
|
2020-02-09 12:31:31 +01:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
2019-12-26 22:58:40 +01:00
|
|
|
}
|