mirror of
https://github.com/rectorphp/rector.git
synced 2025-03-24 09:19:47 +01:00
[DeadCode] Add RemoveUnusedDoctrineEntityMethodAndPropertyRector (#1800)
[DeadCode] Add RemoveUnusedDoctrineEntityMethodAndPropertyRector
This commit is contained in:
commit
59222aa086
@ -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",
|
||||
|
@ -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: ~
|
||||
|
1
ecs.yaml
1
ecs.yaml
@ -39,6 +39,7 @@ services:
|
||||
- 'Symplify\PackageBuilder\*'
|
||||
- 'Symfony\Component\Console\Input\*Input'
|
||||
- '*ValueObject'
|
||||
- 'PHPStan\Analyser\NameScope'
|
||||
|
||||
Symplify\CodingStandard\Fixer\Naming\PropertyNameMatchingTypeFixer:
|
||||
extra_skipped_classes:
|
||||
|
@ -301,6 +301,10 @@ CODE_SAMPLE
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node $node
|
||||
* @return string[]|null
|
||||
*/
|
||||
private function resolveAssignPropertyToVariableOrNull(Node $node): ?array
|
||||
{
|
||||
if ($node instanceof Expression) {
|
||||
|
@ -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
|
||||
|
167
packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php
Normal file
167
packages/DeadCode/src/Doctrine/DoctrineEntityManipulator.php
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
@ -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',
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
}
|
||||
|
||||
?>
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()];
|
||||
}
|
||||
}
|
@ -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: ~
|
||||
|
@ -0,0 +1,2 @@
|
||||
extensions:
|
||||
- Rector\NodeTypeResolver\PHPStanOverride\DependencyInjection\ReplaceNodeScopeResolverClassCompilerExtension
|
@ -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, []);
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ final class StaticTypeToStringResolverTest extends AbstractKernelTestCase
|
||||
|
||||
/**
|
||||
* @dataProvider provideStaticTypesToStrings()
|
||||
* @param string[] $expectedStrings
|
||||
*/
|
||||
public function test(Type $type, array $expectedStrings): void
|
||||
{
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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#'
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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] }
|
||||
|
Loading…
x
Reference in New Issue
Block a user