Improve trait node scope resolving

This commit is contained in:
Tomas Votruba 2019-08-05 18:52:55 +02:00
parent 59222aa086
commit a4302b44be
17 changed files with 163 additions and 365 deletions

View File

@ -15,7 +15,6 @@
"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",

View File

@ -27,3 +27,8 @@ trait SkipTraitCalledMethod
$entity->getName();
}
}
class SkipTraitCalledMethodAddict
{
use SkipTraitCalledMethod;
}

View File

@ -0,0 +1,57 @@
<?php
namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Fixture;
use Doctrine\ORM\Mapping as ORM;
use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Source\SomeEntityProvider;
class SomeClassOnlyUsingTrait
{
use SkipTraitComplex;
}
trait SkipTraitComplex
{
public function run(): void
{
$provider = self::getProvider();
$entities = $provider->provideMultiple([]);
foreach ($entities as $entity) {
$entity->getStatus();
}
}
public static function getProvider(): SomeStatusEntityProvider
{
return new SomeStatusEntityProvider;
}
}
/**
* @ORM\Entity
*/
class SomeStatusEntity
{
/**
* @var string
*/
private $status;
public function getStatus(): string
{
return $this->status;
}
}
class SomeStatusEntityProvider
{
/**
* @return SomeStatusEntity[]
*/
public function provideMultiple($items): array
{
return $items;
}
}

View File

@ -5,6 +5,11 @@ namespace Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAn
use Doctrine\ORM\Mapping as ORM;
use Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\Source\SomeEntityProvider;
class SkipTraitDocTypedAddict
{
use SkipTraitDocTyped;
}
trait SkipTraitDocTyped
{
/**

View File

@ -18,6 +18,7 @@ final class RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest extends Abstra
__DIR__ . '/Fixture/skip_id_and_system.php.inc',
__DIR__ . '/Fixture/skip_trait_called_method.php.inc',
__DIR__ . '/Fixture/skip_trait_doc_typed.php.inc',
__DIR__ . '/Fixture/skip_trait_complex.php.inc',
]);
}

View File

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

View File

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

View File

@ -459,6 +459,7 @@ final class NodeTypeResolver
}
$type = $nodeScope->getType($staticCall);
if ($type instanceof ObjectType) {
return [$type->getClassName()];
}

View File

@ -0,0 +1,56 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStan\Collector;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Node\VirtualNode;
use Rector\PhpParser\Printer\BetterStandardPrinter;
final class TraitNodeScopeCollector
{
/**
* @var BetterStandardPrinter
*/
private $betterStandardPrinter;
/**
* @var Scope[]
*/
private $scopeByTraitNodeHash = [];
public function __construct(BetterStandardPrinter $betterStandardPrinter)
{
$this->betterStandardPrinter = $betterStandardPrinter;
}
public function addForTraitAndNode(string $traitName, Node $node, Scope $scope): void
{
if ($node instanceof VirtualNode) {
return;
}
$traitNodeHash = $this->createHash($traitName, $node);
// probably set from another class
if (isset($this->scopeByTraitNodeHash[$traitNodeHash])) {
return;
}
$this->scopeByTraitNodeHash[$traitNodeHash] = $scope;
}
public function getScopeForTraitAndNode(string $traitName, Node $node): ?Scope
{
$traitNodeHash = $this->createHash($traitName, $node);
return $this->scopeByTraitNodeHash[$traitNodeHash] ?? null;
}
private function createHash(string $traitName, Node $node): string
{
$printedNode = $this->betterStandardPrinter->print($node);
return sha1($traitName . $printedNode);
}
}

View File

@ -2,7 +2,6 @@
namespace Rector\NodeTypeResolver\PHPStan\Scope;
use Closure;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
@ -11,15 +10,11 @@ use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeTraverser;
use PHPStan\Analyser\NodeScopeResolver as PHPStanNodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\ScopeContext;
use PHPStan\Broker\Broker;
use Rector\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
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;
/**
* @inspired by https://github.com/silverstripe/silverstripe-upgrader/blob/532182b23e854d02e0b27e68ebc394f436de0682/src/UpgradeRule/PHP/Visitor/PHPStanScopeVisitor.php
@ -48,29 +43,22 @@ final class NodeScopeResolver
private $removeDeepChainMethodCallNodeVisitor;
/**
* @var PrivatesAccessor
* @var TraitNodeScopeCollector
*/
private $privatesAccessor;
/**
* @var ScopeTraitNodeVisitor
*/
private $scopeTraitNodeVisitor;
private $traitNodeScopeCollector;
public function __construct(
ScopeFactory $scopeFactory,
PHPStanNodeScopeResolver $phpStanNodeScopeResolver,
Broker $broker,
RemoveDeepChainMethodCallNodeVisitor $removeDeepChainMethodCallNodeVisitor,
PrivatesAccessor $privatesAccessor,
ScopeTraitNodeVisitor $scopeTraitNodeVisitor
TraitNodeScopeCollector $traitNodeScopeCollector
) {
$this->scopeFactory = $scopeFactory;
$this->phpStanNodeScopeResolver = $phpStanNodeScopeResolver;
$this->broker = $broker;
$this->removeDeepChainMethodCallNodeVisitor = $removeDeepChainMethodCallNodeVisitor;
$this->privatesAccessor = $privatesAccessor;
$this->scopeTraitNodeVisitor = $scopeTraitNodeVisitor;
$this->traitNodeScopeCollector = $traitNodeScopeCollector;
}
/**
@ -90,8 +78,14 @@ final class NodeScopeResolver
// 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);
}
// traversing trait inside class that is using it scope (from referenced) - the trait traversed by Rector is different (directly from parsed file)
if ($scope->isInTrait()) {
$traitName = $scope->getTraitReflection()->getName();
$this->traitNodeScopeCollector->addForTraitAndNode($traitName, $node, $scope);
return;
}
$node->setAttribute(AttributeKey::SCOPE, $scope);
@ -99,7 +93,7 @@ final class NodeScopeResolver
$this->phpStanNodeScopeResolver->processNodes($nodes, $scope, $nodeCallback);
return $this->resolveScopeInTrait($nodes, $nodeCallback);
return $nodes;
}
/**
@ -138,45 +132,4 @@ final class NodeScopeResolver
return $classOrInterfaceNode->name->toString();
}
private function resolveTraitScope(Trait_ $trait, Scope $scope): Scope
{
$traitName = $this->resolveClassName($trait);
$traitReflection = $this->broker->getClass($traitName);
/** @var ScopeContext $scopeContext */
$scopeContext = $this->privatesAccessor->getPrivateProperty($scope, 'context');
// 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,
ClassReflectionForUnusedTrait::class,
null
);
// set stub
$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

