Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -54,15 +24,20 @@ public function isVendorLocked(ClassMethod $classMethod): bool
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);
Expand All @@ -71,37 +46,24 @@ public function isVendorLocked(ClassMethod $classMethod): bool
}

$interfaceNames = $this->classManipulator->getClassLikeNodeParentInterfaceNames($classNode);

return $this->isVendorLockedByInterface($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;
return $this->isInterfaceParamVendorLockin($interfaceNames, $methodName);
}

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) {
$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;
// 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
Expand All @@ -110,13 +72,10 @@ private function isVendorLockedByParentClass(string $parentClassName, string $me
// 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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Loading