do not override printing files with previous stmts if possible

This commit is contained in:
Tomas Votruba 2019-05-23 19:32:46 +02:00
parent aa67fe90e0
commit c0415f9216
12 changed files with 198 additions and 75 deletions

View File

@ -23,3 +23,6 @@ services:
# add spaces between class elements
PhpCsFixer\Fixer\ClassNotation\ClassAttributesSeparationFixer: ~
# add declare strict types to start of the file
PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer: ~

View File

@ -28,6 +28,7 @@ services:
- 'PhpParser\NodeVisitor\NameResolver'
- 'PhpParser\Node\*'
- 'PhpParser\Comment'
- 'PhpParser\Lexer'
- 'PhpParser\Comment\Doc'
- 'PhpParser\NodeTraverser'
- 'Rector\Reporting\FileDiff'

View File

@ -254,14 +254,10 @@ CODE_SAMPLE
private function resolveUsedClassNames(Node $searchNode): void
{
/** @var ClassLike[] $classLikeNodes */
$classLikeNodes = $this->betterNodeFinder->findInstanceOf($searchNode, ClassLike::class);
foreach ($classLikeNodes as $classLikeNode) {
if ($classLikeNode->name === null) { // skip anonymous classes
continue;
}
/** @var ClassLike[] $classLikes */
$classLikes = $this->betterNodeFinder->findClassLikes([$searchNode]);
foreach ($classLikes as $classLikeNode) {
$name = $this->getName($classLikeNode->name);
$this->resolvedNodeNames[$name][] = [$classLikeNode->name, $classLikeNode];
}

View File

@ -2,16 +2,20 @@
namespace Rector\FileSystemRector\Rector;
use Nette\Utils\Strings;
use PhpParser\Lexer;
use PhpParser\Node;
use PhpParser\ParserFactory;
use Rector\Application\FileSystem\RemovedAndAddedFilesCollector;
use Rector\Configuration\Configuration;
use Rector\FileSystemRector\Contract\FileSystemRectorInterface;
use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator;
use Rector\PhpParser\Parser\Parser;
use Rector\PhpParser\Printer\BetterStandardPrinter;
use Rector\PhpParser\Printer\FormatPerservingPrinter;
use Rector\Rector\AbstractRectorTrait;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
use TypeError;
abstract class AbstractFileSystemRector implements FileSystemRectorInterface
{
@ -52,23 +56,32 @@ abstract class AbstractFileSystemRector implements FileSystemRectorInterface
*/
private $removedAndAddedFilesCollector;
/**
* @var ParserFactory
*/
private $parserFactory;
/**
* @required
*/
public function setAbstractFileSystemRectorDependencies(
Parser $parser,
ParserFactory $parserFactory,
Lexer $lexer,
FormatPerservingPrinter $formatPerservingPrinter,
NodeScopeAndMetadataDecorator $nodeScopeAndMetadataDecorator,
RemovedAndAddedFilesCollector $removedAndAddedFilesCollector,
Configuration $configuration
Configuration $configuration,
BetterStandardPrinter $betterStandardPrinter
): void {
$this->parser = $parser;
$this->parserFactory = $parserFactory;
$this->lexer = $lexer;
$this->formatPerservingPrinter = $formatPerservingPrinter;
$this->nodeScopeAndMetadataDecorator = $nodeScopeAndMetadataDecorator;
$this->removedAndAddedFilesCollector = $removedAndAddedFilesCollector;
$this->configuration = $configuration;
$this->betterStandardPrinter = $betterStandardPrinter;
}
/**
@ -99,6 +112,46 @@ abstract class AbstractFileSystemRector implements FileSystemRectorInterface
$this->addFile($fileDestination, $fileContent);
}
/**
* @param Node[] $nodes
*/
protected function printNewNodesToFilePath(array $nodes, string $fileDestination): void
{
// 1. if nodes are the same, prefer format preserving printer
try {
$dummyLexer = new Lexer();
$dummyParser = $this->parserFactory->create(ParserFactory::PREFER_PHP7, $dummyLexer);
$dummyParser->parse('<?php ' . $this->print($nodes));
$dummyTokenCount = count($dummyLexer->getTokens());
$modelTokenCount = count($this->lexer->getTokens());
if ($dummyTokenCount > $modelTokenCount) {
// nothing we can do - this would end by "Undefined offset in TokenStream.php on line X" error
$formatPreservingContent = '';
} else {
$formatPreservingContent = $this->formatPerservingPrinter->printToString(
$nodes,
$this->oldStmts,
$this->lexer->getTokens()
);
}
} catch (TypeError $typeError) {
// incompatible tokens, nothing we can do to preserve format
$formatPreservingContent = '';
}
$prettyPrintContent = '<?php' . PHP_EOL . $this->betterStandardPrinter->prettyPrint($nodes);
if ($this->areStringsSameWithoutSpaces($formatPreservingContent, $prettyPrintContent)) {
$fileContent = $formatPreservingContent;
} else {
$fileContent = $prettyPrintContent;
}
$this->addFile($fileDestination, $fileContent);
}
protected function removeFile(SmartFileInfo $smartFileInfo): void
{
$this->removedAndAddedFilesCollector->removeFile($smartFileInfo);
@ -108,4 +161,32 @@ abstract class AbstractFileSystemRector implements FileSystemRectorInterface
{
$this->removedAndAddedFilesCollector->addFileWithContent($filePath, $content);
}
/**
* Also without FQN "\" that are added by basic printer
*/
private function areStringsSameWithoutSpaces(string $firstString, string $secondString): bool
{
// remove all comments
// remove all spaces
$firstString = Strings::replace($firstString, '#\s+#', '');
$secondString = Strings::replace($secondString, '#\s+#', '');
$firstString = $this->removeComments($firstString);
$secondString = $this->removeComments($secondString);
// remove FQN "\" that are added by basic printer
$firstString = Strings::replace($firstString, '#\\\\#', '');
$secondString = Strings::replace($secondString, '#\\\\#', '');
return $firstString === $secondString;
}
private function removeComments(string $string): string
{
$string = Strings::replace($string, '#/\*.*?\*/#s', '');
return Strings::replace($string, '#\n\s*\n#', "\n");
}
}

View File

@ -170,3 +170,6 @@ parameters:
- '#Cannot call method getRealPath\(\) on Symplify\\PackageBuilder\\FileSystem\\SmartFileInfo\|null#'
# cascade irelevant
- '#Parameter (.*?) expects array<PhpParser\\Node\\Stmt\>, array<PhpParser\\Node\> given#'
# known value
- '#Parameter \#1 \$node of method Rector\\Rector\\AbstractRector\:\:getName\(\) expects PhpParser\\Node, PhpParser\\Node\\Identifier\|null given#'

View File

@ -15,4 +15,4 @@ parameters:
php_version_features: '7.1'
services:
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~

View File

@ -7,6 +7,8 @@ use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Expression;
use PhpParser\NodeFinder;
use Rector\NodeTypeResolver\Node\AttributeKey;
@ -112,6 +114,27 @@ final class BetterNodeFinder
return $this->nodeFinder->find($nodes, $filter);
}
/**
* Excludes anonymous classes!
*
* @param Node[] $nodes
* @return ClassLike[]
*/
public function findClassLikes(array $nodes): array
{
return $this->find($nodes, function (Node $node): bool {
if (! $node instanceof ClassLike) {
return false;
}
if ($node instanceof Class_ && $node->isAnonymous()) {
return false;
}
return true;
});
}
/**
* @param Node|Node[] $nodes
*/

View File

@ -13,7 +13,6 @@ use Rector\Application\FileSystem\RemovedAndAddedFilesCollector;
use Rector\Contract\Rector\PhpRectorInterface;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\Php\PhpVersionProvider;
use Rector\PhpParser\Node\Value\ValueResolver;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symplify\PackageBuilder\FileSystem\SmartFileInfo;
@ -36,11 +35,6 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
*/
private $removedAndAddedFilesCollector;
/**
* @var ValueResolver
*/
private $valueResolver;
/**
* @var PhpVersionProvider
*/
@ -58,13 +52,11 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
*/
public function setAbstractRectorDependencies(
SymfonyStyle $symfonyStyle,
ValueResolver $valueResolver,
RemovedAndAddedFilesCollector $removedAndAddedFilesCollector,
PhpVersionProvider $phpVersionProvider,
BuilderFactory $builderFactory
): void {
$this->symfonyStyle = $symfonyStyle;
$this->valueResolver = $valueResolver;
$this->removedAndAddedFilesCollector = $removedAndAddedFilesCollector;
$this->phpVersionProvider = $phpVersionProvider;
$this->builderFactory = $builderFactory;
@ -153,22 +145,6 @@ abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorIn
$this->removedAndAddedFilesCollector->addFileWithContent($filePath, $content);
}
/**
* @return mixed
*/
protected function getValue(Expr $expr)
{
return $this->valueResolver->resolve($expr);
}
/**
* @param mixed $expectedValue
*/
protected function isValue(Expr $expr, $expectedValue): bool
{
return $this->getValue($expr) === $expectedValue;
}
/**
* @param Expr[]|null[] $nodes
* @param mixed[] $expectedValues

View File

@ -12,4 +12,5 @@ trait AbstractRectorTrait
use NodeCommandersTrait;
use NodeFactoryTrait;
use VisibilityTrait;
use ValueResolverTrait;
}

View File

@ -4,6 +4,7 @@ namespace Rector\Rector\Psr4;
use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassLike;
use PhpParser\Node\Stmt\Declare_;
@ -71,7 +72,7 @@ CODE_SAMPLE
return;
}
$shouldDelete = true;
$shouldDelete = false;
/** @var Namespace_[] $namespaceNodes */
$namespaceNodes = $this->betterNodeFinder->findInstanceOf($nodes, Namespace_::class);
@ -79,8 +80,14 @@ CODE_SAMPLE
if (count($namespaceNodes)) {
$shouldDelete = $this->processNamespaceNodes($smartFileInfo, $namespaceNodes, $nodes);
} else {
$declareNode = null;
// process only files with 2 classes and more
$classes = $this->betterNodeFinder->findClassLikes($nodes);
if (count($classes) <= 1) {
return;
}
$declareNode = null;
foreach ($nodes as $node) {
if ($node instanceof Declare_) {
$declareNode = $node;
@ -91,8 +98,9 @@ CODE_SAMPLE
}
$fileDestination = $this->createClassLikeFileDestination($node, $smartFileInfo);
if ($smartFileInfo->getRealPath() === $fileDestination) {
$shouldDelete = false;
if ($smartFileInfo->getRealPath() !== $fileDestination) {
$shouldDelete = true;
}
// has file changed?
@ -103,7 +111,12 @@ CODE_SAMPLE
$nodes = [$node];
}
$this->printNodesToFilePath($nodes, $fileDestination);
// has file changed?
if ($shouldDelete) {
$this->printNewNodesToFilePath($nodes, $fileDestination);
} else {
$this->printNodesToFilePath($nodes, $fileDestination);
}
}
}
@ -117,20 +130,20 @@ CODE_SAMPLE
*/
private function shouldSkip(SmartFileInfo $smartFileInfo, array $nodes): bool
{
/** @var ClassLike[] $classLikeNodes */
$classLikeNodes = $this->betterNodeFinder->findInstanceOf($nodes, ClassLike::class);
/** @var ClassLike[] $classLikes */
$classLikes = $this->betterNodeFinder->findClassLikes($nodes);
$nonAnonymousClassLikeNodes = array_filter($classLikeNodes, function (ClassLike $classLikeNode): ?Identifier {
$nonAnonymousClassLikes = array_filter($classLikes, function (ClassLike $classLikeNode): ?Identifier {
return $classLikeNode->name;
});
// only process file with multiple classes || class with non PSR-4 format
if ($nonAnonymousClassLikeNodes === []) {
if ($nonAnonymousClassLikes === []) {
return true;
}
if (count($nonAnonymousClassLikeNodes) === 1) {
$nonAnonymousClassNode = $nonAnonymousClassLikeNodes[0];
if (count($nonAnonymousClassLikes) === 1) {
$nonAnonymousClassNode = $nonAnonymousClassLikes[0];
if ((string) $nonAnonymousClassNode->name === $smartFileInfo->getFilename()) {
return true;
}
@ -172,11 +185,11 @@ CODE_SAMPLE
/**
* @param Namespace_[] $namespaceNodes
* @param Node\Stmt[] $nodes
* @param Stmt[] $nodes
*/
private function processNamespaceNodes(SmartFileInfo $smartFileInfo, array $namespaceNodes, array $nodes): bool
{
$shouldDelete = true;
$shouldDelete = false;
foreach ($namespaceNodes as $namespaceNode) {
$newStmtsSet = $this->removeAllOtherNamespaces($nodes, $namespaceNode);
@ -186,26 +199,29 @@ CODE_SAMPLE
continue;
}
/** @var ClassLike[] $namespacedClassLikeNodes */
$namespacedClassLikeNodes = $this->betterNodeFinder->findInstanceOf($newStmt->stmts, ClassLike::class);
/** @var ClassLike[] $classLikes */
$classLikes = $this->betterNodeFinder->findClassLikes($nodes);
foreach ($namespacedClassLikeNodes as $classLikeNode) {
if ($classLikeNode instanceof Class_ && $classLikeNode->isAnonymous()) {
continue;
}
if (count($classLikes) <= 1) {
continue;
}
foreach ($classLikes as $classLikeNode) {
$this->removeAllClassLikesFromNamespaceNode($newStmt);
$newStmt->stmts[] = $classLikeNode;
$fileDestination = $this->createClassLikeFileDestination($classLikeNode, $smartFileInfo);
if ($smartFileInfo->getRealPath() === $fileDestination) {
$shouldDelete = false;
if ($smartFileInfo->getRealPath() !== $fileDestination) {
$shouldDelete = true;
}
// has file changed?
$this->printNodesToFilePath($newStmtsSet, $fileDestination);
if ($shouldDelete) {
$this->printNewNodesToFilePath($newStmtsSet, $fileDestination);
} else {
$this->printNodesToFilePath($newStmtsSet, $fileDestination);
}
}
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace Rector\Rector;
use PhpParser\Node\Expr;
use Rector\PhpParser\Node\Value\ValueResolver;
/**
* This could be part of @see AbstractRector, but decopuling to trait
* makes clear what code has 1 purpose.
*/
trait ValueResolverTrait
{
/**
* @var ValueResolver
*/
private $valueResolver;
/**
* @required
*/
public function setValueResolver(ValueResolver $valueResolver): void
{
$this->valueResolver = $valueResolver;
}
/**
* @return mixed
*/
protected function getValue(Expr $expr)
{
return $this->valueResolver->resolve($expr);
}
/**
* @param mixed $expectedValue
*/
protected function isValue(Expr $expr, $expectedValue): bool
{
return $this->getValue($expr) === $expectedValue;
}
}

View File

@ -77,25 +77,6 @@ final class MultipleClassFileToPsr4ClassesRectorTest extends AbstractKernelTestC
__DIR__ . '/Fixture/SecondException.php' => __DIR__ . '/Expected/SecondException.php',
],
];
// source: https://github.com/nette/utils/blob/798f8c1626a8e0e23116d90e588532725cce7d0e/src/Utils/exceptions.php
yield [
__DIR__ . '/Source/nette-exceptions.php',
[
__DIR__ . '/Fixture/ArgumentOutOfRangeException.php' => __DIR__ . '/Expected/ArgumentOutOfRangeException.php',
__DIR__ . '/Fixture/InvalidStateException.php' => __DIR__ . '/Expected/InvalidStateException.php',
__DIR__ . '/Fixture/RegexpException.php' => __DIR__ . '/Expected/RegexpException.php',
__DIR__ . '/Fixture/UnknownImageFileException.php' => __DIR__ . '/Expected/UnknownImageFileException.php',
],
];
// non PSR-4 file with one class
yield [
__DIR__ . '/Source/exception.php',
[
__DIR__ . '/Fixture/JustOneException.php' => __DIR__ . '/Expected/JustOneException.php',
],
];
}
public function provideWithoutNamespace(): Iterator