Add changed files caching

This commit is contained in:
TomasVotruba 2020-04-01 03:55:44 +02:00
parent 7e2394e9b6
commit 5f4a2c1e3b
60 changed files with 1027 additions and 144 deletions

View File

@ -38,7 +38,7 @@ jobs:
- run: composer install --no-progress
## First run Rector
- run: composer rector-ci-fix
- run: composer rector
-
name: Check for Rector modified files

View File

@ -21,7 +21,7 @@ jobs:
# cannot install dev deps (--no-dev), because doctrine/orm might inherit from them in different version
composer install --no-progress --no-dev
# loads autoload-dev packages as well, there are utils that might be used later
git clone https://github.com/doctrine/orm.git
git clone https://github.com/doctrine/orm.git --depth 1
# older version have breaking "object" type
composer require doctrine/cache:^1.10 -d orm --no-update

View File

@ -3,6 +3,7 @@
declare(strict_types=1);
use Rector\Caching\ChangedFilesDetector;
use Rector\Core\Configuration\Configuration;
use Rector\Core\Console\Application;
use Rector\Core\Console\Style\SymfonyStyleFactory;
@ -36,6 +37,10 @@ try {
/** @var Configuration $configuration */
$configuration = $container->get(Configuration::class);
$configuration->setFirstResolverConfig($rectorConfigsResolver->getFirstResolvedConfig());
/** @var ChangedFilesDetector $changedFilesDetector */
$changedFilesDetector = $container->get(ChangedFilesDetector::class);
$changedFilesDetector->setFirstUsedConfig(realpath($rectorConfigsResolver->getFirstResolvedConfig()));
} catch (Throwable $throwable) {
$symfonyStyle = (new SymfonyStyleFactory(new PrivatesCaller()))->create();
$symfonyStyle->error($throwable->getMessage());

View File

@ -1,5 +1,6 @@
parameters:
auto_import_names: false
sets:
- "dead-code"

View File

@ -24,7 +24,9 @@
"phpstan/phpdoc-parser": "^0.4",
"phpstan/phpstan": "^0.12.19",
"phpstan/phpstan-phpunit": "^0.12",
"psr/simple-cache": "^1.0",
"sebastian/diff": "^3.0|^4.0",
"symfony/cache": "^4.4.6|^5.0.6",
"symfony/console": "^4.4.6|^5.0.6",
"symfony/dependency-injection": "^4.4.6|^5.0.6",
"symfony/finder": "^4.4.6|^5.0.6",
@ -88,6 +90,7 @@
"Rector\\PHPStanStaticTypeMapper\\": "packages/phpstan-static-type-mapper/src",
"Rector\\PHPStan\\": "rules/phpstan/src",
"Rector\\PHPUnitSymfony\\": "rules/phpunit-symfony/src",
"Rector\\Caching\\": "packages/caching/src",
"Rector\\PHPUnit\\": "rules/phpunit/src",
"Rector\\PSR4\\": "rules/psr4/src",
"Rector\\Phalcon\\": "rules/phalcon/src",
@ -138,6 +141,7 @@
"Rector\\CakePHP\\Tests\\": "rules/cakephp/tests",
"Rector\\Celebrity\\Tests\\": "rules/celebrity/tests",
"Rector\\CodeQuality\\Tests\\": "rules/code-quality/tests",
"Rector\\Caching\\Tests\\": "packages/caching/tests",
"Rector\\CodingStyle\\Tests\\": "rules/coding-style/tests",
"Rector\\Core\\Tests\\": "tests",
"Rector\\DeadCode\\Tests\\": "rules/dead-code/tests",
@ -263,8 +267,7 @@
"bin/rector dump-rectors > docs/AllRectorsOverview.md",
"bin/rector dump-nodes > docs/NodesOverview.md"
],
"rector-ci-fix": "bin/rector process --config rector-ci.yaml --ansi --no-progress-bar",
"rector-ci": "bin/rector process --config rector-ci.yaml --ansi --no-progress-bar --dry-run",
"rector-ci": "bin/rector process --config rector-ci.yaml --debug --dry-run",
"rector": "bin/rector process --config rector-ci.yaml --ansi"
},
"config": {

View File

@ -0,0 +1,30 @@
parameters:
is_cache_enabled: false
services:
_defaults:
autowire: true
public: true
Rector\Caching\:
resource: '../src'
PHPStan\Dependency\DependencyResolver:
factory: ['@Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory', 'createDependencyResolver']
PHPStan\File\FileHelper:
factory: ['@Rector\NodeTypeResolver\DependencyInjection\PHPStanServicesFactory', 'createFileHelper']
# inspired from https://raw.githubusercontent.com/symplify/easy-coding-standard/master/config/services/services_cache.yaml
Symfony\Component\Cache\Psr16Cache: ~
Psr\SimpleCache\CacheInterface: '@Symfony\Component\Cache\Psr16Cache'
Symfony\Component\Cache\Adapter\FilesystemAdapter:
factory: ['@Rector\Caching\Cache\Adapter\FilesystemAdapterFactory', 'create']
Symfony\Component\Cache\Adapter\TagAwareAdapter:
$itemsPool: '@Symfony\Component\Cache\Adapter\FilesystemAdapter'
Psr\Cache\CacheItemPoolInterface: '@Symfony\Component\Cache\Adapter\FilesystemAdapter'
Symfony\Component\Cache\Adapter\TagAwareAdapterInterface: '@Symfony\Component\Cache\Adapter\TagAwareAdapter'

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Rector\Caching\Cache\Adapter;
use Nette\Utils\Strings;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
final class FilesystemAdapterFactory
{
public function create(): FilesystemAdapter
{
return new FilesystemAdapter(
// unique per project
Strings::webalize(getcwd()),
0,
sys_get_temp_dir() . '/_rector_cached_files'
);
}
}

View File

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Rector\Caching;
use Nette\Utils\Strings;
use Rector\Caching\Config\FileHashComputer;
use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface;
use Symplify\SmartFileSystem\SmartFileInfo;
/**
* Inspired by https://github.com/symplify/symplify/pull/90/files#diff-72041b2e1029a08930e13d79d298ef11
*/
final class ChangedFilesDetector
{
/**
* @var string
*/
private const CONFIGURATION_HASH_KEY = 'configuration_hash';
/**
* @var TagAwareAdapterInterface
*/
private $tagAwareAdapter;
/**
* @var FileHashComputer
*/
private $fileHashComputer;
public function __construct(TagAwareAdapterInterface $tagAwareAdapter, FileHashComputer $fileHashComputer)
{
$this->tagAwareAdapter = $tagAwareAdapter;
$this->fileHashComputer = $fileHashComputer;
}
/**
* @param string[] $dependentFiles
*/
public function addFileWithDependencies(SmartFileInfo $smartFileInfo, array $dependentFiles): void
{
$fileInfoCacheKey = $this->getFileInfoCacheKey($smartFileInfo);
$hash = $this->hashFile($smartFileInfo);
$this->saveItemWithValue($fileInfoCacheKey, $hash);
$this->saveItemWithValue($fileInfoCacheKey . '_files', $dependentFiles);
}
public function hasFileChanged(SmartFileInfo $smartFileInfo): bool
{
$currentFileHash = $this->hashFile($smartFileInfo);
$fileInfoCacheKey = $this->getFileInfoCacheKey($smartFileInfo);
$item = $this->tagAwareAdapter->getItem($fileInfoCacheKey);
$oldFileHash = $item->get();
return $currentFileHash !== $oldFileHash;
}
public function invalidateFile(SmartFileInfo $smartFileInfo): void
{
$fileInfoCacheKey = $this->getFileInfoCacheKey($smartFileInfo);
$this->tagAwareAdapter->deleteItem($fileInfoCacheKey);
}
public function clear(): void
{
$this->tagAwareAdapter->clear();
}
/**
* @return SmartFileInfo[]
*/
public function getDependentFileInfos(SmartFileInfo $fileInfo): array
{
$fileInfoCacheKey = $this->getFileInfoCacheKey($fileInfo);
$item = $this->tagAwareAdapter->getItem($fileInfoCacheKey . '_files');
if ($item->get() === null) {
return [];
}
$dependentFileInfos = [];
$dependentFiles = $item->get();
foreach ($dependentFiles as $dependentFile) {
if (! file_exists($dependentFile)) {
continue;
}
$dependentFileInfos[] = new SmartFileInfo($dependentFile);
}
return $dependentFileInfos;
}
/**
* @api
*/
public function setFirstUsedConfig(string $firstConfig): void
{
// the first config is core to all → if it was changed, just invalidate it
$configHash = $this->fileHashComputer->compute($firstConfig);
$this->storeConfigurationDataHash($firstConfig, $configHash);
}
private function hashFile(SmartFileInfo $smartFileInfo): string
{
return hash_file('sha1', $smartFileInfo->getRealPath());
}
private function getFileInfoCacheKey(SmartFileInfo $smartFileInfo): string
{
return sha1($smartFileInfo->getRealPath());
}
private function storeConfigurationDataHash(string $configPath, string $configurationHash): void
{
$key = self::CONFIGURATION_HASH_KEY . '_' . Strings::webalize($configPath);
$this->invalidateCacheIfConfigurationChanged($key, $configurationHash);
$this->saveItemWithValue($key, $configurationHash);
}
private function invalidateCacheIfConfigurationChanged(string $key, string $configurationHash): void
{
$cacheItem = $this->tagAwareAdapter->getItem($key);
$oldConfigurationHash = $cacheItem->get();
if ($configurationHash !== $oldConfigurationHash) {
// should be unique per getcwd()
$this->clear();
}
}
private function saveItemWithValue(string $key, $value): void
{
$item = $this->tagAwareAdapter->getItem($key);
$item->set($value);
$this->tagAwareAdapter->save($item);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Rector\Caching\Config;
use Nette\Utils\Strings;
use Rector\Core\Exception\ShouldNotHappenException;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
/**
* Inspired by https://github.com/symplify/easy-coding-standard/blob/e598ab54686e416788f28fcfe007fd08e0f371d9/packages/changed-files-detector/src/FileHashComputer.php
*/
final class FileHashComputer
{
public function compute(string $filePath): string
{
$this->ensureIsYaml($filePath);
$containerBuilder = new ContainerBuilder();
$yamlFileLoader = new YamlFileLoader($containerBuilder, new FileLocator(dirname($filePath)));
$yamlFileLoader->load($filePath);
return $this->arrayToHash($containerBuilder->getDefinitions()) .
$this->arrayToHash($containerBuilder->getParameterBag()->all());
}
/**
* @param mixed[] $array
*/
private function arrayToHash(array $array): string
{
return md5(serialize($array));
}
private function ensureIsYaml(string $filePath): void
{
if (Strings::match($filePath, '#\.(yml|yaml)$#')) {
return;
}
throw new ShouldNotHappenException(sprintf(
'Provide only yml/yaml file, ready for Symfony DI. "%s" given', $filePath
));
}
}

View File

@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace Rector\Caching\Contract\Rector;
/**
* Rectors implementing this interface require to run with --clear-cache, so full application is analysed.
* Such rules can be remove unused public method, remove unused class etc. They need full app to decide correctly.
*/
interface ZeroCacheRectorInterface
{
}

View File

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Rector\Caching\FileSystem;
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Dependency\DependencyResolver as PHPStanDependencyResolver;
use PHPStan\File\FileHelper;
use Rector\Core\Configuration\Configuration;
final class DependencyResolver
{
/**
* @var FileHelper
*/
private $fileHelper;
/**
* @var Configuration
*/
private $configuration;
/**
* @var PHPStanDependencyResolver
*/
private $phpStanDependencyResolver;
public function __construct(
FileHelper $fileHelper,
Configuration $configuration,
PHPStanDependencyResolver $phpStanDependencyResolver
) {
$this->fileHelper = $fileHelper;
$this->configuration = $configuration;
$this->phpStanDependencyResolver = $phpStanDependencyResolver;
}
/**
* @return string[]
*/
public function resolveDependencies(Node $node, Scope $scope): array
{
$analysedFiles = $this->configuration->getFileInfos();
$analysedFileAbsolutesPaths = [];
foreach ($analysedFiles as $analysedFile) {
$analysedFileAbsolutesPaths[] = $analysedFile->getRealPath();
}
$dependencies = [];
foreach ($this->phpStanDependencyResolver->resolveDependencies($node, $scope) as $dependencyReflection) {
$dependencyFile = $dependencyReflection->getFileName();
if (! $dependencyFile) {
continue;
}
$dependencyFile = $this->fileHelper->normalizePath($dependencyFile);
if ($scope->getFile() === $dependencyFile) {
continue;
}
if (! in_array($dependencyFile, $analysedFileAbsolutesPaths, true)) {
continue;
}
$dependencies[] = $dependencyFile;
}
$dependencies = array_unique($dependencies, SORT_STRING);
return array_values($dependencies);
}
}

View File

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Rector\Caching;
use Symplify\SmartFileSystem\SmartFileInfo;
final class UnchangedFilesFilter
{
/**
* @var ChangedFilesDetector
*/
private $changedFilesDetector;
public function __construct(ChangedFilesDetector $changedFilesDetector)
{
$this->changedFilesDetector = $changedFilesDetector;
}
/**
* @param SmartFileInfo[] $fileInfos
* @return SmartFileInfo[]
*/
public function filterAndJoinWithDependentFileInfos(array $fileInfos): array
{
$changedFileInfos = [];
$dependentFileInfos = [];
foreach ($fileInfos as $fileInfo) {
if (! $this->changedFilesDetector->hasFileChanged($fileInfo)) {
continue;
}
$changedFileInfos[] = $fileInfo;
$this->changedFilesDetector->invalidateFile($fileInfo);
$dependentFileInfos = array_merge(
$dependentFileInfos,
$this->changedFilesDetector->getDependentFileInfos($fileInfo)
);
}
// add dependent files
$dependentFileInfos = array_merge($dependentFileInfos, $changedFileInfos);
return array_unique($dependentFileInfos);
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Rector\Caching\Tests\Config;
use Iterator;
use Rector\Caching\Config\FileHashComputer;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\HttpKernel\RectorKernel;
use Symplify\PackageBuilder\Tests\AbstractKernelTestCase;
final class FileHashComputerTest extends AbstractKernelTestCase
{
/**
* @var FileHashComputer
*/
private $fileHashComputer;
protected function setUp(): void
{
$this->bootKernel(RectorKernel::class);
$this->fileHashComputer = self::$container->get(FileHashComputer::class);
}
/**
* @dataProvider provideDataForIdenticalHash()
*/
public function testHashIsIdentical(string $firstConfig, string $secondConfig): void
{
$configAHash = $this->fileHashComputer->compute($firstConfig);
$configBHash = $this->fileHashComputer->compute($secondConfig);
$this->assertSame($configAHash, $configBHash);
}
public function provideDataForIdenticalHash(): Iterator
{
yield [__DIR__ . '/Source/config_content_a.yaml', __DIR__ . '/Source/config_content_b.yaml'];
yield [__DIR__ . '/Source/Import/import_a.yaml', __DIR__ . '/Source/Import/import_b.yaml'];
}
public function testInvalidType(): void
{
$this->expectException(ShouldNotHappenException::class);
$this->fileHashComputer->compute(__DIR__ . '/Source/file.php');
}
}

View File

@ -0,0 +1,5 @@
imports:
- { resource: "imported_file_a.yaml"}
services:
TwoService: null

View File

@ -0,0 +1,5 @@
imports:
- { resource: "imported_file_b.yaml"}
services:
TwoService: null

View File

@ -0,0 +1,2 @@
services:
OneService: null

View File

@ -0,0 +1,2 @@
services:
OneService: null

View File

@ -0,0 +1,2 @@
services:
OneService: null

View File

@ -0,0 +1,2 @@
services:
OneService: null

View File

@ -0,0 +1,3 @@
<?php
echo 'hi';

View File

@ -119,6 +119,19 @@ final class ErrorAndDiffCollector
return $this->fileDiffs;
}
/**
* @return SmartFileInfo[]
*/
public function getAffectedFileInfos(): array
{
$fileInfos = [];
foreach ($this->fileDiffs as $fileDiff) {
$fileInfos[] = $fileDiff->getFileInfo();
}
return array_unique($fileInfos);
}
public function getFileDiffsCount(): int
{
return count($this->fileDiffs);

View File

@ -384,4 +384,4 @@ class ConstantPreservingAnnotationReader implements \Doctrine\Common\Annotations
$this->imports[$name] = array_merge(self::$globalImports, $this->phpParser->parseClass($class), ['__NAMESPACE__' => $class->getNamespaceName()]);
$this->ignoredAnnotationNames[$name] = $ignoredAnnotationNames;
}
}
}

View File

@ -864,4 +864,4 @@ final class ConstantPreservingDocParser
}
return false;
}
}
}

