diff --git a/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php b/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php index eb329d1e3b94..38b68515b5c4 100644 --- a/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php +++ b/packages/FileSystemRector/src/Rector/AbstractFileSystemRector.php @@ -146,6 +146,7 @@ protected function printNewNodesToFilePath(array $nodes, string $fileDestination if ($this->areStringsSameWithoutSpaces($formatPreservingContent, $prettyPrintContent)) { $fileContent = $formatPreservingContent; } else { + $prettyPrintContent = $this->resolveLastEmptyLine($prettyPrintContent); $fileContent = $prettyPrintContent; } @@ -167,26 +168,46 @@ protected function addFile(string $filePath, string $content): void */ private function areStringsSameWithoutSpaces(string $firstString, string $secondString): bool { - // remove all comments + return $this->clearString($firstString) === $this->clearString($secondString); + } - // remove all spaces - $firstString = Strings::replace($firstString, '#\s+#', ''); - $secondString = Strings::replace($secondString, '#\s+#', ''); + private function clearString(string $string): string + { + $string = $this->removeComments($string); - $firstString = $this->removeComments($firstString); - $secondString = $this->removeComments($secondString); + // remove all spaces + $string = Strings::replace($string, '#\s+#', ''); // remove FQN "\" that are added by basic printer - $firstString = Strings::replace($firstString, '#\\\\#', ''); - $secondString = Strings::replace($secondString, '#\\\\#', ''); + $string = Strings::replace($string, '#\\\\#', ''); - return $firstString === $secondString; + // remove trailing commas, as one of them doesn't have to contain them + return Strings::replace($string, '#\,#', ''); } private function removeComments(string $string): string { + // remove comments like this noe + $string = Strings::replace($string, '#\/\/(.*?)\n#', ''); + $string = Strings::replace($string, '#/\*.*?\*/#s', ''); return Strings::replace($string, '#\n\s*\n#', "\n"); } + + /** + * Add empty line in the end, if it is in the original tokens + */ + private function resolveLastEmptyLine(string $prettyPrintContent): string + { + $tokens = $this->lexer->getTokens(); + $lastToken = array_pop($tokens); + if ($lastToken) { + if (Strings::contains($lastToken[1], "\n")) { + $prettyPrintContent = trim($prettyPrintContent) . PHP_EOL; + } + } + + return $prettyPrintContent; + } } diff --git a/src/Rector/Psr4/MultipleClassFileToPsr4ClassesRector.php b/src/Rector/Psr4/MultipleClassFileToPsr4ClassesRector.php index 770db0a6653e..02c48f90d34e 100644 --- a/src/Rector/Psr4/MultipleClassFileToPsr4ClassesRector.php +++ b/src/Rector/Psr4/MultipleClassFileToPsr4ClassesRector.php @@ -3,12 +3,12 @@ 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_; use PhpParser\Node\Stmt\Namespace_; +use Rector\CodingStyle\Naming\ClassNaming; use Rector\FileSystemRector\Rector\AbstractFileSystemRector; use Rector\RectorDefinition\CodeSample; use Rector\RectorDefinition\RectorDefinition; @@ -16,6 +16,16 @@ final class MultipleClassFileToPsr4ClassesRector extends AbstractFileSystemRector { + /** + * @var ClassNaming + */ + private $classNaming; + + public function __construct(ClassNaming $classNaming) + { + $this->classNaming = $classNaming; + } + public function getDefinition(): RectorDefinition { return new RectorDefinition( @@ -67,57 +77,15 @@ final class SecondException extends Exception public function refactor(SmartFileInfo $smartFileInfo): void { $nodes = $this->parseFileInfoToNodes($smartFileInfo); - - if ($this->shouldSkip($smartFileInfo, $nodes)) { - return; - } - - $shouldDelete = false; + $shouldDelete = $this->shouldDeleteFileInfo($smartFileInfo, $nodes); /** @var Namespace_[] $namespaceNodes */ $namespaceNodes = $this->betterNodeFinder->findInstanceOf($nodes, Namespace_::class); if (count($namespaceNodes)) { - $shouldDelete = $this->processNamespaceNodes($smartFileInfo, $namespaceNodes, $nodes); + $this->processNamespaceNodes($smartFileInfo, $namespaceNodes, $nodes, $shouldDelete); } else { - // 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; - } - - if (! $node instanceof Class_ || $node->isAnonymous()) { - continue; - } - - $fileDestination = $this->createClassLikeFileDestination($node, $smartFileInfo); - - if ($smartFileInfo->getRealPath() !== $fileDestination) { - $shouldDelete = true; - } - - // has file changed? - - if ($declareNode) { - $nodes = array_merge([$declareNode], [$node]); - } else { - $nodes = [$node]; - } - - // has file changed? - if ($shouldDelete) { - $this->printNewNodesToFilePath($nodes, $fileDestination); - } else { - $this->printNodesToFilePath($nodes, $fileDestination); - } - } + $this->processNodesWithoutNamespace($nodes, $smartFileInfo, $shouldDelete); } if ($shouldDelete) { @@ -125,33 +93,6 @@ public function refactor(SmartFileInfo $smartFileInfo): void } } - /** - * @param Node[] $nodes - */ - private function shouldSkip(SmartFileInfo $smartFileInfo, array $nodes): bool - { - /** @var ClassLike[] $classLikes */ - $classLikes = $this->betterNodeFinder->findClassLikes($nodes); - - $nonAnonymousClassLikes = array_filter($classLikes, function (ClassLike $classLikeNode): ?Identifier { - return $classLikeNode->name; - }); - - // only process file with multiple classes || class with non PSR-4 format - if ($nonAnonymousClassLikes === []) { - return true; - } - - if (count($nonAnonymousClassLikes) === 1) { - $nonAnonymousClassNode = $nonAnonymousClassLikes[0]; - if ((string) $nonAnonymousClassNode->name === $smartFileInfo->getFilename()) { - return true; - } - } - - return false; - } - /** * @param Node[] $nodes * @return Node[] @@ -187,10 +128,12 @@ private function createClassLikeFileDestination(ClassLike $classLike, SmartFileI * @param Namespace_[] $namespaceNodes * @param Stmt[] $nodes */ - private function processNamespaceNodes(SmartFileInfo $smartFileInfo, array $namespaceNodes, array $nodes): bool - { - $shouldDelete = false; - + private function processNamespaceNodes( + SmartFileInfo $smartFileInfo, + array $namespaceNodes, + array $nodes, + bool $shouldDeleteFile + ): void { foreach ($namespaceNodes as $namespaceNode) { $newStmtsSet = $this->removeAllOtherNamespaces($nodes, $namespaceNode); @@ -201,7 +144,6 @@ private function processNamespaceNodes(SmartFileInfo $smartFileInfo, array $name /** @var ClassLike[] $classLikes */ $classLikes = $this->betterNodeFinder->findClassLikes($nodes); - if (count($classLikes) <= 1) { continue; } @@ -212,12 +154,8 @@ private function processNamespaceNodes(SmartFileInfo $smartFileInfo, array $name $fileDestination = $this->createClassLikeFileDestination($classLikeNode, $smartFileInfo); - if ($smartFileInfo->getRealPath() !== $fileDestination) { - $shouldDelete = true; - } - // has file changed? - if ($shouldDelete) { + if ($shouldDeleteFile) { $this->printNewNodesToFilePath($newStmtsSet, $fileDestination); } else { $this->printNodesToFilePath($newStmtsSet, $fileDestination); @@ -225,7 +163,65 @@ private function processNamespaceNodes(SmartFileInfo $smartFileInfo, array $name } } } + } - return $shouldDelete; + /** + * @param Stmt[] $nodes + */ + private function shouldDeleteFileInfo(SmartFileInfo $smartFileInfo, array $nodes): bool + { + $classLikes = $this->betterNodeFinder->findClassLikes($nodes); + foreach ($classLikes as $classLike) { + $className = $this->getName($classLike); + if ($className === null) { + continue; + } + + $classShortName = $this->classNaming->getShortName($className); + if ($smartFileInfo->getBasenameWithoutSuffix() === $classShortName) { + return false; + } + } + + return true; + } + + /** + * @param Stmt[] $nodes + */ + private function processNodesWithoutNamespace(array $nodes, SmartFileInfo $smartFileInfo, bool $shouldDelete): void + { + // 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; + } + + if (! $node instanceof Class_ || $node->isAnonymous()) { + continue; + } + + $fileDestination = $this->createClassLikeFileDestination($node, $smartFileInfo); + + if ($declareNode) { + $nodes = array_merge([$declareNode], [$node]); + } else { + $nodes = [$node]; + } + + // has file changed? + if ($shouldDelete) { + $this->printNewNodesToFilePath($nodes, $fileDestination); + } else { + $this->printNodesToFilePath($nodes, $fileDestination); + } + } } } diff --git a/tests/Rector/Psr4/MultipleClassFileToPsr4ClassesRector/Expected/JustOneException.php b/tests/Rector/Psr4/MultipleClassFileToPsr4ClassesRector/Expected/JustOneException.php deleted file mode 100644 index cc13e2dd4187..000000000000 --- a/tests/Rector/Psr4/MultipleClassFileToPsr4ClassesRector/Expected/JustOneException.php +++ /dev/null @@ -1,7 +0,0 @@ -assertFileEquals($expectedFormat, $expectedExceptionLocation); } - $this->assertFileNotExists($temporaryFilePath); + if ($shouldDeleteOriginalFile) { + $this->assertFileNotExists($temporaryFilePath); + } + } + + public function provideNetteExceptions(): Iterator + { + // 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', + ], + true, + ]; } public function provideExceptionsData(): Iterator @@ -77,6 +95,7 @@ public function provideExceptionsData(): Iterator __DIR__ . '/Fixture/FirstException.php' => __DIR__ . '/Expected/FirstException.php', __DIR__ . '/Fixture/SecondException.php' => __DIR__ . '/Expected/SecondException.php', ], + true, ]; } @@ -89,6 +108,7 @@ public function provideWithoutNamespace(): Iterator __DIR__ . '/Fixture/JustOneExceptionWithoutNamespace.php' => __DIR__ . '/Expected/JustOneExceptionWithoutNamespace.php', __DIR__ . '/Fixture/JustTwoExceptionWithoutNamespace.php' => __DIR__ . '/Expected/JustTwoExceptionWithoutNamespace.php', ], + true, ]; } @@ -100,6 +120,7 @@ public function provideMissNamed(): Iterator __DIR__ . '/Fixture/Miss.php' => __DIR__ . '/Expected/Miss.php', __DIR__ . '/Fixture/Named.php' => __DIR__ . '/Expected/Named.php', ], + true, ]; } @@ -112,6 +133,7 @@ public function provideClassLike(): Iterator __DIR__ . '/Fixture/MyClass.php' => __DIR__ . '/Expected/MyClass.php', __DIR__ . '/Fixture/MyInterface.php' => __DIR__ . '/Expected/MyInterface.php', ], + true, ]; } @@ -123,6 +145,7 @@ public function provideFileNameMatchingOneClass(): Iterator __DIR__ . '/Fixture/SomeClass.php' => __DIR__ . '/Expected/SomeClass.php', __DIR__ . '/Fixture/SomeClass_Exception.php' => __DIR__ . '/Expected/SomeClass_Exception.php', ], + false, ]; }