[DeadCode] Add RemoveUnusedDoctrineEntityMethodAndPropertyRector (#1800)

[DeadCode] Add RemoveUnusedDoctrineEntityMethodAndPropertyRector
This commit is contained in:
Tomáš Votruba 2019-08-05 16:55:42 +02:00 committed by GitHub
commit 59222aa086
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1415 additions and 127 deletions

View File

@ -15,11 +15,12 @@
"doctrine/inflector": "^1.3",
"jean85/pretty-package-versions": "^1.2",
"jetbrains/phpstorm-stubs": "^2019.1",
"nette/di": "^3.0",
"nette/robot-loader": "^3.1",
"nette/utils": "^2.5|^3.0",
"nikic/php-parser": "^4.2.2",
"phpstan/phpdoc-parser": "^0.3.4",
"phpstan/phpstan": "^0.11.6",
"phpstan/phpdoc-parser": "^0.3.5",
"phpstan/phpstan": "^0.11.10",
"phpstan/phpstan-phpunit": "^0.11.2",
"sebastian/diff": "^3.0",
"symfony/console": "^3.4|^4.2",

View File

@ -24,3 +24,4 @@ services:
Rector\DeadCode\Rector\ClassMethod\RemoveDelegatingParentCallRector: ~
Rector\DeadCode\Rector\Instanceof_\RemoveDuplicatedInstanceOfRector: ~
Rector\DeadCode\Rector\Switch_\RemoveDuplicatedCaseInSwitchRector: ~
Rector\DeadCode\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector: ~

View File

@ -39,6 +39,7 @@ services:
- 'Symplify\PackageBuilder\*'
- 'Symfony\Component\Console\Input\*Input'
- '*ValueObject'
- 'PHPStan\Analyser\NameScope'
Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer:
extra_skipped_classes:

View File

@ -301,6 +301,10 @@ CODE_SAMPLE
}
}
/**
* @param Node $node
* @return string[]|null
*/
private function resolveAssignPropertyToVariableOrNull(Node $node): ?array
{
if ($node instanceof Expression) {

View File

@ -36,20 +36,20 @@ final class RemoveUnusedAliasRector extends AbstractRector
*/
private $resolvedDocPossibleAliases = [];
/**
* @var ShortNameResolver
*/
private $shortNameResolver;
/**
* @var ClassNaming
*/
private $classNaming;
public function __construct(ShortNameResolver $shortNameResolver, ClassNaming $classNaming)
/**
* @var ShortNameResolver
*/
private $shortNameResolver;
public function __construct(ClassNaming $classNaming, ShortNameResolver $shortNameResolver)
{
$this->shortNameResolver = $shortNameResolver;
$this->classNaming = $classNaming;
$this->shortNameResolver = $shortNameResolver;
}
public function getDefinition(): RectorDefinition

View File

@ -0,0 +1,167 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\Doctrine;
use Nette\Utils\Strings;
use PhpParser\Comment\Doc;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\NamespaceAnalyzer;
final class DoctrineEntityManipulator
{
/**
* @var string
*/
private const TARGET_ENTITY_PATTERN = '#targetEntity="(?<class>.*?)"#';
/**
* @var string
*/
private const TARGET_PROPERTY_PATTERN = '#(inversedBy|mappedBy)="(?<property>.*?)"#';
/**
* @var string[]
*/
private const RELATION_ANNOTATIONS = [
'Doctrine\ORM\Mapping\OneToMany',
'Doctrine\ORM\Mapping\ManyToOne',
'Doctrine\ORM\Mapping\OneToOne',
'Doctrine\ORM\Mapping\ManyToMany',
];
/**
* @var string
*/
private const MAPPED_OR_INVERSED_BY_PATTERN = '#(,\s+)?(inversedBy|mappedBy)="(?<property>.*?)"#';
/**
* @var string
*/
private const JOIN_COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\JoinColumn';
/**
* @var DocBlockManipulator
*/
private $docBlockManipulator;
/**
* @var NamespaceAnalyzer
*/
private $namespaceAnalyzer;
public function __construct(DocBlockManipulator $docBlockManipulator, NamespaceAnalyzer $namespaceAnalyzer)
{
$this->docBlockManipulator = $docBlockManipulator;
$this->namespaceAnalyzer = $namespaceAnalyzer;
}
public function resolveTargetClass(Property $property): ?string
{
foreach (self::RELATION_ANNOTATIONS as $relationAnnotation) {
if (! $this->docBlockManipulator->hasTag($property, $relationAnnotation)) {
continue;
}
$relationTag = $this->docBlockManipulator->getTagByName($property, $relationAnnotation);
if (! $relationTag->value instanceof GenericTagValueNode) {
throw new ShouldNotHappenException();
}
$match = Strings::match($relationTag->value->value, self::TARGET_ENTITY_PATTERN);
if (! isset($match['class'])) {
return null;
}
$class = $match['class'];
// fqnize possibly shorten class
if (Strings::contains($class, '\\')) {
return $class;
}
if (! class_exists($class)) {
return $this->namespaceAnalyzer->resolveTypeToFullyQualified($class, $property);
}
return $class;
}
return null;
}
public function resolveOtherProperty(Property $property): ?string
{
foreach (self::RELATION_ANNOTATIONS as $relationAnnotation) {
if (! $this->docBlockManipulator->hasTag($property, $relationAnnotation)) {
continue;
}
$relationTag = $this->docBlockManipulator->getTagByName($property, $relationAnnotation);
if (! $relationTag->value instanceof GenericTagValueNode) {
throw new ShouldNotHappenException();
}
$match = Strings::match($relationTag->value->value, self::TARGET_PROPERTY_PATTERN);
return $match['property'] ?? null;
}
return null;
}
public function isStandaloneDoctrineEntityClass(Class_ $class): bool
{
if ($class->isAnonymous()) {
return false;
}
if ($class->isAbstract()) {
return false;
}
// is parent entity
if ($this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\InheritanceType')) {
return false;
}
return $this->docBlockManipulator->hasTag($class, 'Doctrine\ORM\Mapping\Entity');
}
public function removeMappedByOrInversedByFromProperty(Property $property): void
{
$doc = $property->getDocComment();
if ($doc === null) {
return;
}
$originalDocText = $doc->getText();
$clearedDocText = Strings::replace($originalDocText, self::MAPPED_OR_INVERSED_BY_PATTERN);
// no change
if ($originalDocText === $clearedDocText) {
return;
}
$property->setDocComment(new Doc($clearedDocText));
}
public function isNullableRelation(Property $property): bool
{
if (! $this->docBlockManipulator->hasTag($property, self::JOIN_COLUMN_ANNOTATION)) {
// @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#joincolumn
return true;
}
$joinColumnTag = $this->docBlockManipulator->getTagByName($property, self::JOIN_COLUMN_ANNOTATION);
if ($joinColumnTag->value instanceof GenericTagValueNode) {
return (bool) Strings::match($joinColumnTag->value->value, '#nullable=true#');
}
return false;
}
}

View File

@ -113,7 +113,7 @@ CODE_SAMPLE
public function refactor(Node $node): ?Node
{
$classNode = $node->getAttribute(AttributeKey::CLASS_NODE);
if (! $classNode instanceof Class_) {
if (! $classNode instanceof Class_ || $classNode->isAnonymous()) {
return null;
}

View File

@ -0,0 +1,288 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\New_;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use Rector\DeadCode\Doctrine\DoctrineEntityManipulator;
use Rector\DeadCode\UnusedNodeResolver\ClassUnusedPrivateClassMethodResolver;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Rector\AbstractRector;
use Rector\RectorDefinition\CodeSample;
use Rector\RectorDefinition\RectorDefinition;
/**
* @see \Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest
*/
final class RemoveUnusedDoctrineEntityMethodAndPropertyRector extends AbstractRector
{
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;
/**
* @var Assign[]
*/
private $collectionByPropertyName = [];
/**
* @var ClassUnusedPrivateClassMethodResolver
*/
private $classUnusedPrivateClassMethodResolver;
/**
* @var ClassManipulator
*/
private $classManipulator;
/**
* @var DoctrineEntityManipulator
*/
private $doctrineEntityManipulator;
public function __construct(
ParsedNodesByType $parsedNodesByType,
ClassUnusedPrivateClassMethodResolver $classUnusedPrivateClassMethodResolver,
ClassManipulator $classManipulator,
DoctrineEntityManipulator $doctrineEntityManipulator
) {
$this->parsedNodesByType = $parsedNodesByType;
$this->classUnusedPrivateClassMethodResolver = $classUnusedPrivateClassMethodResolver;
$this->classManipulator = $classManipulator;
$this->doctrineEntityManipulator = $doctrineEntityManipulator;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Removes unused methods and properties from Doctrine entity classes', [
new CodeSample(
<<<'CODE_SAMPLE'
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UserEntity
{
/**
* @ORM\Column
*/
private $name;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UserEntity
{
}
CODE_SAMPLE
),
]);
}
/**
* @return string[]
*/
public function getNodeTypes(): array
{
return [Class_::class];
}
/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->doctrineEntityManipulator->isStandaloneDoctrineEntityClass($node)) {
return null;
}
$unusedMethodNames = $this->classUnusedPrivateClassMethodResolver->getClassUnusedMethodNames($node);
if ($unusedMethodNames !== []) {
$node = $this->removeClassMethodsByNames($node, $unusedMethodNames);
}
$unusedPropertyNames = $this->resolveUnusedPrivatePropertyNames($node);
if ($unusedPropertyNames !== []) {
$node = $this->removeClassPrivatePropertiesByNames($node, $unusedPropertyNames);
}
return $node;
}
/**
* Remove unused methods immediately, so we can then remove unused properties.
* @param string[] $unusedMethodNames
*/
private function removeClassMethodsByNames(Class_ $class, array $unusedMethodNames): Class_
{
foreach ($class->stmts as $key => $classStmt) {
if (! $classStmt instanceof ClassMethod) {
continue;
}
if ($this->isNames($classStmt, $unusedMethodNames)) {
// remove immediately
unset($class->stmts[$key]);
}
}
return $class;
}
/**
* @return string[]
*/
private function resolveUnusedPrivatePropertyNames(Class_ $class): array
{
$privatePropertyNames = $this->classManipulator->getPrivatePropertyNames($class);
// get list of fetched properties
$usedPropertyNames = $this->resolveClassUsedPropertyFetchNames($class);
return array_diff($privatePropertyNames, $usedPropertyNames);
}
/**
* @param string[] $unusedPropertyNames
*/
private function removeClassPrivatePropertiesByNames(Class_ $node, array $unusedPropertyNames): Class_
{
foreach ($node->stmts as $key => $stmt) {
if (! $stmt instanceof Property) {
continue;
}
if (! $this->isNames($stmt, $unusedPropertyNames)) {
continue;
}
unset($node->stmts[$key]);
// remove "$this->someProperty = new ArrayCollection()"
$propertyName = $this->getName($stmt);
if (isset($this->collectionByPropertyName[$propertyName])) {
$this->removeNode($this->collectionByPropertyName[$propertyName]);
}
$this->removeInversedByOrMappedByOnRelatedProperty($stmt);
}
return $node;
}
private function getOtherRelationProperty(Property $property): ?Property
{
$targetClass = $this->doctrineEntityManipulator->resolveTargetClass($property);
$otherProperty = $this->doctrineEntityManipulator->resolveOtherProperty($property);
if ($targetClass === null || $otherProperty === null) {
return null;
}
// get the class property and remove "mappedBy/inversedBy" from annotation
$relatedEntityClass = $this->parsedNodesByType->findClass($targetClass);
if (! $relatedEntityClass instanceof Class_) {
return null;
}
foreach ($relatedEntityClass->stmts as $relatedEntityClassStmt) {
if (! $relatedEntityClassStmt instanceof Property) {
continue;
}
if (! $this->isName($relatedEntityClassStmt, $otherProperty)) {
continue;
}
return $relatedEntityClassStmt;
}
return null;
}
private function removeInversedByOrMappedByOnRelatedProperty(Property $property): void
{
$otherRelationProperty = $this->getOtherRelationProperty($property);
if ($otherRelationProperty === null) {
return;
}
$this->doctrineEntityManipulator->removeMappedByOrInversedByFromProperty($otherRelationProperty);
}
private function isPropertyFetchAssignOfArrayCollection(PropertyFetch $propertyFetch): bool
{
$parentNode = $propertyFetch->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentNode instanceof Assign) {
return false;
}
if (! $parentNode->expr instanceof New_) {
return false;
}
/** @var New_ $new */
$new = $parentNode->expr;
return $this->isName($new->class, 'Doctrine\Common\Collections\ArrayCollection');
}
/**
* @return string[]
*/
private function resolveClassUsedPropertyFetchNames(Class_ $class): array
{
$usedPropertyNames = [];
$this->traverseNodesWithCallable($class->stmts, function (Node $node) use (&$usedPropertyNames) {
if (! $node instanceof PropertyFetch) {
return null;
}
if (! $this->isName($node->var, 'this')) {
return null;
}
/** @var string $propertyName */
$propertyName = $this->getName($node->name);
// skip collection initialization, e.g. "$this->someProperty = new ArrayCollection();"
if ($this->isPropertyFetchAssignOfArrayCollection($node)) {
/** @var Assign $parentNode */
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
$this->collectionByPropertyName[$propertyName] = $parentNode;
return null;
}
$usedPropertyNames[] = $propertyName;
return null;
});
return $usedPropertyNames;
}
}