View File

@ -111,6 +111,7 @@ abstract class AbstractFileSystemRector implements FileSystemRectorInterface
$oldStmts = $this->parser->parseFileInfo($smartFileInfo);
// needed for format preserving
$this->oldStmts = $oldStmts;
return $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($oldStmts, $smartFileInfo);
}

View File

@ -6,3 +6,5 @@ services:
Rector\NodeNestingScope\:
resource: '../src'
exclude:
- '../src/ValueObject/*'

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Rector\NodeNestingScope\NodeFinder;
use PhpParser\Node;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
use Rector\NodeNestingScope\ValueObject\ControlStructure;
final class ScopeAwareNodeFinder
{
/**
* @var BetterNodeFinder
*/
private $betterNodeFinder;
/**
* @var bool
*/
private $isBreakingNodeFoundFirst = false;
public function __construct(BetterNodeFinder $betterNodeFinder)
{
$this->betterNodeFinder = $betterNodeFinder;
}
/**
* Find node based on $callable or null, when the nesting scope is broken
* @param class-string[] $allowedTypes
*/
public function findParent(Node $node, callable $callable, array $allowedTypes): ?Node
{
$foundNode = null;
$parentNestingBreakTypes = array_diff(ControlStructure::NODE_TYPES, $allowedTypes);
$this->isBreakingNodeFoundFirst = false;
$foundNode = $this->betterNodeFinder->findFirstPrevious($node, function (Node $node) use (
$callable,
$parentNestingBreakTypes
) {
if ($callable($node)) {
return true;
}
foreach ($parentNestingBreakTypes as $parentNestingBreakType) {
if (is_a($node, $parentNestingBreakType, true)) {
$this->isBreakingNodeFoundFirst = true;
return true;
}
}
return false;
});
if ($this->isBreakingNodeFoundFirst) {
return null;
}
return $foundNode;
}
}

