mirror of
https://github.com/rectorphp/rector.git
synced 2024-06-25 20:32:35 +00:00
do not override printing files with previous stmts if possible
This commit is contained in:
parent
aa67fe90e0
commit
c0415f9216
|
@ -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: ~
|
||||
|
|
1
ecs.yaml
1
ecs.yaml
|
@ -28,6 +28,7 @@ services:
|
|||
- 'PhpParser\NodeVisitor\NameResolver'
|
||||
- 'PhpParser\Node\*'
|
||||
- 'PhpParser\Comment'
|
||||
- 'PhpParser\Lexer'
|
||||
- 'PhpParser\Comment\Doc'
|
||||
- 'PhpParser\NodeTraverser'
|
||||
- 'Rector\Reporting\FileDiff'
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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#'
|
||||
|
|
|
@ -15,4 +15,4 @@ parameters:
|
|||
php_version_features: '7.1'
|
||||
|
||||
services:
|
||||
# Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
|
||||
Rector\CodingStyle\Rector\Namespace_\ImportFullyQualifiedNamesRector: ~
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,4 +12,5 @@ trait AbstractRectorTrait
|
|||
use NodeCommandersTrait;
|
||||
use NodeFactoryTrait;
|
||||
use VisibilityTrait;
|
||||
use ValueResolverTrait;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
42
src/Rector/ValueResolverTrait.php
Normal file
42
src/Rector/ValueResolverTrait.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user