View File

@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\UnusedNodeResolver;
use Nette\Utils\Strings;
use PhpParser\Node\Stmt\Class_;
use Rector\NodeContainer\ParsedNodesByType;
use Rector\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\PhpParser\Node\Resolver\NameResolver;
final class ClassUnusedPrivateClassMethodResolver
{
/**
* @var NameResolver
*/
private $nameResolver;
/**
* @var ParsedNodesByType
*/
private $parsedNodesByType;
/**
* @var ClassManipulator
*/
private $classManipulator;
public function __construct(
NameResolver $nameResolver,
ParsedNodesByType $parsedNodesByType,
ClassManipulator $classManipulator
) {
$this->nameResolver = $nameResolver;
$this->parsedNodesByType = $parsedNodesByType;
$this->classManipulator = $classManipulator;
}
/**
* @return string[]
*/
public function getClassUnusedMethodNames(Class_ $class): array
{
/** @var string $className */
$className = $this->nameResolver->getName($class);
$classMethodCalls = $this->parsedNodesByType->findMethodCallsOnClass($className);
$usedMethodNames = array_keys($classMethodCalls);
$classPublicMethodNames = $this->classManipulator->getPublicMethodNames($class);
return $this->getUnusedMethodNames($class, $classPublicMethodNames, $usedMethodNames);
}
/**
* @param string[] $classPublicMethodNames
* @param string[] $usedMethodNames
* @return string[]
*/
private function getUnusedMethodNames(Class_ $class, array $classPublicMethodNames, array $usedMethodNames): array
{
$unusedMethods = array_diff($classPublicMethodNames, $usedMethodNames);
$unusedMethods = $this->filterOutSystemMethods($unusedMethods);
return $this->filterOutInterfaceRequiredMethods($class, $unusedMethods);
}
/**
* @param string[] $unusedMethods
* @return string[]
*/
private function filterOutSystemMethods(array $unusedMethods): array
{
foreach ($unusedMethods as $key => $unusedMethod) {
// skip Doctrine-needed methods
if (in_array($unusedMethod, ['getId', 'setId'], true)) {
unset($unusedMethods[$key]);
}
// skip magic methods
if (Strings::startsWith($unusedMethod, '__')) {
unset($unusedMethods[$key]);
}
}
return $unusedMethods;
}
/**
* @param string[] $unusedMethods
* @return string[]
*/
private function filterOutInterfaceRequiredMethods(Class_ $class, array $unusedMethods): array
{
/** @var string $className */
$className = $this->nameResolver->getName($class);
$interfaces = class_implements($className);
$interfaceMethods = [];
foreach ($interfaces as $interface) {
$interfaceMethods = array_merge($interfaceMethods, get_class_methods($interface));
}
return array_diff($unusedMethods, $interfaceMethods);
}
}

