rector/packages/node-type-resolver/src/PHPStan/Scope/PHPStanNodeScopeResolver.php

260 lines
8.2 KiB
PHP
Raw Normal View History

2019-10-13 05:59:52 +00:00
<?php
declare(strict_types=1);
namespace Rector\NodeTypeResolver\PHPStan\Scope;
2020-07-26 09:00:23 +00:00
use Nette\Utils\Strings;
use PhpParser\Node;
2018-08-04 20:32:01 +00:00
use PhpParser\Node\Stmt\Class_;
2019-03-10 23:47:43 +00:00
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Interface_;
2019-05-12 08:19:38 +00:00
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeTraverser;
2020-04-01 01:55:44 +00:00
use PHPStan\AnalysedCodeException;
2019-12-08 21:00:38 +00:00
use PHPStan\Analyser\MutatingScope;
2020-06-16 19:43:45 +00:00
use PHPStan\Analyser\NodeScopeResolver;
2020-08-27 10:19:22 +00:00
use PHPStan\Analyser\Scope;
use PHPStan\Node\UnreachableStatementNode;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Caching\Detector\ChangedFilesDetector;
2020-04-01 01:55:44 +00:00
use Rector\Caching\FileSystem\DependencyResolver;
use Rector\Core\Configuration\Configuration;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
2019-08-05 16:52:55 +00:00
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\RemoveDeepChainMethodCallNodeVisitor;
2020-04-01 01:55:44 +00:00
use Symfony\Component\Console\Style\SymfonyStyle;
2020-04-19 17:56:05 +00:00
use Symplify\SmartFileSystem\SmartFileInfo;
/**
* @inspired by https://github.com/silverstripe/silverstripe-upgrader/blob/532182b23e854d02e0b27e68ebc394f436de0682/src/UpgradeRule/PHP/Visitor/PHPStanScopeVisitor.php
* - https://github.com/silverstripe/silverstripe-upgrader/pull/57/commits/e5c7cfa166ad940d9d4ff69537d9f7608e992359#diff-5e0807bb3dc03d6a8d8b6ad049abd774
*/
2020-06-16 19:43:45 +00:00
final class PHPStanNodeScopeResolver
{
/**
* @var string
*/
private const ANONYMOUS_CLASS_START_REGEX = '#^AnonymousClass(\w+)#';
/**
* @var string[]
*/
private $dependentFiles = [];
/**
2020-06-16 19:43:45 +00:00
* @var NodeScopeResolver
*/
2020-06-16 19:43:45 +00:00
private $nodeScopeResolver;
/**
* @var ScopeFactory
*/
private $scopeFactory;
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
/**
* @var RemoveDeepChainMethodCallNodeVisitor
*/
private $removeDeepChainMethodCallNodeVisitor;
2019-05-12 08:19:38 +00:00
/**
2019-08-05 16:52:55 +00:00
* @var TraitNodeScopeCollector
2019-05-12 08:19:38 +00:00
*/
2019-08-05 16:52:55 +00:00
private $traitNodeScopeCollector;
2020-04-01 01:55:44 +00:00
/**
* @var DependencyResolver
*/
private $dependencyResolver;
/**
* @var ChangedFilesDetector
*/
private $changedFilesDetector;
/**
* @var Configuration
*/
private $configuration;
/**
* @var SymfonyStyle
*/
private $symfonyStyle;
public function __construct(
2020-04-01 01:55:44 +00:00
ChangedFilesDetector $changedFilesDetector,
2020-07-26 07:49:22 +00:00
Configuration $configuration,
DependencyResolver $dependencyResolver,
2020-06-16 19:43:45 +00:00
NodeScopeResolver $nodeScopeResolver,
ReflectionProvider $reflectionProvider,
2019-05-12 08:19:38 +00:00
RemoveDeepChainMethodCallNodeVisitor $removeDeepChainMethodCallNodeVisitor,
2020-07-26 07:49:22 +00:00
ScopeFactory $scopeFactory,
2020-06-16 19:43:45 +00:00
SymfonyStyle $symfonyStyle,
2020-07-26 07:49:22 +00:00
TraitNodeScopeCollector $traitNodeScopeCollector
) {
$this->scopeFactory = $scopeFactory;
2020-06-16 19:43:45 +00:00
$this->nodeScopeResolver = $nodeScopeResolver;
$this->reflectionProvider = $reflectionProvider;
$this->removeDeepChainMethodCallNodeVisitor = $removeDeepChainMethodCallNodeVisitor;
2019-08-05 16:52:55 +00:00
$this->traitNodeScopeCollector = $traitNodeScopeCollector;
2020-04-01 01:55:44 +00:00
$this->dependencyResolver = $dependencyResolver;
$this->changedFilesDetector = $changedFilesDetector;
$this->configuration = $configuration;
$this->symfonyStyle = $symfonyStyle;
}
/**
* @param Node[] $nodes
* @return Node[]
*/
2020-04-19 17:56:05 +00:00
public function processNodes(array $nodes, SmartFileInfo $smartFileInfo): array
{
$this->removeDeepChainMethodCallNodes($nodes);
2020-04-19 17:56:05 +00:00
$scope = $this->scopeFactory->createFromFile($smartFileInfo);
2020-04-01 01:55:44 +00:00
$this->dependentFiles = [];
// skip chain method calls, performance issue: https://github.com/phpstan/phpstan/issues/254
2020-08-27 10:19:22 +00:00
$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_) {
2020-08-27 10:19:22 +00:00
/** @var Scope $scope */
$scope = $this->resolveClassOrInterfaceScope($node, $scope);
2019-08-05 16:52:55 +00:00
}
// 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();
2019-08-05 16:52:55 +00:00
$this->traitNodeScopeCollector->addForTraitAndNode($traitName, $node, $scope);
return;
}
// special case for unreachable nodes
if ($node instanceof UnreachableStatementNode) {
$originalNode = $node->getOriginalStatement();
$originalNode->setAttribute(AttributeKey::IS_UNREACHABLE, true);
$originalNode->setAttribute(AttributeKey::SCOPE, $scope);
} else {
$node->setAttribute(AttributeKey::SCOPE, $scope);
}
};
2020-04-01 01:55:44 +00:00
foreach ($nodes as $node) {
2020-04-01 01:55:44 +00:00
$this->resolveDependentFiles($node, $scope);
}
2019-12-08 21:00:38 +00:00
/** @var MutatingScope $scope */
2020-06-16 19:43:45 +00:00
$this->nodeScopeResolver->processNodes($nodes, $scope, $nodeCallback);
2020-04-01 01:55:44 +00:00
$this->reportCacheDebugAndSaveDependentFiles($smartFileInfo, $this->dependentFiles);
2019-08-05 16:52:55 +00:00
return $nodes;
}
2018-12-16 13:24:31 +00:00
/**
* @param Node[] $nodes
*/
private function removeDeepChainMethodCallNodes(array $nodes): void
{
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor($this->removeDeepChainMethodCallNodeVisitor);
$nodeTraverser->traverse($nodes);
}
/**
2020-07-26 09:00:23 +00:00
* @param Class_|Interface_ $classLike
*/
2020-08-27 10:19:22 +00:00
private function resolveClassOrInterfaceScope(ClassLike $classLike, Scope $scope): MutatingScope
{
2020-07-26 09:00:23 +00:00
$className = $this->resolveClassName($classLike);
// is anonymous class? - not possible to enter it since PHPStan 0.12.33, see https://github.com/phpstan/phpstan-src/commit/e87fb0ec26f9c8552bbeef26a868b1e5d8185e91
if ($classLike instanceof Class_ && Strings::match($className, self::ANONYMOUS_CLASS_START_REGEX)) {
2020-08-27 10:19:22 +00:00
$classReflection = $this->reflectionProvider->getAnonymousClassReflection($classLike, $scope);
2020-07-26 09:00:23 +00:00
} else {
$classReflection = $this->reflectionProvider->getClass($className);
}
2019-03-10 23:47:43 +00:00
2020-08-27 10:19:22 +00:00
/** @var MutatingScope $scope */
return $scope->enterClass($classReflection);
2019-03-10 23:47:43 +00:00
}
2020-08-27 10:19:22 +00:00
private function resolveDependentFiles(Node $node, Scope $scope): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
try {
2020-08-27 10:19:22 +00:00
foreach ($this->dependencyResolver->resolveDependencies($node, $scope) as $dependentFile) {
$this->dependentFiles[] = $dependentFile;
}
} catch (AnalysedCodeException $analysedCodeException) {
// @ignoreException
}
}
/**
* @param string[] $dependentFiles
*/
private function reportCacheDebugAndSaveDependentFiles(SmartFileInfo $smartFileInfo, array $dependentFiles): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
$this->reportCacheDebug($smartFileInfo, $dependentFiles);
// save for cache
$this->changedFilesDetector->addFileWithDependencies($smartFileInfo, $dependentFiles);
}
2019-03-10 23:47:43 +00:00
/**
* @param Class_|Interface_|Trait_ $classLike
2019-03-10 23:47:43 +00:00
*/
private function resolveClassName(ClassLike $classLike): string
{
2020-07-04 15:37:44 +00:00
if (property_exists($classLike, 'namespacedName')) {
return (string) $classLike->namespacedName;
}
if ($classLike->name === null) {
2019-09-21 11:03:30 +00:00
throw new ShouldNotHappenException();
}
return $classLike->name->toString();
}
2020-04-01 01:55:44 +00:00
/**
* @param string[] $dependentFiles
*/
2020-04-01 01:55:44 +00:00
private function reportCacheDebug(SmartFileInfo $smartFileInfo, array $dependentFiles): void
{
if (! $this->configuration->isCacheDebug()) {
return;
}
2020-07-05 23:57:19 +00:00
$message = sprintf(
'[debug] %d dependencies for %s file',
count($dependentFiles),
$smartFileInfo->getRealPath()
2020-04-01 01:55:44 +00:00
);
2020-07-05 23:57:19 +00:00
$this->symfonyStyle->note($message);
2020-04-01 01:55:44 +00:00
if ($dependentFiles !== []) {
$this->symfonyStyle->listing($dependentFiles);
}
}
}