@ -1,62 +0,0 @@
<?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

@ -1,7 +0,0 @@
<?php declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStan\Scope\Stub;
final class ClassReflectionForUnusedTrait
{
}

View File

@ -1,157 +0,0 @@
<?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

@ -1,46 +0,0 @@
<?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

@ -5,6 +5,7 @@ namespace Rector\NodeTypeResolver\PerNodeTypeResolver;
use PhpParser\Node;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Trait_;
use PHPStan\Analyser\Scope;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ThisType;
@ -13,6 +14,7 @@ use Rector\NodeTypeResolver\Contract\PerNodeTypeResolver\PerNodeTypeResolverInte
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Rector\NodeTypeResolver\PhpDoc\NodeAnalyzer\DocBlockManipulator;
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
use Rector\NodeTypeResolver\PHPStan\Type\StaticTypeToStringResolver;
use Rector\PhpParser\Node\Resolver\NameResolver;
@ -38,14 +40,21 @@ final class VariableTypeResolver implements PerNodeTypeResolverInterface, NodeTy
*/
private $nodeTypeResolver;
/**
* @var TraitNodeScopeCollector
*/
private $traitNodeScopeCollector;
public function __construct(
DocBlockManipulator $docBlockManipulator,
StaticTypeToStringResolver $staticTypeToStringResolver,
NameResolver $nameResolver
NameResolver $nameResolver,
TraitNodeScopeCollector $traitNodeScopeCollector
) {
$this->docBlockManipulator = $docBlockManipulator;
$this->staticTypeToStringResolver = $staticTypeToStringResolver;
$this->nameResolver = $nameResolver;
$this->traitNodeScopeCollector = $traitNodeScopeCollector;
}
/**
@ -93,33 +102,31 @@ final class VariableTypeResolver implements PerNodeTypeResolverInterface, NodeTy
$this->nodeTypeResolver = $nodeTypeResolver;
}
private function resolveNodeScope(Node $variableNode): ?Scope
private function resolveNodeScope(Node $node): ?Scope
{
/** @var Scope|null $nodeScope */
$nodeScope = $variableNode->getAttribute(AttributeKey::SCOPE);
if ($nodeScope instanceof Scope) {
return $nodeScope;
$nodeScope = $node->getAttribute(AttributeKey::SCOPE);
// is node in trait
$classNode = $node->getAttribute(AttributeKey::CLASS_NODE);
if ($classNode instanceof Trait_) {
/** @var string $traitName */
$traitName = $node->getAttribute(AttributeKey::CLASS_NAME);
$traitNodeScope = $this->traitNodeScopeCollector->getScopeForTraitAndNode($traitName, $node);
}
$parentNode = $variableNode->getAttribute(AttributeKey::PARENT_NODE);
$parentNode = $node->getAttribute(AttributeKey::PARENT_NODE);
if ($parentNode instanceof Node) {
$nodeScope = $parentNode->getAttribute(AttributeKey::SCOPE);
if ($nodeScope instanceof Scope) {
return $nodeScope;
}
$parentNodeScope = $parentNode->getAttribute(AttributeKey::SCOPE);
}
// get nearest variable scope
$method = $variableNode->getAttribute(AttributeKey::METHOD_NODE);
$method = $node->getAttribute(AttributeKey::METHOD_NODE);
if ($method instanceof Node) {
$nodeScope = $method->getAttribute(AttributeKey::SCOPE);
if ($nodeScope instanceof Scope) {
return $nodeScope;
}
$methodNodeScope = $method->getAttribute(AttributeKey::SCOPE);
}
// unknown scope
return null;
return $nodeScope ?? $traitNodeScope ?? $parentNodeScope ?? $methodNodeScope ?? null;
}
/**

View File

@ -186,4 +186,3 @@ parameters:
# 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

@ -124,14 +124,6 @@ final class ParsedNodesByType
return $this->simpleParsedNodesByType[New_::class] ?? [];
}
/**
* @return StaticCall[]
*/
public function getStaticCallNodes(): array
{
return $this->simpleParsedNodesByType[StaticCall::class] ?? [];
}
/**
* Due to circular reference
* @required
@ -536,7 +528,6 @@ final class ParsedNodesByType
}
$methodName = $this->nameResolver->getName($classMethod);
$this->methodsByType[$className][$methodName] = $classMethod;
}