View File

@ -1,16 +1,15 @@
<?php
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\FixtureInterface {
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\FixtureInterface
{
interface Foo
{
public function bar($value1);
}
}
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\Fixture {
namespace Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\Fixture
{
use Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\FixtureInterface\Foo;
class Baz

View File

@ -18,7 +18,7 @@ final class RemoveUnusedParameterRectorTest extends AbstractRectorTestCase
__DIR__ . '/Fixture/in_between_parameter.php.inc',
__DIR__ . '/Fixture/compact.php.inc',
__DIR__ . '/Fixture/keep_magic_methods_param.php.inc',
__DIR__ . '/Fixture/anonymous_classes.php.inc',
__DIR__ . '/Fixture/keep_anonymous_classes.php.inc',
]);
}

View File

@ -0,0 +1,43 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UserEntity
{
/**
* @ORM\Column
*/
private $name;
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class UserEntity
{
}
?>

View File

@ -0,0 +1,81 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class Answer
{
/**
* @ORM\ManyToMany(targetEntity="RemoveInversedBy", inversedBy="answers")
*/
private $voters;
public function getVoters()
{
return $this->voters;
}
}
$answer = new Answer();
$answer->getVoters();
/**
* @ORM\Entity
*/
class RemoveInversedBy
{
/**
* @ORM\ManyToMany(targetEntity="Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture\Answer", mappedBy="voters")
*/
private $answers;
public function __construct()
{
$this->answers = new ArrayCollection;
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class Answer
{
/**
* @ORM\ManyToMany(targetEntity="RemoveInversedBy")
*/
private $voters;
public function getVoters()
{
return $this->voters;
}
}
$answer = new Answer();
$answer->getVoters();
/**
* @ORM\Entity
*/
class RemoveInversedBy
{
public function __construct()
{
}
}
?>

View File

@ -0,0 +1,81 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class Question
{
/**
* @ORM\ManyToMany(targetEntity="RemoveInversedByNonFqn", inversedBy="answers")
*/
private $voters;
public function getVoters()
{
return $this->voters;
}
}
$answer = new Question();
$answer->getVoters();
/**
* @ORM\Entity
*/
class RemoveInversedByNonFqn
{
/**
* @ORM\ManyToMany(targetEntity="Question", mappedBy="voters")
*/
private $answers;
public function __construct()
{
$this->answers = new ArrayCollection;
}
}
?>
-----
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class Question
{
/**
* @ORM\ManyToMany(targetEntity="RemoveInversedByNonFqn")
*/
private $voters;
public function getVoters()
{
return $this->voters;
}
}
$answer = new Question();
$answer->getVoters();
/**
* @ORM\Entity
*/
class RemoveInversedByNonFqn
{
public function __construct()
{
}
}
?>

View File

@ -0,0 +1,48 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity
*/
class FirstOne
{
/**
* @ORM\Column
*/
private $name;
public function getName()
{
return $this->name;
}
}
/**
* @ORM\Entity
*/
class SecondOne
{
/**
* @ORM\Column
*/
private $name;
public function getName()
{
return $this->name;
}
}
class SkipDoubleEntityCall
{
public function callOnMe($entity)
{
if ($entity instanceof FirstOne || $entity instanceof SecondOne) {
$entity->getName();
}
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class SkipIdAndSystem
{
/**
* @ORM\Column
*/
private $id;
public function setId($id)
{
return $this->id = $id;
}
public function getId()
{
return $this->id;
}
public function __toString()
{
return 'keep me';
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Insider
{
/**
* @ORM\Column
*/
private $name;
public function getName()
{
return $this->name;
}
}
trait SkipTraitCalledMethod
{
public function callOnMe(Insider $entity)
{
$entity->getName();
}
}

View File

@ -0,0 +1,35 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Source\SomeEntityProvider;
trait SkipTraitDocTyped
{
/**
* @param SomeEntityProvider $someEntityProvider
*/
public function run($someEntityProvider): void
{
foreach ($someEntityProvider->provide() as $someEntity) {
$test = $someEntity->getStatus();
}
}
}
/**
* @ORM\Entity
*/
class SomeEntity
{
/**
* @var string
*/
private $status;
public function getStatus(): string
{
return $this->status;
}
}

View File

@ -0,0 +1,28 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector;
use Rector\DeadCode\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest extends AbstractRectorTestCase
{
public function test(): void
{
$this->doTestFiles([
__DIR__ . '/Fixture/fixture.php.inc',
__DIR__ . '/Fixture/remove_inversed_by.php.inc',
__DIR__ . '/Fixture/remove_inversed_by_non_fqn.php.inc',
// skip
__DIR__ . '/Fixture/skip_double_entity_call.php.inc',
__DIR__ . '/Fixture/skip_id_and_system.php.inc',
__DIR__ . '/Fixture/skip_trait_called_method.php.inc',
__DIR__ . '/Fixture/skip_trait_doc_typed.php.inc',
]);
}
protected function getRectorClass(): string
{
return RemoveUnusedDoctrineEntityMethodAndPropertyRector::class;
}
}

View File

@ -0,0 +1,16 @@
<?php declare(strict_types=1);
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Source;
use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture\SomeEntity;
class SomeEntityProvider
{
/**
* @return SomeEntity[]
*/
public function provide(): array
{
return [new SomeEntity()];
}
}

View File

@ -5,7 +5,7 @@ services:
Rector\NodeTypeResolver\:
resource: '../src'
exclude: '../src/{Contract,Php/*Info.php}'
exclude: '../src/{Contract,Php/*Info.php,PHPStanOverride/*}'
Rector\Php\TypeAnalyzer: ~
Rector\FileSystem\FilesFinder: ~

View File

@ -0,0 +1,2 @@
extensions:
- Rector\NodeTypeResolver\PHPStanOverride\DependencyInjection\ReplaceNodeScopeResolverClassCompilerExtension

View File

@ -27,6 +27,8 @@ final class PHPStanServicesFactory
$additionalConfigFiles[] = $phpstanPhpunitExtensionConfig;
}
$additionalConfigFiles[] = __DIR__ . '/../../config/phpstan_services_override.neon';
$this->container = $containerFactory->create(sys_get_temp_dir(), $additionalConfigFiles, []);
}

View File

@ -13,6 +13,7 @@ use Rector\NodeTypeResolver\NodeVisitor\NamespaceNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\NodeCollectorNodeVisitor;
use Rector\NodeTypeResolver\NodeVisitor\ParentAndNextNodeVisitor;
use Rector\NodeTypeResolver\PHPStan\Scope\NodeScopeResolver;
use Rector\PhpParser\Node\BetterNodeFinder;
final class NodeScopeAndMetadataDecorator
{
@ -56,6 +57,11 @@ final class NodeScopeAndMetadataDecorator
*/
private $nodeCollectorNodeVisitor;
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
public function __construct(
NodeScopeResolver $nodeScopeResolver,
ParentAndNextNodeVisitor $parentAndNextNodeVisitor,
@ -64,7 +70,8 @@ final class NodeScopeAndMetadataDecorator
NamespaceNodeVisitor $namespaceNodeVisitor,
ExpressionNodeVisitor $expressionNodeVisitor,
FileInfoNodeVisitor $fileInfoNodeVisitor,
NodeCollectorNodeVisitor $nodeCollectorNodeVisitor
NodeCollectorNodeVisitor $nodeCollectorNodeVisitor,
BetterNodeFinder $betterNodeFinder
) {
$this->nodeScopeResolver = $nodeScopeResolver;
$this->parentAndNextNodeVisitor = $parentAndNextNodeVisitor;
@ -74,6 +81,7 @@ final class NodeScopeAndMetadataDecorator
$this->expressionNodeVisitor = $expressionNodeVisitor;
$this->fileInfoNodeVisitor = $fileInfoNodeVisitor;
$this->nodeCollectorNodeVisitor = $nodeCollectorNodeVisitor;
$this->betterNodeFinder = $betterNodeFinder;
}
/**
@ -89,6 +97,15 @@ final class NodeScopeAndMetadataDecorator
]));
$nodes = $nodeTraverser->traverse($nodes);
// dual run of these is conflicting with anonymous classes, e.g. in \Rector\DeadCode\Rector\ClassMethod\RemoveUnusedParameterRector; solve later
if ($this->betterNodeFinder->findFirstInstanceOf($nodes, Node\Stmt\Trait_::class)) {
// needed for trait scoping
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor($this->namespaceNodeVisitor);
$nodeTraverser->addVisitor($this->classAndMethodNodeVisitor);
$nodes = $nodeTraverser->traverse($nodes);
}
$nodes = $this->nodeScopeResolver->processNodes($nodes, $filePath);
$nodeTraverser = new NodeTraverser();

View File

@ -52,7 +52,7 @@ final class ClassAndMethodNodeVisitor extends NodeVisitorAbstract
*/
public function enterNode(Node $node)
{
if ($node instanceof Class_ && $this->isClassAnonymous($node)) {
if ($this->isClassAnonymous($node)) {
return null;
}
@ -108,17 +108,21 @@ final class ClassAndMethodNodeVisitor extends NodeVisitorAbstract
$node->setAttribute(AttributeKey::PARENT_CLASS_NAME, $parentClassResolvedName);
}
private function isClassAnonymous(Class_ $classNode): bool
private function isClassAnonymous(Node $node): bool
{
if ($classNode->isAnonymous()) {
if (! $node instanceof Class_) {
return false;
}
if ($node->isAnonymous()) {
return true;
}
if ($classNode->name === null) {
if ($node->name === null) {
return true;
}
// PHPStan polution
return Strings::startsWith($classNode->name->toString(), 'AnonymousClass');
return Strings::startsWith($node->name->toString(), 'AnonymousClass');
}
}

View File

@ -2,6 +2,7 @@
namespace Rector\NodeTypeResolver\PHPStan\Scope;
use Closure;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
@ -15,6 +16,7 @@ use PHPStan\Broker\Broker;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\RemoveDeepChainMethodCallNodeVisitor;
use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\ScopeTraitNodeVisitor;
use Rector\NodeTypeResolver\PHPStan\Scope\Stub\ClassReflectionForUnusedTrait;
use ReflectionClass;
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
@ -50,18 +52,25 @@ final class NodeScopeResolver
*/
private $privatesAccessor;
/**
* @var ScopeTraitNodeVisitor
*/
private $scopeTraitNodeVisitor;
public function __construct(
ScopeFactory $scopeFactory,
PHPStanNodeScopeResolver $phpStanNodeScopeResolver,
Broker $broker,
RemoveDeepChainMethodCallNodeVisitor $removeDeepChainMethodCallNodeVisitor,
PrivatesAccessor $privatesAccessor
PrivatesAccessor $privatesAccessor,
ScopeTraitNodeVisitor $scopeTraitNodeVisitor
) {
$this->scopeFactory = $scopeFactory;
$this->phpStanNodeScopeResolver = $phpStanNodeScopeResolver;
$this->broker = $broker;
$this->removeDeepChainMethodCallNodeVisitor = $removeDeepChainMethodCallNodeVisitor;
$this->privatesAccessor = $privatesAccessor;
$this->scopeTraitNodeVisitor = $scopeTraitNodeVisitor;
}
/**
@ -73,25 +82,24 @@ final class NodeScopeResolver
$this->removeDeepChainMethodCallNodes($nodes);
$this->phpStanNodeScopeResolver->setAnalysedFiles([$filePath]);
$scope = $this->scopeFactory->createFromFile($filePath);
// skip chain method calls, performance issue: https://github.com/phpstan/phpstan/issues/254
$this->phpStanNodeScopeResolver->processNodes(
$nodes,
$this->scopeFactory->createFromFile($filePath),
function (Node $node, Scope $scope): void {
// the class reflection is resolved AFTER entering to class node
// so we need to get it from the first after this one
if ($node instanceof Class_ || $node instanceof Interface_) {
$scope = $this->resolveClassOrInterfaceScope($node, $scope);
} elseif ($node instanceof Trait_) {
$scope = $this->resolveTraitScope($node, $scope);
}
$node->setAttribute(AttributeKey::SCOPE, $scope);
$nodeCallback = function (Node $node, Scope $scope): void {
// the class reflection is resolved AFTER entering to class node
// so we need to get it from the first after this one
if ($node instanceof Class_ || $node instanceof Interface_) {
$scope = $this->resolveClassOrInterfaceScope($node, $scope);
} elseif ($node instanceof Trait_) {
$scope = $this->resolveTraitScope($node, $scope);
}
);
return $nodes;
$node->setAttribute(AttributeKey::SCOPE, $scope);
};
$this->phpStanNodeScopeResolver->processNodes($nodes, $scope, $nodeCallback);
return $this->resolveScopeInTrait($nodes, $nodeCallback);
}
/**
@ -138,11 +146,8 @@ final class NodeScopeResolver
/** @var ScopeContext $scopeContext */
$scopeContext = $this->privatesAccessor->getPrivateProperty($scope, 'context');
if ($scopeContext->getClassReflection() !== null) {
return $scope->enterTrait($traitReflection);
}
// we need to emulate class reflection, because PHPStan is unable to analyze trait without it
// we need to emulate class reflection, because PHPStan is unable to analyze bare trait without it
$classReflection = new ReflectionClass(ClassReflectionForUnusedTrait::class);
$phpstanClassReflection = $this->broker->getClassFromReflection(
$classReflection,
@ -154,8 +159,24 @@ final class NodeScopeResolver
$this->privatesAccessor->setPrivateProperty($scopeContext, 'classReflection', $phpstanClassReflection);
$traitScope = $scope->enterTrait($traitReflection);
// clear stub
$this->privatesAccessor->setPrivateProperty($scopeContext, 'classReflection', null);
return $traitScope;
}
/**
* @param Node[] $nodes
* @return Node[]
*/
private function resolveScopeInTrait(array $nodes, Closure $nodeCallback): array
{
$traitNodeTraverser = new NodeTraverser();
$this->scopeTraitNodeVisitor->setNodeCallback($nodeCallback);
$traitNodeTraverser->addVisitor($this->scopeTraitNodeVisitor);
return $traitNodeTraverser->traverse($nodes);
}
}

View File

@ -0,0 +1,62 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor;
use Closure;
use PhpParser\Node;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeVisitorAbstract;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* Adds scope to all nodes inside trait even without class that is using it (that what PHPStan needs to add a scope to them)
*/
final class ScopeTraitNodeVisitor extends NodeVisitorAbstract
{
/**
* @var NodeScopeResolver
*/
private $nodeScopeResolver;
/**
* @var Closure
*/
private $nodeCallback;
public function __construct(NodeScopeResolver $nodeScopeResolver)
{
$this->nodeScopeResolver = $nodeScopeResolver;
}
public function setNodeCallback(Closure $nodeCallback): void
{
$this->nodeCallback = $nodeCallback;
}
public function enterNode(Node $node): ?Node
{
if ($this->nodeCallback === null) {
throw new ShouldNotHappenException(sprintf(
'Set "$nodeCallback" property via "setNodeCallback()" on "%s" first',
self::class
));
}
if (! $node instanceof Trait_) {
return null;
}
/** @var Scope|null $traitScope */
$traitScope = $node->getAttribute(AttributeKey::SCOPE);
if ($traitScope === null) {
throw new ShouldNotHappenException(sprintf('A trait "%s" is missing a scope', (string) $node->name));
}
$this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $traitScope, $this->nodeCallback);
return $node;
}
}

View File

@ -0,0 +1,157 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStanOverride\Analyser;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\Function_;
use PhpParser\Node\Stmt\Trait_;
use PHPStan\Analyser\NameScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Broker\Broker;
use PHPStan\File\FileHelper;
use PHPStan\Parser\Parser;
use PHPStan\PhpDoc\PhpDocStringResolver;
use PHPStan\PhpDoc\ResolvedPhpDocBlock;
use PHPStan\PhpDoc\Tag\ParamTag;
use PHPStan\Type\FileTypeMapper;
use PHPStan\Type\Type;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* This services is not used in Rector directly,
* but replaces a services in PHPStan container.
*/
final class StandaloneTraitAwarePHPStanNodeScopeResolver extends NodeScopeResolver
{
/**
* @var PhpDocStringResolver
*/
private $phpDocStringResolver;
/**
* @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
*/
public function __construct(
Broker $broker,
Parser $parser,
FileTypeMapper $fileTypeMapper,
FileHelper $fileHelper,
TypeSpecifier $typeSpecifier,
bool $polluteScopeWithLoopInitialAssignments,
bool $polluteCatchScopeWithTryAssignments,
bool $polluteScopeWithAlwaysIterableForeach,
array $earlyTerminatingMethodCalls,
bool $allowVarTagAboveStatements,
PhpDocStringResolver $phpDocStringResolver
) {
parent::__construct($broker, $parser, $fileTypeMapper, $fileHelper, $typeSpecifier, $polluteScopeWithLoopInitialAssignments, $polluteCatchScopeWithTryAssignments, $polluteScopeWithAlwaysIterableForeach, $earlyTerminatingMethodCalls, $allowVarTagAboveStatements);
$this->phpDocStringResolver = $phpDocStringResolver;
}
/**
* @inheritDoc
*/
public function getPhpDocs(Scope $scope, FunctionLike $functionLike): array
{
if (! $this->isClassMethodInTrait($functionLike) || $functionLike->getDocComment() === null) {
return parent::getPhpDocs($scope, $functionLike);
}
// special case for traits
$phpDocString = $functionLike->getDocComment()->getText();
$nameScope = $this->createNameScope($functionLike);
$resolvedPhpDocs = $this->phpDocStringResolver->resolve($phpDocString, $nameScope);
return $this->convertResolvedPhpDocToArray($resolvedPhpDocs, $functionLike, $scope);
}
private function isClassMethodInTrait(FunctionLike $functionLike): bool
{
if ($functionLike instanceof Function_) {
return false;
}
$classNode = $functionLike->getAttribute(AttributeKey::CLASS_NODE);
return $classNode instanceof Trait_;
}
private function createNameScope(FunctionLike $functionLike): NameScope
{
$namespace = $functionLike->getAttribute(AttributeKey::NAMESPACE_NAME);
/** @var Node\Stmt\Use_[] $useNodes */
$useNodes = $functionLike->getAttribute(AttributeKey::USE_NODES) ?? [];
$uses = [];
foreach ($useNodes as $useNode) {
foreach ($useNode->uses as $useUserNode) {
$useImport = $useUserNode->name->toString();
/** @var string $alias */
$alias = $useUserNode->alias ? (string) $useUserNode->alias : Strings::after($useImport, '\\', -1);
$phpstanAlias = strtolower($alias);
$uses[$phpstanAlias] = $useImport;
}
}
$className = $functionLike->getAttribute(AttributeKey::CLASS_NAME);
return new NameScope($namespace, $uses, $className);
}
/**
* Copy pasted from last part of @see \PHPStan\Analyser\NodeScopeResolver::getPhpDocs()
* @return mixed[]
*/
private function convertResolvedPhpDocToArray(
ResolvedPhpDocBlock $resolvedPhpDocBlock,
FunctionLike $functionLike,
Scope $scope
): array {
$phpDocParameterTypes = $this->resolvePhpDocParameterTypes($resolvedPhpDocBlock);
$nativeReturnType = $scope->getFunctionType($functionLike->getReturnType(), false, false);
$phpDocThrowType = $resolvedPhpDocBlock->getThrowsTag() !== null ? $resolvedPhpDocBlock->getThrowsTag()->getType() : null;
$deprecatedDescription = $resolvedPhpDocBlock->getDeprecatedTag() !== null ? $resolvedPhpDocBlock->getDeprecatedTag()->getMessage() : null;
return [
$phpDocParameterTypes,
$this->resolvePhpDocReturnType($resolvedPhpDocBlock, $nativeReturnType),
$phpDocThrowType,
$deprecatedDescription,
$resolvedPhpDocBlock->isDeprecated(),
$resolvedPhpDocBlock->isInternal(),
$resolvedPhpDocBlock->isFinal(),
];
}
private function resolvePhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDocBlock, Type $nativeReturnType): ?Type
{
if ($resolvedPhpDocBlock->getReturnTag() !== null && (
$nativeReturnType->isSuperTypeOf($resolvedPhpDocBlock->getReturnTag()->getType())->yes()
)) {
return $resolvedPhpDocBlock->getReturnTag()->getType();
}
return null;
}
/**
* @return Type[]
*/
private function resolvePhpDocParameterTypes(ResolvedPhpDocBlock $resolvedPhpDocBlock): array
{
return array_map(static function (ParamTag $tag): Type {
return $tag->getType();
}, $resolvedPhpDocBlock->getParamTags());
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStanOverride\DependencyInjection;
use Nette\DI\CompilerExtension;
use Nette\DI\Definitions\Reference;
use Nette\DI\Definitions\ServiceDefinition;
use Nette\DI\Definitions\Statement;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\PhpDoc\PhpDocStringResolver;
use Rector\NodeTypeResolver\PHPStanOverride\Analyser\StandaloneTraitAwarePHPStanNodeScopeResolver;
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
/**
* This services is not used in Rector directly,
* but replaces a services in PHPStan container.
*/
final class ReplaceNodeScopeResolverClassCompilerExtension extends CompilerExtension
{
/**
* @var PrivatesAccessor
*/
private $privatesAccessor;
public function __construct()
{
$this->privatesAccessor = new PrivatesAccessor();
}
public function beforeCompile(): void
{
/** @var ServiceDefinition $nodeScopeResolver */
$nodeScopeResolver = $this->getContainerBuilder()->getDefinitionByType(NodeScopeResolver::class);
// @see https://github.com/nette/di/blob/47bf203c9ae0f3ccf51de9e5ea309a1cdff4d5e9/src/DI/Definitions/ServiceDefinition.php
/** @var Statement $factory */
$factory = $this->privatesAccessor->getPrivateProperty($nodeScopeResolver, 'factory');
$serviceArguments = $factory->arguments;
// new extra dependency
$serviceArguments['phpDocStringResolver'] = new Reference(PhpDocStringResolver::class);
$serviceArguments['allowVarTagAboveStatements'] = true;
$nodeScopeResolver->setFactory(StandaloneTraitAwarePHPStanNodeScopeResolver::class, $serviceArguments);
}
}

View File

@ -26,6 +26,7 @@ final class StaticTypeToStringResolverTest extends AbstractKernelTestCase
/**
* @dataProvider provideStaticTypesToStrings()
* @param string[] $expectedStrings
*/
public function test(Type $type, array $expectedStrings): void
{

View File

@ -2,11 +2,11 @@
namespace Rector\PHPUnit\Rector;
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\Rector\AbstractPHPUnitRector;
@ -93,7 +93,7 @@ CODE_SAMPLE
continue;
}
/** @var Generic[] $tags */
/** @var GenericTagValueNode[] $tags */
$tags = $this->docBlockManipulator->getTagsByName($node, $annotation);
$methodCallExpressions = array_map(function (PhpDocTagNode $phpDocTagNode) use ($method): Expression {

View File

@ -2,12 +2,9 @@
namespace Rector\TypeDeclaration\PropertyTypeInferer;
use Nette\Utils\Strings;
use PhpParser\Node;
use PhpParser\Node\Stmt\Property;
use PHPStan\PhpDocParser\Ast\PhpDoc\GenericTagValueNode;
use Rector\DeadCode\Doctrine\DoctrineEntityManipulator;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\NamespaceAnalyzer;
use Rector\TypeDeclaration\Contract\PropertyTypeInfererInterface;
final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererInterface
@ -23,11 +20,6 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
*/
private const TO_ONE_ANNOTATIONS = ['Doctrine\ORM\Mapping\ManyToOne', 'Doctrine\ORM\Mapping\OneToOne'];
/**
* @var string
*/
private const JOIN_COLUMN_ANNOTATION = 'Doctrine\ORM\Mapping\JoinColumn';
/**
* @var string
*/
@ -39,14 +31,16 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
private $docBlockManipulator;
/**
* @var NamespaceAnalyzer
* @var DoctrineEntityManipulator
*/
private $namespaceAnalyzer;
private $doctrineEntityManipulator;
public function __construct(DocBlockManipulator $docBlockManipulator, NamespaceAnalyzer $namespaceAnalyzer)
{
public function __construct(
DocBlockManipulator $docBlockManipulator,
DoctrineEntityManipulator $doctrineEntityManipulator
) {
$this->docBlockManipulator = $docBlockManipulator;
$this->namespaceAnalyzer = $namespaceAnalyzer;
$this->doctrineEntityManipulator = $doctrineEntityManipulator;
}
/**
@ -59,7 +53,7 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
continue;
}
return $this->processToManyRelation($property, $doctrineRelationAnnotation);
return $this->processToManyRelation($property);
}
foreach (self::TO_ONE_ANNOTATIONS as $doctrineRelationAnnotation) {
@ -67,7 +61,7 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
continue;
}
return $this->processToOneRelation($property, $doctrineRelationAnnotation);
return $this->processToOneRelation($property);
}
return [];
@ -81,12 +75,12 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
/**
* @return string[]
*/
private function processToManyRelation(Property $property, string $doctrineRelationAnnotation): array
private function processToManyRelation(Property $property): array
{
$types = [];
$relationType = $this->resolveRelationType($property, $doctrineRelationAnnotation);
if ($relationType) {
$relationType = $this->doctrineEntityManipulator->resolveTargetClass($property);
if ($relationType !== null) {
$types[] = $relationType . '[]';
}
@ -98,65 +92,19 @@ final class DoctrineRelationPropertyTypeInferer implements PropertyTypeInfererIn
/**
* @return string[]
*/
private function processToOneRelation(Property $property, string $doctrineRelationAnnotation): array
private function processToOneRelation(Property $property): array
{
$types = [];
$relationType = $this->resolveRelationType($property, $doctrineRelationAnnotation);
if ($relationType) {
$relationType = $this->doctrineEntityManipulator->resolveTargetClass($property);
if ($relationType !== null) {
$types[] = $relationType;
}
if ($this->isNullableOneRelation($property)) {
if ($this->doctrineEntityManipulator->isNullableRelation($property)) {
$types[] = 'null';
}
return $types;
}
private function resolveTargetEntity(GenericTagValueNode $genericTagValueNode): ?string
{
$match = Strings::match($genericTagValueNode->value, '#targetEntity=\"(?<targetEntity>.*?)\"#');
return $match['targetEntity'] ?? null;
}
private function resolveRelationType(Property $property, string $doctrineRelationAnnotation): ?string
{
$relationTag = $this->docBlockManipulator->getTagByName($property, $doctrineRelationAnnotation);
if ($relationTag->value instanceof GenericTagValueNode) {
$resolveTargetType = $this->resolveTargetEntity($relationTag->value);
if ($resolveTargetType) {
if (Strings::contains($resolveTargetType, '\\')) {
return $resolveTargetType;
}
// is FQN?
if (! class_exists($resolveTargetType)) {
return $this->namespaceAnalyzer->resolveTypeToFullyQualified($resolveTargetType, $property);
}
return $resolveTargetType;
}
}
return null;
}
private function isNullableOneRelation(Node $node): bool
{
if (! $this->docBlockManipulator->hasTag($node, self::JOIN_COLUMN_ANNOTATION)) {
// @see https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/annotations-reference.html#joincolumn
return true;
}
$joinColumnTag = $this->docBlockManipulator->getTagByName($node, self::JOIN_COLUMN_ANNOTATION);
if ($joinColumnTag->value instanceof GenericTagValueNode) {
return (bool) Strings::match($joinColumnTag->value->value, '#nullable=true#');
}
return false;
}
}

View File

@ -179,3 +179,11 @@ parameters:
- '#Parameter \#2 \$listener of method Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher\:\:getListenerPriority\(\) expects callable\(\)\: mixed, array given#'
- '#Parameter \#1 \$kernelClass of method Rector\\Symfony\\Bridge\\DependencyInjection\\SymfonyContainerFactory\:\:createFromKernelClass\(\) expects string, string\|null given#'
- '#If condition is always true#'
# known value
- '#Method Rector\\PhpParser\\Node\\Manipulator\\ClassMethodManipulator\:\:addMethodParameterIfMissing\(\) should return string but returns string\|null#'
# symfony future compatibility
- '#Call to an undefined static method Symfony\\Component\\EventDispatcher\\EventDispatcher\:\:__construct\(\)#'
- '#Rector\\EventDispatcher\\AutowiredEventDispatcher\:\:__construct\(\) calls parent constructor but parent does not have one#'
- '#Method Rector\\NodeTypeResolver\\PHPStanOverride\\Analyser\\StandaloneTraitAwarePHPStanNodeScopeResolver\:\:getPhpDocs\(\) should return array\(array<PHPStan\\Type\\Type\>, PHPStan\\Type\\Type\|null, PHPStan\\Type\\Type\|null, string\|null, bool, bool, bool\) but returns array#'

View File

@ -584,15 +584,9 @@ final class ParsedNodesByType
*/
private function addCall(Node $node): void
{
if ($node instanceof MethodCall && $node->var instanceof Variable && $node->var->name === 'this') {
$className = $node->getAttribute(AttributeKey::CLASS_NAME);
} elseif ($node instanceof MethodCall) {
$className = $this->nodeTypeResolver->resolve($node->var)[0] ?? null;
} else {
$className = $this->nodeTypeResolver->resolve($node)[0] ?? null;
}
if ($className === null) { // anonymous
// one node can be of multiple-class types
$classTypes = $this->resolveNodeClassTypes($node);
if ($classTypes === []) { // anonymous
return;
}
@ -601,7 +595,9 @@ final class ParsedNodesByType
return;
}
$this->methodsCallsByTypeAndMethod[$className][$methodName][] = $node;
foreach ($classTypes as $classType) {
$this->methodsCallsByTypeAndMethod[$classType][$methodName][] = $node;
}
}
/**
@ -669,4 +665,21 @@ final class ParsedNodesByType
return false;
}
/**
* @return string[]
*/
private function resolveNodeClassTypes(Node $node): array
{
if ($node instanceof MethodCall && $node->var instanceof Variable && $node->var->name === 'this') {
$className = $node->getAttribute(AttributeKey::CLASS_NAME);
return $className ? [$className] : [];
}
if ($node instanceof MethodCall) {
return $this->nodeTypeResolver->resolve($node->var);
}
return $this->nodeTypeResolver->resolve($node);
}
}

View File

@ -290,6 +290,53 @@ final class ClassManipulator
return null;
}
/**
* @return string[]
*/
public function getPrivatePropertyNames(Class_ $class): array
{
$privatePropertyNames = [];
foreach ($class->stmts as $stmt) {
if (! $stmt instanceof Property) {
continue;
}
if (! $stmt->isPrivate()) {
continue;
}
/** @var string $propertyName */
$propertyName = $this->nameResolver->getName($stmt);
$privatePropertyNames[] = $propertyName;
}
return $privatePropertyNames;
}
/**
* @return string[]
*/
public function getPublicMethodNames(Class_ $class): array
{
$publicMethodNames = [];
foreach ($class->getMethods() as $method) {
if ($method->isAbstract()) {
continue;
}
if (! $method->isPublic()) {
continue;
}
/** @var string $methodName */
$methodName = $this->nameResolver->getName($method);
$publicMethodNames[] = $methodName;
}
return $publicMethodNames;
}
private function tryInsertBeforeFirstMethod(Class_ $classNode, Stmt $stmt): bool
{
foreach ($classNode->stmts as $key => $classElementNode) {

View File

@ -5,6 +5,7 @@ namespace Rector\PhpParser\Node\Manipulator;
use Iterator;
use PhpParser\Node;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
@ -174,8 +175,8 @@ final class FunctionLikeManipulator
*/
private function resolveFromYieldNodes(FunctionLike $functionLike): array
{
/** @var Node\Expr\Yield_[] $yieldNodes */
$yieldNodes = $this->betterNodeFinder->findInstanceOf((array) $functionLike->stmts, Node\Expr\Yield_::class);
/** @var Yield_[] $yieldNodes */
$yieldNodes = $this->betterNodeFinder->findInstanceOf((array) $functionLike->stmts, Yield_::class);
if (count($yieldNodes)) {
$this->isVoid = false;

View File

@ -4,7 +4,7 @@ services:
# $node->geAttribute($1) => Type|null by $1
- { class: Rector\PHPStanExtensions\Rector\Type\GetAttributeReturnTypeExtension, tags: [phpstan.broker.dynamicMethodReturnTypeExtension] }
# $nameResolver->resolve() => in some cases always string
# $nameResolver->getName() => in some cases always string
- { class: Rector\PHPStanExtensions\Rector\Type\NameResolverReturnTypeExtension, tags: [phpstan.broker.dynamicMethodReturnTypeExtension] }
# $nameResolverTrait->getName() => in some cases always string
- { class: Rector\PHPStanExtensions\Rector\Type\NameResolverTraitReturnTypeExtension, tags: [phpstan.broker.dynamicMethodReturnTypeExtension] }