View File

@ -5,36 +5,12 @@ declare(strict_types=1);
namespace Rector\NodeNestingScope;
use PhpParser\Node;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\While_;
use Rector\Core\PhpParser\Node\BetterNodeFinder;
use Rector\NodeNestingScope\ValueObject\ControlStructure;
final class ScopeNestingComparator
{
/**
* @var class-string[]
*/
private const CONTROL_STRUCTURE_NODES = [
For_::class,
Foreach_::class,
If_::class,
While_::class,
Do_::class,
Else_::class,
ElseIf_::class,
Catch_::class,
Case_::class,
FunctionLike::class,
];
/**
* @var BetterNodeFinder
*/
@ -55,6 +31,6 @@ final class ScopeNestingComparator
private function findParentControlStructure(Node $node): ?Node
{
return $this->betterNodeFinder->findFirstParentInstanceOf($node, self::CONTROL_STRUCTURE_NODES);
return $this->betterNodeFinder->findFirstParentInstanceOf($node, ControlStructure::NODE_TYPES);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Rector\NodeNestingScope\ValueObject;
use PhpParser\Node\FunctionLike;
use PhpParser\Node\Stmt\Case_;
use PhpParser\Node\Stmt\Catch_;
use PhpParser\Node\Stmt\Do_;
use PhpParser\Node\Stmt\Else_;
use PhpParser\Node\Stmt\ElseIf_;
use PhpParser\Node\Stmt\For_;
use PhpParser\Node\Stmt\Foreach_;
use PhpParser\Node\Stmt\If_;
use PhpParser\Node\Stmt\While_;
final class ControlStructure
{
/**
* @var class-string[]
*/
public const NODE_TYPES = [
For_::class,
Foreach_::class,
If_::class,
While_::class,
Do_::class,
Else_::class,
ElseIf_::class,
Catch_::class,
Case_::class,
FunctionLike::class,
];
}

View File

@ -9,10 +9,12 @@ use Nette\Utils\Strings;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\Analyser\ScopeFactory;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Dependency\DependencyResolver;
use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\ContainerFactory;
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
use PHPStan\DependencyInjection\Type\OperatorTypeSpecifyingExtensionRegistryProvider;
use PHPStan\File\FileHelper;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\Reflection\ReflectionProvider;
@ -119,6 +121,22 @@ final class PHPStanServicesFactory
return $this->container->getByType(DynamicReturnTypeExtensionRegistryProvider::class);
}
/**
* @api
*/
public function createDependencyResolver(): DependencyResolver
{
return $this->container->getByType(DependencyResolver::class);
}
/**
* @api
*/
public function createFileHelper(): FileHelper
{
return $this->container->getByType(FileHelper::class);
}
/**
* @api
*/

View File

@ -107,7 +107,7 @@ final class NodeScopeAndMetadataDecorator
* @param Node[] $nodes
* @return Node[]
*/
public function decorateNodesFromFile(array $nodes, SmartFileInfo $fileInfo, bool $needsScope = false): array
public function decorateNodesFromFile(array $nodes, SmartFileInfo $smartFileInfo, bool $needsScope = false): array
{
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor(new NameResolver(null, [
@ -119,15 +119,18 @@ final class NodeScopeAndMetadataDecorator
// node scoping is needed only for Scope
if ($needsScope || $this->configuration->areAnyPhpRectorsLoaded()) {
$nodes = $this->nodeScopeResolver->processNodes($nodes, $fileInfo);
$nodes = $this->nodeScopeResolver->processNodes($nodes, $smartFileInfo);
}
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor(new NameResolver(null, [
$preservingNameResolver = new NameResolver(null, [
'preserveOriginalNames' => true,
// this option would override old non-fqn-namespaced nodes otherwise, so it needs to be disabled
'replaceNodes' => false,
]));
]);
$nodeTraverser->addVisitor($preservingNameResolver);
$nodes = $nodeTraverser->traverse($nodes);
$nodeTraverser = new NodeTraverser();

View File

@ -10,15 +10,19 @@ use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Interface_;
use PhpParser\Node\Stmt\Trait_;
use PhpParser\NodeTraverser;
use PHPStan\AnalysedCodeException;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver as PHPStanNodeScopeResolver;
use PHPStan\Analyser\Scope;
use PHPStan\Node\UnreachableStatementNode;
use PHPStan\Reflection\ReflectionProvider;
use Rector\Caching\ChangedFilesDetector;
use Rector\Caching\FileSystem\DependencyResolver;
use Rector\Core\Configuration\Configuration;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\PHPStan\Collector\TraitNodeScopeCollector;
use Rector\NodeTypeResolver\PHPStan\Scope\NodeVisitor\RemoveDeepChainMethodCallNodeVisitor;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\SmartFileSystem\SmartFileInfo;
/**
@ -52,18 +56,51 @@ final class NodeScopeResolver
*/
private $traitNodeScopeCollector;
/**
* @var DependencyResolver
*/
private $dependencyResolver;
/**
* @var ChangedFilesDetector
*/
private $changedFilesDetector;
/**
* @var Configuration
*/
private $configuration;
/**
* @var SymfonyStyle
*/
private $symfonyStyle;
/**
* @var string[]
*/
private $dependentFiles = [];
public function __construct(
ChangedFilesDetector $changedFilesDetector,
ScopeFactory $scopeFactory,
PHPStanNodeScopeResolver $phpStanNodeScopeResolver,
ReflectionProvider $reflectionProvider,
RemoveDeepChainMethodCallNodeVisitor $removeDeepChainMethodCallNodeVisitor,
TraitNodeScopeCollector $traitNodeScopeCollector
TraitNodeScopeCollector $traitNodeScopeCollector,
DependencyResolver $dependencyResolver,
Configuration $configuration,
SymfonyStyle $symfonyStyle
) {
$this->scopeFactory = $scopeFactory;
$this->phpStanNodeScopeResolver = $phpStanNodeScopeResolver;
$this->reflectionProvider = $reflectionProvider;
$this->removeDeepChainMethodCallNodeVisitor = $removeDeepChainMethodCallNodeVisitor;
$this->traitNodeScopeCollector = $traitNodeScopeCollector;
$this->dependencyResolver = $dependencyResolver;
$this->changedFilesDetector = $changedFilesDetector;
$this->configuration = $configuration;
$this->symfonyStyle = $symfonyStyle;
}
/**
@ -76,6 +113,8 @@ final class NodeScopeResolver
$scope = $this->scopeFactory->createFromFile($smartFileInfo);
$this->dependentFiles = [];
// skip chain method calls, performance issue: https://github.com/phpstan/phpstan/issues/254
$nodeCallback = function (Node $node, MutatingScope $scope): void {
// the class reflection is resolved AFTER entering to class node
@ -100,11 +139,15 @@ final class NodeScopeResolver
} else {
$node->setAttribute(AttributeKey::SCOPE, $scope);
}
$this->resolveDependentFiles($node, $scope);
};
/** @var MutatingScope $scope */
$this->phpStanNodeScopeResolver->processNodes($nodes, $scope, $nodeCallback);
$this->reportCacheDebugAndSaveDependentFiles($smartFileInfo, $this->dependentFiles);
return $nodes;
}
@ -146,4 +189,49 @@ final class NodeScopeResolver
return $classLike->name->toString();
}
private function reportCacheDebug(SmartFileInfo $smartFileInfo, array $dependentFiles): void
{
if (! $this->configuration->isCacheDebug()) {
return;
}
$this->symfonyStyle->note(
sprintf('[debug] %d dependencies for %s file', count($dependentFiles), $smartFileInfo->getRealPath())
);
if ($dependentFiles !== []) {
$this->symfonyStyle->listing($dependentFiles);
}
}
private function resolveDependentFiles(Node $node, MutatingScope $mutatingScope): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
try {
foreach ($this->dependencyResolver->resolveDependencies($node, $mutatingScope) 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);
}
}

View File

@ -2,6 +2,8 @@
namespace Rector\NodeTypeResolver\Tests\PerNodeTypeResolver\PropertyTypeResolver\Source;
use Rector\NodeTypeResolver\Tests\PerNodeTypeResolver\PropertyTypeResolver\Source\SomeChild;
class ActionClass
{
/**

View File

@ -23,30 +23,20 @@ parameters:
- compiler/src
excludes_analyse:
# generated files
- 'packages/doctrine-annotation-generated/src/ConstantPreservingDocParser.php'
- 'packages/doctrine-annotation-generated/src/ConstantPreservingAnnotationReader.php'
- ci/check_services_in_yaml_configs.php
- "*/Expected/*"
# complex printer
- "packages/ContributorTools/src/Command/DumpNodesCommand.php"
- "utils/phpstan/generate-paths.php"
# test files
- '*packages/NodeTypeResolver/tests/Source/AnotherClass.php'
- '*tests/Rector/MethodCall/RenameMethodRector/**/SomeClass.php'
- '*packages/BetterReflection/tests/Reflector/NotLoadedSource/SomeClass.php'
- 'packages/NodeTypeResolver/tests/PerNodeTypeResolver/VariableTypeResolver/Source/NewClass.php'
# tests files
- '*tests/*/Fixture/*'
- '*tests/*/Source/*'
- '*tests/Source/*'
- 'packages/NodeTypeResolver/tests/Source/SomeClass.php'
# intentionally original
- 'packages/Php70/src/EregToPcreTransformer.php'
- '*/packages/ContributorTools/templates/*'
# part of composer
- 'tests/Composer/AutoloadWrongCasesEventSubscriber.php'
- '*/tests/Rector/Psr4/MultipleClassFileToPsr4ClassesRector/Expected/Just*ExceptionWithoutNamespace.php'
# stubs
- 'stubs/*'
@ -159,9 +149,6 @@ parameters:
# test
- '#Class Rector\\DynamicTypeAnalysis\\Tests\\Rector\\ClassMethod\\AddArgumentTypeWithProbeDataRector\\Fixture\\SomeClass not found#'
# known value
- '#Parameter \#1 \$type of method PhpParser\\Builder\\Param\:\:setType\(\) expects PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|string, PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType given#'
-
message: '#Class Rector\\Core\\Tests\\Rector\\StaticCall\\SwapClassMethodArgumentsRector\\Fixture\\SomeClass not found#'
path: tests/Rector/StaticCall/SwapClassMethodArgumentsRector/SwapClassMethodArgumentsRectorTest.php
@ -226,10 +213,7 @@ parameters:
- '#Method Rector\\SOLID\\Reflection\\ParentConstantReflectionResolver\:\:(.*?)\(\) should return ReflectionClassConstant\|null but returns ReflectionClassConstant\|false#'
- '#Parameter \#1 \$firstStmt of method Rector\\Core\\Rector\\MethodBody\\NormalToFluentRector\:\:isBothMethodCallMatch\(\) expects PhpParser\\Node\\Stmt\\Expression, PhpParser\\Node\\Stmt given#'
- '#Method Rector\\Core\\Rector\\AbstractRector\:\:wrapToArg\(\) should return array<PhpParser\\Node\\Arg\> but returns array<PhpParser\\Node\\Arg\|PhpParser\\Node\\Expr\>#'
- '#Property PhpParser\\Node\\Stmt\\ClassMethod\:\:\$returnType \(PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType\|null\) does not accept PhpParser\\Node#'
- '#Parameter \#1 \$possibleSubtype of method Rector\\TypeDeclaration\\PhpParserTypeAnalyzer\:\:isSubtypeOf\(\) expects PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType, PhpParser\\Node given#'
- '#Parameter \#2 \$inferredReturnNode of method Rector\\TypeDeclaration\\Rector\\FunctionLike\\ReturnTypeDeclarationRector\:\:addReturnType\(\) expects PhpParser\\Node, PhpParser\\Node\\Identifier\|PhpParser\\Node\\Name\|PhpParser\\Node\\NullableType\|PhpParser\\Node\\UnionType\|null given#'
- '#Method Rector\\FileSystemRector\\Rector\\AbstractFileSystemRector\:\:wrapToArg\(\) should return array<PhpParser\\Node\\Arg\> but returns array<PhpParser\\Node\\Arg\|PhpParser\\Node\\Expr\>#'
- '#Strict comparison using \=\=\= between mixed and null will always evaluate to false#'
- '#Cannot call method (.*?)\(\) on Rector\\BetterPhpDocParser\\PhpDocInfo\\PhpDocInfo\|null#'
@ -263,3 +247,6 @@ parameters:
- '#Method Rector\\PHPOffice\\Rector\\MethodCall\\IncreaseColumnIndexRector\:\:findVariableAssignName\(\) should return PhpParser\\Node\\Expr\\Assign\|null but returns PhpParser\\Node\|null#'
- '#Parameter \#1 \$keyName of method Rector\\AttributeAwarePhpDoc\\Ast\\Type\\AttributeAwareArrayShapeItemNode\:\:createKeyWithSpacePattern\(\) expects PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprIntegerNode\|PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode\|null, PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprIntegerNode\|PHPStan\\PhpDocParser\\Ast\\ConstExpr\\ConstExprStringNode\|PHPStan\\PhpDocParser\\Ast\\Type\\IdentifierTypeNode\|null given#'
- '#Method Rector\\Caching\\ChangedFilesDetector\:\:hashFile\(\) should return string but returns string\|false#'
- '#If condition is always false#'

View File

@ -2,6 +2,9 @@ imports:
- { resource: "create-rector.yaml", ignore_errors: 'not_found' }
parameters:
# bleeding edge feature
# is_cache_enabled: true
paths:
- src
- tests

View File

@ -13,6 +13,7 @@ use PhpParser\Node\Stmt\Foreach_;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
use Rector\NodeNestingScope\NodeFinder\ScopeAwareNodeFinder;
use Rector\NodeTypeResolver\Node\AttributeKey;
/**
@ -20,6 +21,16 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
*/
final class ForeachItemsAssignToEmptyArrayToAssignRector extends AbstractRector
{
/**
* @var ScopeAwareNodeFinder
*/
private $scopeAwareNodeFinder;
public function __construct(ScopeAwareNodeFinder $scopeAwareNodeFinder)
{
$this->scopeAwareNodeFinder = $scopeAwareNodeFinder;
}
public function getDefinition(): RectorDefinition
{
return new RectorDefinition('Change foreach() items assign to empty array to direct assign', [
@ -85,7 +96,7 @@ PHP
return null;
}
// must be empty array, othewise it will false override
// must be empty array, otherwise it will false override
$defaultValue = $this->getValue($previousDeclarationParentNode->expr);
if ($defaultValue !== []) {
return null;
@ -126,13 +137,13 @@ PHP
private function findPreviousNodeUsage(Node $node, Expr $expr): ?Node
{
return $this->betterNodeFinder->findFirstPrevious($node, function (Node $node) use ($expr) {
return $this->scopeAwareNodeFinder->findParent($node, function (Node $node) use ($expr) {
if ($node === $expr) {
return false;
}
return $this->areNodesEqual($node, $expr);
});
}, [Foreach_::class]);
}
private function shouldSkipAsPartOfNestedForeach(Foreach_ $foreach): bool

View File

@ -0,0 +1,27 @@
<?php
namespace Rector\CodeQuality\Tests\Rector\Foreach_\ForeachItemsAssignToEmptyArrayToAssignRector\Fixture;
class SkipForeachAssign
{
/**
* @var mixed[]
*/
private $dependentFiles = [];
public function processNodes()
{
$this->dependentFiles = [];
}
private function resolveDependentFiles(Node $node, MutatingScope $mutatingScope): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
foreach ($this->dependencyResolver->resolveDependencies($node, $mutatingScope) as $dependentFile) {
$this->dependentFiles[] = $dependentFile;
}
}
}

View File

@ -6,6 +6,7 @@ namespace Rector\DeadCode\Rector\ClassConst;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassConst;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -15,7 +16,7 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
/**
* @see \Rector\DeadCode\Tests\Rector\ClassConst\RemoveUnusedClassConstantRector\RemoveUnusedClassConstantRectorTest
*/
final class RemoveUnusedClassConstantRector extends AbstractRector
final class RemoveUnusedClassConstantRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var ClassConstParsedNodesFinder

View File

@ -9,6 +9,7 @@ use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -21,7 +22,7 @@ use Rector\VendorLocker\NodeVendorLocker\ClassMethodVendorLockResolver;
/**
* @see \Rector\DeadCode\Tests\Rector\ClassMethod\RemoveDeadRecursiveClassMethodRector\RemoveDeadRecursiveClassMethodRectorTest
*/
final class RemoveDeadRecursiveClassMethodRector extends AbstractRector
final class RemoveDeadRecursiveClassMethodRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var MethodCallParsedNodesFinder

View File

@ -8,6 +8,7 @@ use PhpParser\Node;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Core\PhpParser\Node\Manipulator\ClassMethodManipulator;
use Rector\Core\Rector\AbstractRector;
@ -23,7 +24,7 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
* @see \Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\RemoveUnusedParameterRectorTest
* @see \Rector\DeadCode\Tests\Rector\ClassMethod\RemoveUnusedParameterRector\OpenSourceRectorTest
*/
final class RemoveUnusedParameterRector extends AbstractRector
final class RemoveUnusedParameterRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var ClassManipulator

View File

@ -6,6 +6,7 @@ namespace Rector\DeadCode\Rector\Class_;
use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -14,7 +15,7 @@ use Rector\DeadCode\UnusedNodeResolver\UnusedClassResolver;
/**
* @see \Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedClassesRector\RemoveUnusedClassesRectorTest
*/
final class RemoveUnusedClassesRector extends AbstractRector
final class RemoveUnusedClassesRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var UnusedClassResolver

View File

@ -13,6 +13,7 @@ use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Property;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\BetterPhpDocParser\PhpDocNode\Doctrine\Property_\IdTagValueNode;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\PhpParser\Node\Manipulator\ClassManipulator;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
@ -25,7 +26,7 @@ use Rector\NodeTypeResolver\Node\AttributeKey;
* @sponsor Thanks https://spaceflow.io/ for sponsoring this rule - visit them on https://github.com/SpaceFlow-app
* @see \Rector\DeadCode\Tests\Rector\Class_\RemoveUnusedDoctrineEntityMethodAndPropertyRector\RemoveUnusedDoctrineEntityMethodAndPropertyRectorTest
*/
final class RemoveUnusedDoctrineEntityMethodAndPropertyRector extends AbstractRector
final class RemoveUnusedDoctrineEntityMethodAndPropertyRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var Assign[]

View File

@ -6,6 +6,7 @@ namespace Rector\DeadCode\Rector\Function_;
use PhpParser\Node;
use PhpParser\Node\Stmt\Function_;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -14,7 +15,7 @@ use Rector\NodeCollector\NodeCollector\ParsedFunctionLikeNodeCollector;
/**
* @see \Rector\DeadCode\Tests\Rector\Function_\RemoveUnusedFunctionRector\RemoveUnusedFunctionRectorTest
*/
final class RemoveUnusedFunctionRector extends AbstractRector
final class RemoveUnusedFunctionRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var ParsedFunctionLikeNodeCollector

View File

@ -6,6 +6,7 @@ namespace Rector\Privatization\Rector\ClassConst;
use PhpParser\Node;
use PhpParser\Node\Stmt\ClassConst;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -19,7 +20,7 @@ use Rector\SOLID\ValueObject\ConstantVisibility;
/**
* @see \Rector\Privatization\Tests\Rector\ClassConst\PrivatizeLocalClassConstantRector\PrivatizeLocalClassConstantRectorTest
*/
final class PrivatizeLocalClassConstantRector extends AbstractRector
final class PrivatizeLocalClassConstantRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var string

View File

@ -9,6 +9,7 @@ use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPUnit\Framework\TestCase;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\RectorDefinition\CodeSample;
use Rector\Core\RectorDefinition\RectorDefinition;
@ -19,7 +20,7 @@ use Rector\VendorLocker\NodeVendorLocker\ClassMethodVisibilityVendorLockResolver
/**
* @see \Rector\Privatization\Tests\Rector\ClassMethod\PrivatizeLocalOnlyMethodRector\PrivatizeLocalOnlyMethodRectorTest
*/
final class PrivatizeLocalOnlyMethodRector extends AbstractRector
final class PrivatizeLocalOnlyMethodRector extends AbstractRector implements ZeroCacheRectorInterface
{
/**
* @var ClassMethodVisibilityVendorLockResolver

View File

@ -2,7 +2,6 @@
declare(strict_types=1);
namespace Rector\Privatization\Tests\Rector\ClassMethod\PrivatizeLocalOnlyMethodRector\Source;
final class ExternalClass

View File

@ -112,6 +112,7 @@ PHP
private function getClassDirectInterfaces(string $typeName): array
{
$interfaceNames = class_implements($typeName);
foreach ($interfaceNames as $possibleDirectInterfaceName) {
foreach ($interfaceNames as $key => $interfaceName) {
if ($possibleDirectInterfaceName === $interfaceName) {

View File

@ -4,7 +4,17 @@ declare(strict_types=1);
namespace Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Coconut implements Fruit
{
private $id;
public function getId(): \Ramsey\Uuid\UuidInterface
{
return $this->id;
}
}

View File

@ -2,7 +2,7 @@
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class AssignedVariable
{
@ -15,10 +15,10 @@ class CallerClassAssignedVariable
{
public function run()
{
$building = new Apple();
$coconut = new Coconut();
$someClass = new AssignedVariable();
$assignedVariable = $building->getId();
$assignedVariable = $coconut->getId();
$someClass->getById($assignedVariable);
}
}
@ -29,7 +29,7 @@ class CallerClassAssignedVariable
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class AssignedVariable
{
@ -42,10 +42,10 @@ class CallerClassAssignedVariable
{
public function run()
{
$building = new Apple();
$coconut = new Coconut();
$someClass = new AssignedVariable();
$assignedVariable = $building->getId();
$assignedVariable = $coconut->getId();
$someClass->getById($assignedVariable);
}
}

View File

@ -2,7 +2,7 @@
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class SecondPosition
{
@ -15,7 +15,7 @@ class CallerClassForSecondPosition
{
public function run()
{
$building = new Apple();
$building = new Coconut();
$someClass = new SecondPosition();
$someClass->process('hi', $building->getId());
}
@ -27,7 +27,7 @@ class CallerClassForSecondPosition
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class SecondPosition
{
@ -40,7 +40,7 @@ class CallerClassForSecondPosition
{
public function run()
{
$building = new Apple();
$building = new Coconut();
$someClass = new SecondPosition();
$someClass->process('hi', $building->getId());
}

View File

@ -2,7 +2,7 @@
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class TestingStaticCall
{
@ -15,9 +15,8 @@ class CallerClassForStaticCall
{
public function run()
{
$building = new Apple();
TestingStaticCall::process('hi', $building->getId());
$coconut = new Coconut();
TestingStaticCall::process('hi', $coconut->getId());
}
}
@ -27,7 +26,7 @@ class CallerClassForStaticCall
namespace Rector\Doctrine\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Fixture;
use Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source\Apple;
use Rector\SOLID\Tests\Rector\ClassMethod\UseInterfaceOverImplementationInConstructorRector\Source\Coconut;
class TestingStaticCall
{
@ -40,9 +39,8 @@ class CallerClassForStaticCall
{
public function run()
{
$building = new Apple();
TestingStaticCall::process('hi', $building->getId());
$coconut = new Coconut();
TestingStaticCall::process('hi', $coconut->getId());
}
}

View File

@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
namespace Rector\TypeDeclaration\Tests\Rector\ClassMethod\AddMethodCallBasedParamTypeRector\Source;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class Apple
{
private $id;
public function getId(): \Ramsey\Uuid\UuidInterface
{
return $this->id;
}
}

View File

@ -163,7 +163,7 @@ final class RectorApplication
// 4. remove and add files
$this->removedAndAddedFilesProcessor->run();
// 5. extensions on finish
// 5. various extensions on finish
$this->finishingExtensionRunner->run();
}

View File

@ -13,6 +13,7 @@ use Rector\Core\Exception\Rector\RectorNotFoundOrNotValidRectorClassException;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\Testing\PHPUnit\PHPUnitEnvironment;
use Symfony\Component\Console\Input\InputInterface;
use Symplify\SmartFileSystem\SmartFileInfo;
final class Configuration
{
@ -56,19 +57,44 @@ final class Configuration
*/
private $outputFile;
/**
* @var mixed[]
*/
private $source = [];
/**
* @var CiDetector
*/
private $ciDetector;
public function __construct(CiDetector $ciDetector)
/**
* @var SmartFileInfo[]
*/
private $fileInfos = [];
/**
* @var bool
*/
private $shouldClearCache = false;
/**
* @var bool
*/
private $isCacheDebug = false;
/**
* @var bool
*/
private $isCacheEnabled = false;
/**
* @var string[]
*/
private $fileExtensions = [];
/**
* @param string[] $fileExtensions
*/
public function __construct(CiDetector $ciDetector, bool $isCacheEnabled, array $fileExtensions)
{
$this->ciDetector = $ciDetector;
$this->isCacheEnabled = $isCacheEnabled;
$this->fileExtensions = $fileExtensions;
}
/**
@ -77,9 +103,11 @@ final class Configuration
public function resolveFromInput(InputInterface $input): void
{
$this->isDryRun = (bool) $input->getOption(Option::OPTION_DRY_RUN);
$this->shouldClearCache = (bool) $input->getOption(Option::OPTION_CLEAR_CACHE);
$this->hideAutoloadErrors = (bool) $input->getOption(Option::HIDE_AUTOLOAD_ERRORS);
$this->mustMatchGitDiff = (bool) $input->getOption(Option::MATCH_GIT_DIFF);
$this->showProgressBar = $this->canShowProgressBar($input);
$this->isCacheDebug = (bool) $input->getOption(Option::CACHE_DEBUG);
$outputFileOption = $input->getOption(Option::OPTION_OUTPUT_FILE);
$this->outputFile = $outputFileOption ? (string) $outputFileOption : null;
@ -134,6 +162,10 @@ final class Configuration
return false;
}
if ($this->isCacheDebug) {
return false;
}
return $this->showProgressBar;
}
@ -167,19 +199,42 @@ final class Configuration
}
/**
* @param string[] $source
* @param SmartFileInfo[] $fileInfos
*/
public function setSource(array $source): void
public function setFileInfos(array $fileInfos): void
{
$this->source = $source;
$this->fileInfos = $fileInfos;
}
/**
* @return SmartFileInfo[]
*/
public function getFileInfos(): array
{
return $this->fileInfos;
}
public function shouldClearCache(): bool
{
return $this->shouldClearCache;
}
public function isCacheDebug(): bool
{
return $this->isCacheDebug;
}
public function isCacheEnabled(): bool
{
return $this->isCacheEnabled;
}
/**
* @return string[]
*/
public function getSource(): array
public function getFileExtensions(): array
{
return $this->source;
return $this->fileExtensions;
}
private function canShowProgressBar(InputInterface $input): bool

View File

@ -76,6 +76,11 @@ final class Option
*/
public const OPTION_OUTPUT_FILE = 'output-file';
/**
* @var string
*/
public const OPTION_CLEAR_CACHE = 'clear-cache';
/**
* @var string
*/
@ -90,4 +95,9 @@ final class Option
* @var string
*/
public const PROJECT_TYPE_OPEN_SOURCE_UNDESCORED = 'open_source';
/**
* @var string
*/
public const CACHE_DEBUG = 'cache-debug';
}

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Rector\Core\Console\Command;
use Rector\Caching\ChangedFilesDetector;
use Rector\Caching\UnchangedFilesFilter;
use Rector\ChangesReporting\Application\ErrorAndDiffCollector;
use Rector\ChangesReporting\Output\ConsoleOutputFormatter;
use Rector\Core\Application\RectorApplication;
@ -21,16 +23,13 @@ use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\Console\Command\CommandNaming;
use Symplify\PackageBuilder\Console\ShellCode;
use Symplify\SmartFileSystem\SmartFileInfo;
final class ProcessCommand extends AbstractCommand
{
/**
* @var string[]
*/
private $fileExtensions = [];
/**
* @var string[]
*/
@ -91,9 +90,23 @@ final class ProcessCommand extends AbstractCommand
*/
private $yamlProcessor;
/**
* @var UnchangedFilesFilter
*/
private $unchangedFilesFilter;
/**
* @var ChangedFilesDetector
*/
private $changedFilesDetector;
/**
* @var SymfonyStyle
*/
private $symfonyStyle;
/**
* @param string[] $paths
* @param string[] $fileExtensions
*/
public function __construct(
FilesFinder $phpFilesFinder,
@ -107,8 +120,10 @@ final class ProcessCommand extends AbstractCommand
RectorNodeTraverser $rectorNodeTraverser,
StubLoader $stubLoader,
YamlProcessor $yamlProcessor,
array $paths,
array $fileExtensions
ChangedFilesDetector $changedFilesDetector,
UnchangedFilesFilter $unchangedFilesFilter,
SymfonyStyle $symfonyStyle,
array $paths
) {
$this->filesFinder = $phpFilesFinder;
$this->additionalAutoloader = $additionalAutoloader;
@ -116,20 +131,24 @@ final class ProcessCommand extends AbstractCommand
$this->errorAndDiffCollector = $errorAndDiffCollector;
$this->configuration = $configuration;
$this->rectorApplication = $rectorApplication;
$this->fileExtensions = $fileExtensions;
$this->outputFormatterCollector = $outputFormatterCollector;
$this->reportingExtensionRunner = $reportingExtensionRunner;
$this->rectorNodeTraverser = $rectorNodeTraverser;
$this->stubLoader = $stubLoader;
$this->paths = $paths;
$this->yamlProcessor = $yamlProcessor;
$this->unchangedFilesFilter = $unchangedFilesFilter;
parent::__construct();
$this->yamlProcessor = $yamlProcessor;
$this->changedFilesDetector = $changedFilesDetector;
$this->symfonyStyle = $symfonyStyle;
}
protected function configure(): void
{
$this->setName(CommandNaming::classToName(self::class));
$this->setAliases(['rectify']);
$this->setDescription('Upgrade or refactor source code with provided rectors');
$this->addArgument(
Option::SOURCE,
@ -193,6 +212,9 @@ final class ProcessCommand extends AbstractCommand
InputOption::VALUE_REQUIRED,
'Location for file to dump result in. Useful for Docker or automated processes'
);
$this->addOption(Option::CACHE_DEBUG, null, InputOption::VALUE_NONE, 'Debug changed file cache');
$this->addOption(Option::OPTION_CLEAR_CACHE, null, InputOption::VALUE_NONE, 'Clear un-chaged files cache');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@ -205,21 +227,29 @@ final class ProcessCommand extends AbstractCommand
$this->stubLoader->loadStubs();
$source = $this->resolvesSourcePaths($input);
$this->configuration->setSource($source);
$phpFileInfos = $this->filesFinder->findInDirectoriesAndFiles(
$source,
$this->fileExtensions,
$this->configuration->getFileExtensions(),
$this->configuration->mustMatchGitDiff()
);
$this->additionalAutoloader->autoloadWithInputAndSource($input, $source);
// yaml
$this->yamlProcessor->run();
$phpFileInfos = $this->processWithCache($phpFileInfos);
if ($this->configuration->isCacheDebug()) {
$this->symfonyStyle->note(sprintf('[cache] %d files after cache filter', count($phpFileInfos)));
$this->symfonyStyle->listing($phpFileInfos);
}
$this->yamlProcessor->run($source);
$this->configuration->setFileInfos($phpFileInfos);
$this->rectorApplication->runOnFileInfos($phpFileInfos);
$this->reportZeroCacheRectorsCondition();
// report diffs and errors
$outputFormat = (string) $input->getOption(Option::OPTION_OUTPUT_FORMAT);
$outputFormatter = $this->outputFormatterCollector->getByName($outputFormat);
@ -227,6 +257,9 @@ final class ProcessCommand extends AbstractCommand
$this->reportingExtensionRunner->run();
// invalidate affected files
$this->invalidateAffectedCacheFiles();
// some errors were found → fail
if ($this->errorAndDiffCollector->getErrors() !== []) {
return ShellCode::SUCCESS;
@ -255,4 +288,53 @@ final class ProcessCommand extends AbstractCommand
// fallback to config defined paths
return $this->paths;
}
/**
* @param SmartFileInfo[] $phpFileInfos
* @return SmartFileInfo[]
*/
private function processWithCache(array $phpFileInfos): array
{
if (! $this->configuration->isCacheEnabled()) {
return $phpFileInfos;
}
// cache stuff
if ($this->configuration->shouldClearCache()) {
$this->changedFilesDetector->clear();
}
if ($this->configuration->isCacheDebug()) {
$this->symfonyStyle->note(sprintf('[cache] %d files before cache filter', count($phpFileInfos)));
}
return $this->unchangedFilesFilter->filterAndJoinWithDependentFileInfos($phpFileInfos);
}
private function reportZeroCacheRectorsCondition(): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
if (! $this->rectorNodeTraverser->hasZeroCacheRectors()) {
return;
}
$this->symfonyStyle->note(sprintf(
'Ruleset contains %d rules that need "--clear-cache" option to analyse full project',
$this->rectorNodeTraverser->getZeroCacheRectorCount()
));
}
private function invalidateAffectedCacheFiles(): void
{
if (! $this->configuration->isCacheEnabled()) {
return;
}
foreach ($this->errorAndDiffCollector->getAffectedFileInfos() as $affectedFileInfo) {
$this->changedFilesDetector->invalidateFile($affectedFileInfo);
}
}
}

View File

@ -6,6 +6,8 @@ namespace Rector\Core\PhpParser\NodeTraverser;
use PhpParser\Node;
use PhpParser\NodeTraverser;
use Rector\Caching\Contract\Rector\ZeroCacheRectorInterface;
use Rector\Core\Configuration\Configuration;
use Rector\Core\Contract\Rector\PhpRectorInterface;
use Rector\Core\Testing\Application\EnabledRectorsProvider;
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
@ -30,10 +32,18 @@ final class RectorNodeTraverser extends NodeTraverser
/**
* @param PhpRectorInterface[] $phpRectors
*/
public function __construct(EnabledRectorsProvider $enabledRectorsProvider, array $phpRectors = [])
{
public function __construct(
EnabledRectorsProvider $enabledRectorsProvider,
Configuration $configuration,
array $phpRectors = []
) {
$this->allPhpRectors = $phpRectors;
foreach ($phpRectors as $phpRector) {
if ($configuration->isCacheEnabled() && ! $configuration->shouldClearCache() && $phpRector instanceof ZeroCacheRectorInterface) {
continue;
}
$this->addVisitor($phpRector);
}
@ -67,6 +77,20 @@ final class RectorNodeTraverser extends NodeTraverser
return count($this->visitors);
}
public function hasZeroCacheRectors(): bool
{
return (bool) $this->getZeroCacheRectorCount();
}
public function getZeroCacheRectorCount(): int
{
$zeroCacheRectors = array_filter($this->allPhpRectors, function (PhpRectorInterface $phpRector) {
return $phpRector instanceof ZeroCacheRectorInterface;
});
return count($zeroCacheRectors);
}
/**
* Mostly used for testing
*/

View File

@ -59,6 +59,11 @@ final class FileDiff
return $this->smartFileInfo->getRelativeFilePath();
}
public function getFileInfo(): SmartFileInfo
{
return $this->smartFileInfo;
}
/**
* @return RectorWithFileAndLineChange[]
*/

View File

@ -45,13 +45,14 @@ final class YamlProcessor
$this->symfonyStyle = $symfonyStyle;
}
public function run(): void
/**
* @param string[] $source
*/
public function run(array $source): void
{
$source = $this->configuration->getSource();
$yamlFileInfos = $this->filesFinder->findInDirectoriesAndFiles($source, ['yaml']);
// 1. raw class rename
$oldToNewClasses = $this->changeConfiguration->getOldToNewClasses();
if ($oldToNewClasses === []) {
return;

View File

@ -13,6 +13,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\Console\Command\CommandNaming;
use Symplify\PackageBuilder\Console\ShellCode;
use Symplify\SmartFileSystem\SmartFileInfo;
final class SyncAnnotationParserCommand extends Command
{
@ -57,16 +58,32 @@ final class SyncAnnotationParserCommand extends Command
foreach ($this->classSyncers as $classSyncer) {
$isSuccess = $classSyncer->sync($dryRun);
if (! $isSuccess) {
$message = sprintf('File "%s" has changed, regenerate it: ', $classSyncer->getTargetFilePath());
$sourceFileInfo = new SmartFileInfo($classSyncer->getSourceFilePath());
$message = sprintf(
'File "%s" has changed,%sregenerate it: %s',
$sourceFileInfo->getRelativeFilePathFromCwd(),
PHP_EOL,
'bin/rector sync-annotation-parser'
);
$this->symfonyStyle->error($message);
return ShellCode::ERROR;
}
$sourceFileInfo = new SmartFileInfo($classSyncer->getSourceFilePath());
$targetFileInfo = new SmartFileInfo($classSyncer->getTargetFilePath());
if ($dryRun) {
$messageFormat = 'Original "%s" is in sync with "%s"';
} else {
$messageFormat = 'Original "%s" was changed and refactored to "%s"';
}
$message = sprintf(
'Original "%s" was changed and refactored to "%s"',
$classSyncer->getSourceFilePath(),
$classSyncer->getTargetFilePath()
$messageFormat,
$sourceFileInfo->getRelativeFilePathFromCwd(),
$targetFileInfo->getRelativeFilePathFromCwd()
);
$this->symfonyStyle->note($message);

View File

@ -51,6 +51,8 @@ abstract class AbstractClassSyncer implements ClassSyncerInterface
{
$printedContent = $this->printNodesToString($nodes);
$printedContent = rtrim($printedContent) . PHP_EOL;
FileSystem::write($this->getTargetFilePath(), $printedContent);
}
@ -59,9 +61,7 @@ abstract class AbstractClassSyncer implements ClassSyncerInterface
*/
protected function printNodesToString(array $nodes): string
{
$printedContent = $this->betterStandardPrinter->prettyPrint($nodes);
return '<?php' . PHP_EOL . PHP_EOL . $printedContent;
return $this->betterStandardPrinter->prettyPrintFile($nodes) . PHP_EOL;
}
/**