diff --git a/.github/workflows/rector_laravel_rector_dev.yaml b/.github/workflows/rector_laravel_rector_dev.yaml index ad80c419076..2a4a4181fa9 100644 --- a/.github/workflows/rector_laravel_rector_dev.yaml +++ b/.github/workflows/rector_laravel_rector_dev.yaml @@ -28,3 +28,4 @@ jobs: - run: git clone https://github.com/driftingly/rector-laravel.git - run: composer require rector/rector:dev-main --working-dir rector-laravel - run: cd rector-laravel && vendor/bin/phpunit + diff --git a/UPGRADING.md b/UPGRADING.md index 70ed3fd5e3e..21a791c0b00 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,74 @@ +# Upgrading from Rector 2.2.11 to 2.2.12 + +* `FileWithoutNamespace` is deprecated, and replaced by `FileNode` that represents both namespaced and non-namespaced files and allow changes inside +* `beforeTraverse()` is now soft marked as `@final`, use `getNodeTypes()` with `FileNode::class` instead + +**Before** + +```php +use Rector\PhpParser\Node\FileWithoutNamespace; +use Rector\Rector\AbstractRector; + +final class SomeRector extends AbstractRector +{ + public function getNodeTypes(): array + { + return [FileWithoutNamespace::class]; + } + + public function beforeTraverse(array $nodes): array + { + // some node hacking + } + + /** + * @param FileWithoutNamespace $node + */ + public function refactor(Node $node): ?Node + { + // ... + } + +} +``` + +**After** + +```php +use Rector\PhpParser\Node\FileNode; +use Rector\Rector\AbstractRector; + +final class SomeRector extends AbstractRector +{ + public function getNodeTypes(): array + { + return [FileNode::class]; + } + + /** + * @param FileNode $node + */ + public function refactor(Node $node): ?Node + { + foreach ($node->stmts as $stmt) { + // check if has declare_strict already? + // ... + + // create it + $declareStrictTypes = $this->createDeclareStrictTypesNode(); + + // add it + $node->stmts = array_merge([$declareStrictTypes], $node->stmts); + } + + return $node; + } + +} +``` + +
+ # Upgrading from Rector 1.x to 2.0 ## PHP version requirements diff --git a/composer.json b/composer.json index 8fca0284522..454e7d126cf 100644 --- a/composer.json +++ b/composer.json @@ -28,9 +28,9 @@ "react/promise": "^3.3", "react/socket": "^1.17", "rector/extension-installer": "^0.11.2", - "rector/rector-doctrine": "dev-main", - "rector/rector-downgrade-php": "dev-main", - "rector/rector-phpunit": "dev-main", + "rector/rector-doctrine": "dev-tv-file-node", + "rector/rector-downgrade-php": "dev-tv-file-node", + "rector/rector-phpunit": "dev-tv-file-node", "rector/rector-symfony": "dev-main", "sebastian/diff": "^6.0", "symfony/console": "^6.4.24", diff --git a/phpstan.neon b/phpstan.neon index c718ab070bf..5f28e1582eb 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,7 +10,7 @@ parameters: # see https://phpstan.org/writing-php-code/phpdoc-types#global-type-aliases typeAliases: - StmtsAware: \PhpParser\Node\Stmt\Block | \PhpParser\Node\Expr\Closure | \PhpParser\Node\Stmt\Case_ | \PhpParser\Node\Stmt\Catch_ | \PhpParser\Node\Stmt\ClassMethod | \PhpParser\Node\Stmt\Do_ | \PhpParser\Node\Stmt\Else_ | \PhpParser\Node\Stmt\ElseIf_ | \PhpParser\Node\Stmt\Finally_ | \PhpParser\Node\Stmt\For_ | \PhpParser\Node\Stmt\Foreach_ | \PhpParser\Node\Stmt\Function_ | \PhpParser\Node\Stmt\If_ | \PhpParser\Node\Stmt\Namespace_ | \PhpParser\Node\Stmt\TryCatch | \PhpParser\Node\Stmt\While_ | \Rector\PhpParser\Node\CustomNode\FileWithoutNamespace + StmtsAware: \PhpParser\Node\Stmt\Block | \PhpParser\Node\Expr\Closure | \PhpParser\Node\Stmt\Case_ | \PhpParser\Node\Stmt\Catch_ | \PhpParser\Node\Stmt\ClassMethod | \PhpParser\Node\Stmt\Do_ | \PhpParser\Node\Stmt\Else_ | \PhpParser\Node\Stmt\ElseIf_ | \PhpParser\Node\Stmt\Finally_ | \PhpParser\Node\Stmt\For_ | \PhpParser\Node\Stmt\Foreach_ | \PhpParser\Node\Stmt\Function_ | \PhpParser\Node\Stmt\If_ | \PhpParser\Node\Stmt\Namespace_ | \PhpParser\Node\Stmt\TryCatch | \PhpParser\Node\Stmt\While_ | \Rector\PhpParser\Node\FileNode # requires exact closure types checkMissingCallableSignature: true @@ -284,7 +284,6 @@ parameters: - src/Configuration/RectorConfigBuilder.php - src/Reporting/DeprecatedRulesReporter.php identifier: classConstant.deprecatedInterface - - '#Class Rector\\PhpParser\\Node\\CustomNode\\FileWithoutNamespace implements deprecated interface Rector\\Contract\\PhpParser\\Node\\StmtsAwareInterface#' # allowed internally only - @@ -391,12 +390,6 @@ parameters: paths: - rules/Php70/Rector/If_/IfToSpaceshipRector.php - # handles full file - - - paths: - - rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php - identifier: rector.noOnlyNullReturnInRefactor - - identifier: rector.noIntegerRefactorReturn paths: @@ -409,16 +402,6 @@ parameters: # condition check, just to be sure - '#Method Rector\\Rector\\AbstractRector\:\:enterNode\(\) never returns 3 so it can be removed from the return type#' - # special case, working on a file-level - - - identifier: rector.noOnlyNullReturnInRefactor - path: rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php - - # handle next with FileNode - - - identifier: method.parentMethodFinalByPhpDoc - path: rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php - # deprecated - identifier: public.method.unused @@ -432,4 +415,3 @@ parameters: - rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php - rules/Php55/Rector/String_/StringClassNameToClassConstantRector.php - rules/Php81/Enum/AttributeName.php - diff --git a/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc b/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc index 07c14c62fa7..03414a5a4cf 100644 --- a/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc +++ b/rules-tests/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector/Fixture/keep_magic_parent_called_method.php.inc @@ -3,11 +3,12 @@ namespace Rector\Tests\Privatization\Rector\ClassMethod\PrivatizeFinalClassMethodRector\Fixture; use PhpParser\PrettyPrinter\Standard; +use Rector\PhpParser\Node\FileNode; final class KeepMagicParentCalledMethod extends Standard { // called from parent by $this->{'p'} method - protected function pFileNode($fileNode) + protected function pFileNode(FileNode $fileNode) { } } diff --git a/rules/CodingStyle/Application/UseImportsAdder.php b/rules/CodingStyle/Application/UseImportsAdder.php index 7396ac4f327..1e1cc7c6529 100644 --- a/rules/CodingStyle/Application/UseImportsAdder.php +++ b/rules/CodingStyle/Application/UseImportsAdder.php @@ -14,7 +14,7 @@ use Rector\CodingStyle\ClassNameImport\UsedImportsResolver; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\StaticTypeMapper\ValueObject\Type\AliasedObjectType; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; @@ -33,7 +33,7 @@ public function __construct( * @param array $functionUseImportTypes */ public function addImportsToStmts( - FileWithoutNamespace $fileWithoutNamespace, + FileNode $fileNode, array $stmts, array $useImportTypes, array $constantUseImportTypes, @@ -45,10 +45,12 @@ public function addImportsToStmts( $existingFunctionUseImports = $usedImports->getFunctionImports(); $useImportTypes = $this->diffFullyQualifiedObjectTypes($useImportTypes, $existingUseImportTypes); + $constantUseImportTypes = $this->diffFullyQualifiedObjectTypes( $constantUseImportTypes, $existingConstantUseImports ); + $functionUseImportTypes = $this->diffFullyQualifiedObjectTypes( $functionUseImportTypes, $existingFunctionUseImports @@ -90,8 +92,8 @@ public function addImportsToStmts( array_splice($stmts, $key + 1, 0, $nodesToAdd); - $fileWithoutNamespace->stmts = $stmts; - $fileWithoutNamespace->stmts = array_values($fileWithoutNamespace->stmts); + $fileNode->stmts = $stmts; + $fileNode->stmts = array_values($fileNode->stmts); return true; } @@ -99,8 +101,8 @@ public function addImportsToStmts( $this->mirrorUseComments($stmts, $newUses); // make use stmts first - $fileWithoutNamespace->stmts = array_merge($newUses, $this->resolveInsertNop($fileWithoutNamespace), $stmts); - $fileWithoutNamespace->stmts = array_values($fileWithoutNamespace->stmts); + $fileNode->stmts = array_merge($newUses, $this->resolveInsertNop($fileNode), $stmts); + $fileNode->stmts = array_values($fileNode->stmts); return true; } @@ -154,7 +156,7 @@ public function addImportsToNamespace( /** * @return Nop[] */ - private function resolveInsertNop(FileWithoutNamespace|Namespace_ $namespace): array + private function resolveInsertNop(FileNode|Namespace_ $namespace): array { $currentStmt = $namespace->stmts[0] ?? null; if (! $currentStmt instanceof Stmt || $currentStmt instanceof Use_ || $currentStmt instanceof GroupUse) { diff --git a/rules/CodingStyle/Application/UseImportsRemover.php b/rules/CodingStyle/Application/UseImportsRemover.php index d20c6bf1eb4..904cf358e3a 100644 --- a/rules/CodingStyle/Application/UseImportsRemover.php +++ b/rules/CodingStyle/Application/UseImportsRemover.php @@ -6,7 +6,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Renaming\Collector\RenamedNameCollector; final readonly class UseImportsRemover @@ -19,7 +19,7 @@ public function __construct( /** * @param string[] $removedUses */ - public function removeImportsFromStmts(FileWithoutNamespace|Namespace_ $node, array $removedUses): bool + public function removeImportsFromStmts(FileNode|Namespace_ $node, array $removedUses): bool { $hasRemoved = false; foreach ($node->stmts as $key => $stmt) { diff --git a/rules/CodingStyle/ClassNameImport/AliasUsesResolver.php b/rules/CodingStyle/ClassNameImport/AliasUsesResolver.php index 65f5e26ae47..2419c381f58 100644 --- a/rules/CodingStyle/ClassNameImport/AliasUsesResolver.php +++ b/rules/CodingStyle/ClassNameImport/AliasUsesResolver.php @@ -10,7 +10,7 @@ use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; final readonly class AliasUsesResolver { @@ -25,11 +25,11 @@ public function __construct( */ public function resolveFromNode(Node $node, array $stmts): array { - if (! $node instanceof Namespace_ && ! $node instanceof FileWithoutNamespace) { - /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ + if (! $node instanceof Namespace_ && ! $node instanceof FileNode) { + /** @var Namespace_[]|FileNode[] $namespaces */ $namespaces = array_filter( $stmts, - static fn (Stmt $stmt): bool => $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace + static fn (Stmt $stmt): bool => $stmt instanceof Namespace_ || $stmt instanceof FileNode ); if (count($namespaces) !== 1) { return []; diff --git a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php index 2829a5a0c78..7b53fab2602 100644 --- a/rules/CodingStyle/ClassNameImport/ShortNameResolver.php +++ b/rules/CodingStyle/ClassNameImport/ShortNameResolver.php @@ -9,7 +9,6 @@ use PhpParser\Node\Name; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\ClassLike; -use PhpParser\Node\Stmt\Namespace_; use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode; use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode; use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfo; @@ -20,7 +19,6 @@ use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; use Rector\PhpDocParser\PhpDocParser\PhpDocNodeTraverser; use Rector\PhpParser\Node\BetterNodeFinder; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; use Rector\ValueObject\Application\File; /** @@ -65,22 +63,15 @@ public function resolveFromFile(File $file): array */ public function resolveShortClassLikeNames(File $file): array { - $newStmts = $file->getNewStmts(); - - /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ - $namespaces = array_filter( - $newStmts, - static fn (Stmt $stmt): bool => $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace - ); - if (count($namespaces) !== 1) { - // only handle single namespace nodes + $rootNode = $file->getUseImportsRootNode(); + + // nothing to resolve + if (! $rootNode instanceof Node) { return []; } - $namespace = current($namespaces); - /** @var ClassLike[] $classLikes */ - $classLikes = $this->betterNodeFinder->findInstanceOf($namespace->stmts, ClassLike::class); + $classLikes = $this->betterNodeFinder->findInstanceOf($rootNode->stmts, ClassLike::class); $shortClassLikeNames = []; foreach ($classLikes as $classLike) { diff --git a/rules/CodingStyle/ClassNameImport/UseImportsTraverser.php b/rules/CodingStyle/ClassNameImport/UseImportsTraverser.php index 679d8977192..0c12c361003 100644 --- a/rules/CodingStyle/ClassNameImport/UseImportsTraverser.php +++ b/rules/CodingStyle/ClassNameImport/UseImportsTraverser.php @@ -10,7 +10,7 @@ use PhpParser\Node\Stmt\Use_; use PhpParser\Node\UseItem; use Rector\NodeNameResolver\NodeNameResolver; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; final readonly class UseImportsTraverser { @@ -26,7 +26,7 @@ public function __construct( public function traverserStmts(array $stmts, callable $callable): void { foreach ($stmts as $stmt) { - if ($stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace) { + if ($stmt instanceof Namespace_ || $stmt instanceof FileNode) { $this->traverserStmts($stmt->stmts, $callable); continue; } diff --git a/rules/CodingStyle/Node/NameImporter.php b/rules/CodingStyle/Node/NameImporter.php index ed8848c4bcf..404bec35ef3 100644 --- a/rules/CodingStyle/Node/NameImporter.php +++ b/rules/CodingStyle/Node/NameImporter.php @@ -32,6 +32,7 @@ public function __construct( */ public function importName(FullyQualified $fullyQualified, File $file, array $currentUses): ?Name { + if ($this->classNameImportSkipper->shouldSkipName($fullyQualified, $currentUses)) { return null; } diff --git a/rules/CodingStyle/Rector/Catch_/CatchExceptionNameMatchingTypeRector.php b/rules/CodingStyle/Rector/Catch_/CatchExceptionNameMatchingTypeRector.php index 2eeb231a608..3048f8cba55 100644 --- a/rules/CodingStyle/Rector/Catch_/CatchExceptionNameMatchingTypeRector.php +++ b/rules/CodingStyle/Rector/Catch_/CatchExceptionNameMatchingTypeRector.php @@ -21,7 +21,7 @@ use PHPStan\Type\ObjectType; use Rector\Naming\Naming\PropertyNaming; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -73,17 +73,11 @@ public function getRuleDefinition(): RuleDefinition */ public function getNodeTypes(): array { - return [ - ClassMethod::class, - Function_::class, - Closure::class, - FileWithoutNamespace::class, - Namespace_::class, - ]; + return [ClassMethod::class, Function_::class, Closure::class, FileNode::class, Namespace_::class]; } /** - * @param ClassMethod|Function_|Closure|FileWithoutNamespace|Namespace_ $node + * @param ClassMethod|Function_|Closure|FileNode|Namespace_ $node */ public function refactor(Node $node): ?Node { @@ -91,6 +85,11 @@ public function refactor(Node $node): ?Node return null; } + if ($node instanceof FileNode && $node->isNamespaced()) { + // handled in Namespace_ node + return null; + } + $hasChanged = false; foreach ($node->stmts as $key => $stmt) { diff --git a/rules/CodingStyle/Rector/Stmt/RemoveUselessAliasInUseStatementRector.php b/rules/CodingStyle/Rector/Stmt/RemoveUselessAliasInUseStatementRector.php index c81af62584a..fbd2df030d4 100644 --- a/rules/CodingStyle/Rector/Stmt/RemoveUselessAliasInUseStatementRector.php +++ b/rules/CodingStyle/Rector/Stmt/RemoveUselessAliasInUseStatementRector.php @@ -9,7 +9,7 @@ use PhpParser\Node\Identifier; use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -42,15 +42,21 @@ public function getRuleDefinition(): RuleDefinition */ public function getNodeTypes(): array { - return [FileWithoutNamespace::class, Namespace_::class]; + return [FileNode::class, Namespace_::class]; } /** - * @param FileWithoutNamespace|Namespace_ $node + * @param Namespace_|FileNode $node */ - public function refactor(Node $node): null|FileWithoutNamespace|Namespace_ + public function refactor(Node $node): null|FileNode|Namespace_ { + if ($node instanceof FileNode && $node->isNamespaced()) { + // handle in Namespace_ node + return null; + } + $hasChanged = false; + foreach ($node->stmts as $stmt) { if (! $stmt instanceof Use_) { continue; diff --git a/rules/CodingStyle/Rector/Use_/SeparateMultiUseImportsRector.php b/rules/CodingStyle/Rector/Use_/SeparateMultiUseImportsRector.php index 8e381f1833a..06b5d7d3cac 100644 --- a/rules/CodingStyle/Rector/Use_/SeparateMultiUseImportsRector.php +++ b/rules/CodingStyle/Rector/Use_/SeparateMultiUseImportsRector.php @@ -11,7 +11,7 @@ use PhpParser\Node\Stmt\TraitUse; use PhpParser\Node\Stmt\TraitUseAdaptation\Alias; use PhpParser\Node\Stmt\Use_; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -56,14 +56,19 @@ class SomeClass */ public function getNodeTypes(): array { - return [FileWithoutNamespace::class, Namespace_::class, Class_::class]; + return [FileNode::class, Namespace_::class, Class_::class]; } /** - * @param FileWithoutNamespace|Namespace_|Class_ $node + * @param FileNode|Namespace_|Class_ $node */ - public function refactor(Node $node): FileWithoutNamespace|Namespace_|Class_|null + public function refactor(Node $node): FileNode|Namespace_|Class_|null { + if ($node instanceof FileNode && $node->isNamespaced()) { + // handled in Namespace_ + return null; + } + $hasChanged = false; foreach ($node->stmts as $key => $stmt) { if ($stmt instanceof Use_) { diff --git a/rules/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector.php b/rules/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector.php index 54d78d29d42..05d0e944390 100644 --- a/rules/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector.php +++ b/rules/DeadCode/Rector/Stmt/RemoveUnreachableStatementRector.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt; use Rector\NodeAnalyzer\TerminatedNodeAnalyzer; use Rector\PhpParser\Enum\NodeGroup; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; @@ -64,6 +65,11 @@ public function getNodeTypes(): array */ public function refactor(Node $node): ?Node { + if ($node instanceof FileNode && $node->isNamespaced()) { + // handled in Namespace_ node + return null; + } + if ($node->stmts === null) { return null; } diff --git a/rules/Naming/Naming/UseImportsResolver.php b/rules/Naming/Naming/UseImportsResolver.php index 001c22665af..f4fcb24ea7d 100644 --- a/rules/Naming/Naming/UseImportsResolver.php +++ b/rules/Naming/Naming/UseImportsResolver.php @@ -4,13 +4,10 @@ namespace Rector\Naming\Naming; -use PhpParser\Node; -use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\GroupUse; -use PhpParser\Node\Stmt\Namespace_; use PhpParser\Node\Stmt\Use_; use Rector\Application\Provider\CurrentFileProvider; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\ValueObject\Application\File; final readonly class UseImportsResolver @@ -25,15 +22,17 @@ public function __construct( */ public function resolve(): array { - $namespace = $this->resolveNamespace(); - if (! $namespace instanceof Node) { + $file = $this->currentFileProvider->getFile(); + if (! $file instanceof File) { + return []; + } + + $rootNode = $file->getFileNode(); + if (! $rootNode instanceof FileNode) { return []; } - return array_filter( - $namespace->stmts, - static fn (Stmt $stmt): bool => $stmt instanceof Use_ || $stmt instanceof GroupUse - ); + return $rootNode->getUsesAndGroupUses(); } /** @@ -42,12 +41,18 @@ public function resolve(): array */ public function resolveBareUses(): array { - $namespace = $this->resolveNamespace(); - if (! $namespace instanceof Node) { + $file = $this->currentFileProvider->getFile(); + + if (! $file instanceof File) { + return []; + } + + $fileNode = $file->getFileNode(); + if (! $fileNode instanceof FileNode) { return []; } - return array_filter($namespace->stmts, static fn (Stmt $stmt): bool => $stmt instanceof Use_); + return $fileNode->getUses(); } public function resolvePrefix(Use_|GroupUse $use): string @@ -56,32 +61,4 @@ public function resolvePrefix(Use_|GroupUse $use): string ? $use->prefix . '\\' : ''; } - - private function resolveNamespace(): Namespace_|FileWithoutNamespace|null - { - /** @var File|null $file */ - $file = $this->currentFileProvider->getFile(); - if (! $file instanceof File) { - return null; - } - - $newStmts = $file->getNewStmts(); - - if ($newStmts === []) { - return null; - } - - /** @var Namespace_[]|FileWithoutNamespace[] $namespaces */ - $namespaces = array_filter( - $newStmts, - static fn (Stmt $stmt): bool => $stmt instanceof Namespace_ || $stmt instanceof FileWithoutNamespace - ); - - // multiple namespaces is not supported - if (count($namespaces) !== 1) { - return null; - } - - return current($namespaces); - } } diff --git a/rules/Php71/Rector/Assign/AssignArrayToStringRector.php b/rules/Php71/Rector/Assign/AssignArrayToStringRector.php index 532c30c0e55..7e69acbbb2d 100644 --- a/rules/Php71/Rector/Assign/AssignArrayToStringRector.php +++ b/rules/Php71/Rector/Assign/AssignArrayToStringRector.php @@ -19,7 +19,7 @@ use PhpParser\Node\Stmt\Property; use PhpParser\NodeVisitor; use PHPStan\Type\UnionType; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\PhpParser\NodeFinder\PropertyFetchFinder; use Rector\Rector\AbstractRector; use Rector\ValueObject\PhpVersionFeature; @@ -67,7 +67,7 @@ public function getNodeTypes(): array { return [ Namespace_::class, - FileWithoutNamespace::class, + FileNode::class, Class_::class, ClassMethod::class, Function_::class, @@ -76,7 +76,7 @@ public function getNodeTypes(): array } /** - * @param Namespace_|FileWithoutNamespace|Class_|ClassMethod|Function_|Closure $node + * @param Namespace_|FileNode|Class_|ClassMethod|Function_|Closure $node */ public function refactor(Node $node): ?Node { @@ -88,6 +88,11 @@ public function refactor(Node $node): ?Node return null; } + if ($node instanceof FileNode && $node->isNamespaced()) { + // handled in Namespace_ + return null; + } + $hasChanged = false; $this->traverseNodesWithCallable( $node->stmts, @@ -171,7 +176,7 @@ private function hasPropertyDefaultEmptyString(Property $property): bool */ private function findSameNamedVariableAssigns( Variable $variable, - Namespace_|FileWithoutNamespace|ClassMethod|Function_|Closure $node + Namespace_|FileNode|ClassMethod|Function_|Closure $node ): array { if ($node->stmts === null) { return []; @@ -234,7 +239,7 @@ private function isReAssignedAsArray(Assign $assign, string $variableName, Varia private function refactorAssign( Assign $assign, - Namespace_|FileWithoutNamespace|ClassMethod|Function_|Closure $node + Namespace_|FileNode|ClassMethod|Function_|Closure $node ): ?Assign { if (! $assign->var instanceof Variable) { return null; diff --git a/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php b/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php index 2bd4a13b4b5..4b8061775e6 100644 --- a/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php +++ b/rules/Privatization/Rector/ClassMethod/PrivatizeFinalClassMethodRector.php @@ -85,6 +85,7 @@ public function refactor(Node $node): ?Node } $scope = ScopeFetcher::fetch($node); + $classReflection = $scope->getClassReflection(); if (! $classReflection instanceof ClassReflection) { return null; diff --git a/rules/TypeDeclaration/NodeAnalyzer/DeclareStrictTypeFinder.php b/rules/TypeDeclaration/NodeAnalyzer/DeclareStrictTypeFinder.php index 281d70dafd2..2d03733e272 100644 --- a/rules/TypeDeclaration/NodeAnalyzer/DeclareStrictTypeFinder.php +++ b/rules/TypeDeclaration/NodeAnalyzer/DeclareStrictTypeFinder.php @@ -4,22 +4,24 @@ namespace Rector\TypeDeclaration\NodeAnalyzer; -use PhpParser\Node; use PhpParser\Node\Stmt\Declare_; +use Rector\PhpParser\Node\FileNode; final readonly class DeclareStrictTypeFinder { - public function hasDeclareStrictTypes(Node $node): bool + public function hasDeclareStrictTypes(FileNode $fileNode): bool { - // when first node is Declare_, verify if there is strict_types definition already, - // as multiple declare is allowed, with declare(strict_types=1) only allowed on very first node - if (! $node instanceof Declare_) { - return false; - } + // when first fileNode is Declare_, verify if there is strict_types definition already, + // as multiple declare is allowed, with declare(strict_types=1) only allowed on very first fileNode + foreach ($fileNode->stmts as $stmt) { + if (! $stmt instanceof Declare_) { + continue; + } - foreach ($node->declares as $declare) { - if ($declare->key->toString() === 'strict_types') { - return true; + foreach ($stmt->declares as $declare) { + if ($declare->key->toString() === 'strict_types') { + return true; + } } } diff --git a/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php b/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php index aa643275adf..012345a8744 100644 --- a/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php +++ b/rules/TypeDeclaration/PHPStan/ObjectTypeSpecifier.php @@ -88,7 +88,6 @@ public function narrowToFullyQualifiedOrAliasedObjectType( } $currentTemplateTag = $templateTags[$className] ?? null; - if ($currentTemplateTag === null) { // invalid type return $this->resolveNamespacedNonExistingObjectType($namespaceName, $className, $withPreslash); diff --git a/rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php b/rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php index 49b99186763..d221c74aa2e 100644 --- a/rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php +++ b/rules/TypeDeclaration/Rector/StmtsAwareInterface/DeclareStrictTypesRector.php @@ -7,10 +7,8 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Nop; -use Rector\ChangesReporting\ValueObject\RectorWithLineChange; use Rector\Contract\Rector\HTMLAverseRectorInterface; -use Rector\PhpParser\Enum\NodeGroup; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Rector\AbstractRector; use Rector\TypeDeclaration\NodeAnalyzer\DeclareStrictTypeFinder; use Rector\ValueObject\Application\File; @@ -62,33 +60,35 @@ function someFunction(int $number) } /** - * @param Stmt[] $nodes - * @return Stmt[]|null + * @param FileNode $node */ - public function beforeTraverse(array $nodes): ?array + public function refactor(Node $node): ?FileNode { - parent::beforeTraverse($nodes); + // parent::beforeTraverse($nodes); - if ($this->shouldSkipNodes($nodes, $this->file)) { + if ($this->shouldSkipNodes($node->stmts, $this->file)) { return null; } - /** @var Node $rootStmt */ - $rootStmt = current($nodes); - if ($rootStmt instanceof FileWithoutNamespace) { + // /** @var Node $rootStmt */ + // $rootStmt = current(); + if (! $node->isNamespaced()) { return null; } // when first stmt is Declare_, verify if there is strict_types definition already, // as multiple declare is allowed, with declare(strict_types=1) only allowed on very first stmt - if ($this->declareStrictTypeFinder->hasDeclareStrictTypes($rootStmt)) { + if ($this->declareStrictTypeFinder->hasDeclareStrictTypes($node)) { return null; } - $rectorWithLineChange = new RectorWithLineChange(self::class, $rootStmt->getStartLine()); - $this->file->addRectorClassWithLine($rectorWithLineChange); + // $rectorWithLineChange = new RectorWithLineChange(self::class, $rootStmt->getStartLine()); + // $this->file->addRectorClassWithLine($rectorWithLineChange); + + $node->stmts = array_merge([$this->nodeFactory->createDeclaresStrictType(), new Nop()], $node->stmts); - return [$this->nodeFactory->createDeclaresStrictType(), new Nop(), ...$nodes]; + return $node; + // return [$this->nodeFactory->createDeclaresStrictType(), new Nop(), ...$nodes]; } /** @@ -96,17 +96,7 @@ public function beforeTraverse(array $nodes): ?array */ public function getNodeTypes(): array { - return NodeGroup::STMTS_AWARE; - } - - /** - * @param StmtsAware $node - */ - public function refactor(Node $node): null - { - // workaround, as Rector now only hooks to specific nodes, not arrays - // avoid traversing, as we already handled in beforeTraverse() - return null; + return [FileNode::class]; } public function provideMinPhpVersion(): int @@ -123,6 +113,7 @@ private function shouldSkipNodes(array $nodes, File $file): bool return true; } + // shebang files cannot have declare strict types if (str_starts_with($file->getFileContent(), '#!')) { return true; } diff --git a/rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php b/rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php index b6584cf6fa2..95fa9b18b95 100644 --- a/rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php +++ b/rules/TypeDeclaration/Rector/StmtsAwareInterface/IncreaseDeclareStrictTypesRector.php @@ -62,7 +62,7 @@ public function getNodeTypes(): array public function refactor(Node $node): ?Node { throw new ShouldNotHappenException(sprintf( - '"%s" is deprecated as changes strict types randomly on each run.. Use "%s" Rector on specific paths instead.', + '"%s" is deprecated as changes strict types randomly on each run. Use "%s" Rector on specific paths instead.', self::class, DeclareStrictTypesRector::class )); diff --git a/src/Application/FileProcessor.php b/src/Application/FileProcessor.php index 9c8c87b5413..0cd2f51f166 100644 --- a/src/Application/FileProcessor.php +++ b/src/Application/FileProcessor.php @@ -13,6 +13,7 @@ use Rector\Exception\ShouldNotHappenException; use Rector\FileSystem\FilePathHelper; use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator; +use Rector\PhpParser\Node\FileNode; use Rector\PhpParser\NodeTraverser\RectorNodeTraverser; use Rector\PhpParser\Parser\ParserErrors; use Rector\PhpParser\Parser\RectorParser; @@ -57,7 +58,6 @@ public function processFile(File $file, Configuration $configuration): FileProce do { $file->changeHasChanged(false); - // 1. change nodes with Rector Rules $newStmts = $this->rectorNodeTraverser->traverse($file->getNewStmts()); // 2. apply post rectors @@ -140,14 +140,14 @@ private function parseFileAndDecorateNodes(File $file): ?SystemError private function printFile(File $file, Configuration $configuration, string $filePath): void { // only save to string first, no need to print to file when not needed - $newContent = $this->betterStandardPrinter->printFormatPreserving( + $newFileContent = $this->betterStandardPrinter->printFormatPreserving( $file->getNewStmts(), $file->getOldStmts(), $file->getOldTokens() ); // change file content early to make $file->hasChanged() based on new content - $file->changeFileContent($newContent); + $file->changeFileContent($newFileContent); if ($configuration->isDryRun()) { return; } @@ -156,7 +156,7 @@ private function printFile(File $file, Configuration $configuration, string $fil return; } - FileSystem::write($filePath, $newContent, null); + FileSystem::write($filePath, $newFileContent, null); } private function parseFileNodes(File $file, bool $forNewestSupportedVersion = true): void @@ -168,9 +168,14 @@ private function parseFileNodes(File $file, bool $forNewestSupportedVersion = tr ); $oldStmts = $stmtsAndTokens->getStmts(); + + // wrap in FileNode to allow file-level rules + $oldStmts = [new FileNode($oldStmts)]; + $oldTokens = $stmtsAndTokens->getTokens(); $newStmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($file->getFilePath(), $oldStmts); + $file->hydrateStmtsAndTokens($newStmts, $oldStmts, $oldTokens); } } diff --git a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php index 5ba3aab93ff..a37f829f3c1 100644 --- a/src/NodeAnalyzer/TerminatedNodeAnalyzer.php +++ b/src/NodeAnalyzer/TerminatedNodeAnalyzer.php @@ -25,7 +25,7 @@ use PhpParser\Node\Stmt\Return_; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\TryCatch; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; final class TerminatedNodeAnalyzer { @@ -53,7 +53,7 @@ public function isAlwaysTerminated(Node $stmtsAware, Stmt $node, Stmt $currentSt return false; } - if (($stmtsAware instanceof FileWithoutNamespace || $stmtsAware instanceof Namespace_) && ($currentStmt instanceof ClassLike || $currentStmt instanceof Function_)) { + if (($stmtsAware instanceof FileNode || $stmtsAware instanceof Namespace_) && ($currentStmt instanceof ClassLike || $currentStmt instanceof Function_)) { return false; } diff --git a/src/NodeTypeResolver/NodeScopeAndMetadataDecorator.php b/src/NodeTypeResolver/NodeScopeAndMetadataDecorator.php index 77dc42edea5..8231490ada2 100644 --- a/src/NodeTypeResolver/NodeScopeAndMetadataDecorator.php +++ b/src/NodeTypeResolver/NodeScopeAndMetadataDecorator.php @@ -8,7 +8,6 @@ use PhpParser\NodeTraverser; use PhpParser\NodeVisitor\CloningVisitor; use Rector\NodeTypeResolver\PHPStan\Scope\PHPStanNodeScopeResolver; -use Rector\PhpParser\NodeTraverser\FileWithoutNamespaceNodeTraverser; final readonly class NodeScopeAndMetadataDecorator { @@ -17,7 +16,6 @@ public function __construct( CloningVisitor $cloningVisitor, private PHPStanNodeScopeResolver $phpStanNodeScopeResolver, - private FileWithoutNamespaceNodeTraverser $fileWithoutNamespaceNodeTraverser ) { // needed for format preserving printing $this->nodeTraverser = new NodeTraverser($cloningVisitor); @@ -29,9 +27,7 @@ public function __construct( */ public function decorateNodesFromFile(string $filePath, array $stmts): array { - $stmts = $this->fileWithoutNamespaceNodeTraverser->traverse($stmts); $stmts = $this->phpStanNodeScopeResolver->processNodes($stmts, $filePath); - return $this->nodeTraverser->traverse($stmts); } } diff --git a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php index eb629aeeab8..67bf6a55b93 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php +++ b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php @@ -106,7 +106,7 @@ use Rector\NodeAnalyzer\ClassAnalyzer; use Rector\NodeNameResolver\NodeNameResolver; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\Util\Reflection\PrivatesAccessor; use Webmozart\Assert\Assert; @@ -202,7 +202,8 @@ public function processNodes( $node->setAttribute(AttributeKey::SCOPE, $mutatingScope); } - if ($node instanceof FileWithoutNamespace) { + // handle unwrapped stmts + if ($node instanceof FileNode) { $this->nodeScopeResolverProcessNodes($node->stmts, $mutatingScope, $nodeCallback); return; } diff --git a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php index 152070d60e3..dc1006472ee 100644 --- a/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php +++ b/src/NodeTypeResolver/PhpDocNodeVisitor/NameImportingPhpDocNodeVisitor.php @@ -73,7 +73,6 @@ public function enterNode(Node $node): ?Node $staticType = $this->identifierPhpDocTypeMapper->mapIdentifierTypeNode($node, $this->currentPhpParserNode); $staticType = $this->resolveFullyQualified($staticType); - if (! $staticType instanceof FullyQualifiedObjectType) { return null; } diff --git a/src/PhpParser/Enum/NodeGroup.php b/src/PhpParser/Enum/NodeGroup.php index 871e8de9d5c..c3ab3264de8 100644 --- a/src/PhpParser/Enum/NodeGroup.php +++ b/src/PhpParser/Enum/NodeGroup.php @@ -27,7 +27,7 @@ use PhpParser\Node\Stmt\Trait_; use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\While_; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; final class NodeGroup { @@ -55,7 +55,7 @@ final class NodeGroup Namespace_::class, TryCatch::class, While_::class, - FileWithoutNamespace::class, + FileNode::class, ]; /** diff --git a/src/PhpParser/Node/CustomNode/FileWithoutNamespace.php b/src/PhpParser/Node/CustomNode/FileWithoutNamespace.php index d4e68fd6392..00dd425de3a 100644 --- a/src/PhpParser/Node/CustomNode/FileWithoutNamespace.php +++ b/src/PhpParser/Node/CustomNode/FileWithoutNamespace.php @@ -8,6 +8,9 @@ use Rector\Contract\PhpParser\Node\StmtsAwareInterface; /** + * @deprecated Use @see \Rector\PhpParser\Node\FileNode instead + * @api + * * Inspired by https://github.com/phpstan/phpstan-src/commit/ed81c3ad0b9877e6122c79b4afda9d10f3994092 */ final class FileWithoutNamespace extends Stmt implements StmtsAwareInterface diff --git a/src/PhpParser/Node/FileNode.php b/src/PhpParser/Node/FileNode.php new file mode 100644 index 00000000000..8b558ff60fd --- /dev/null +++ b/src/PhpParser/Node/FileNode.php @@ -0,0 +1,99 @@ +getAttributes() : []); + + parent::__construct(); + + } + + /** + * This triggers Printed method with "pFileNode" name + * @see \Rector\PhpParser\Printer\BetterStandardPrinter::pStmt_FileNode() + */ + public function getType(): string + { + return 'Stmt_FileNode'; + } + + /** + * @return array + */ + public function getSubNodeNames(): array + { + return ['stmts']; + } + + public function isNamespaced(): bool + { + foreach ($this->stmts as $stmt) { + if ($stmt instanceof Namespace_) { + return true; + } + } + + return false; + } + + public function getNamespace(): ?Namespace_ + { + /** @var Namespace_[] $namespaces */ + $namespaces = array_filter($this->stmts, static fn (Stmt $stmt): bool => $stmt instanceof Namespace_); + + if (count($namespaces) === 1) { + return current($namespaces); + } + + return null; + } + + /** + * @return array + */ + public function getUsesAndGroupUses(): array + { + $rootNode = $this->getNamespace(); + if (! $rootNode instanceof Namespace_) { + $rootNode = $this; + } + + return array_filter( + $rootNode->stmts, + static fn (Stmt $stmt): bool => $stmt instanceof Use_ || $stmt instanceof GroupUse + ); + } + + /** + * @return Use_[] + */ + public function getUses(): array + { + $rootNode = $this->getNamespace(); + if (! $rootNode instanceof Namespace_) { + $rootNode = $this; + } + + return array_filter($rootNode->stmts, static fn (Stmt $stmt): bool => $stmt instanceof Use_); + } +} diff --git a/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php b/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php index 8d9329bdbe6..874676a5148 100644 --- a/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/AbstractImmutableNodeTraverser.php @@ -101,6 +101,7 @@ protected function traverseNode(Node $node): void $traverseChildren = true; $visitorIndex = -1; $currentNodeVisitors = $this->getVisitorsForNode($subNode); + foreach ($currentNodeVisitors as $visitorIndex => $visitor) { $return = $visitor->enterNode($subNode); if ($return !== null) { @@ -177,6 +178,7 @@ protected function traverseArray(array $nodes): array $traverseChildren = true; $visitorIndex = -1; $currentNodeVisitors = $this->getVisitorsForNode($node); + foreach ($currentNodeVisitors as $visitorIndex => $visitor) { $return = $visitor->enterNode($node); if ($return !== null) { diff --git a/src/PhpParser/NodeTraverser/FileWithoutNamespaceNodeTraverser.php b/src/PhpParser/NodeTraverser/FileWithoutNamespaceNodeTraverser.php deleted file mode 100644 index e6594eda8fb..00000000000 --- a/src/PhpParser/NodeTraverser/FileWithoutNamespaceNodeTraverser.php +++ /dev/null @@ -1,31 +0,0 @@ -resolveNewStmts($stmts); + $newStmts = $this->unwrapFileNode($stmts); + $origStmts = $this->unwrapFileNode($origStmts); $content = parent::printFormatPreserving($newStmts, $origStmts, $origTokens); @@ -82,6 +83,7 @@ public function printFormatPreserving(array $stmts, array $origStmts, array $ori */ public function print(Node | array | null $node): string { + if ($node === null) { $node = []; } @@ -90,6 +92,8 @@ public function print(Node | array | null $node): string $node = [$node]; } + $node = $this->unwrapFileNode($node); + return $this->prettyPrint($node); } @@ -104,14 +108,6 @@ public function prettyPrintFile(array $stmts): string return parent::prettyPrintFile($stmts) . PHP_EOL; } - /** - * @api magic method in parent - */ - public function pFileWithoutNamespace(FileWithoutNamespace $fileWithoutNamespace): string - { - return $this->pStmts($fileWithoutNamespace->stmts); - } - protected function p( Node $node, int $precedence = self::MAX_PRECEDENCE, @@ -151,6 +147,11 @@ protected function p( return $content; } + protected function pStmt_FileNode(FileNode $fileNode): string + { + return $this->pStmts($fileNode->stmts, true); + } + protected function pExpr_ArrowFunction(ArrowFunction $arrowFunction, int $precedence, int $lhsPrecedence): string { if (! $arrowFunction->hasAttribute(AttributeKey::COMMENTS)) { @@ -511,11 +512,10 @@ private function getIndentCharacter(): string * @param Node[] $stmts * @return Node[]|mixed[] */ - private function resolveNewStmts(array $stmts): array + private function unwrapFileNode(array $stmts): array { - $stmts = array_values($stmts); - - if (count($stmts) === 1 && $stmts[0] instanceof FileWithoutNamespace) { + // $stmts = array_values($stmts); + if (count($stmts) === 1 && $stmts[0] instanceof FileNode) { return array_values($stmts[0]->stmts); } diff --git a/src/PostRector/Application/PostFileProcessor.php b/src/PostRector/Application/PostFileProcessor.php index 2a309fc9a6f..670d9acf55e 100644 --- a/src/PostRector/Application/PostFileProcessor.php +++ b/src/PostRector/Application/PostFileProcessor.php @@ -86,7 +86,7 @@ private function shouldSkipPostRector(PostRectorInterface $postRector, string $f } /** - * Load on the fly, to allow test reset with different configuration + * Lazy load, to enable test reset with different configuration * @return PostRectorInterface[] */ private function getPostRectors(): array @@ -124,7 +124,6 @@ private function getPostRectors(): array } $this->postRectors = $postRectors; - return $this->postRectors; } } diff --git a/src/PostRector/Collector/UseNodesToAddCollector.php b/src/PostRector/Collector/UseNodesToAddCollector.php index d9d792a756b..ebc27a8357e 100644 --- a/src/PostRector/Collector/UseNodesToAddCollector.php +++ b/src/PostRector/Collector/UseNodesToAddCollector.php @@ -36,6 +36,7 @@ public function __construct( public function addUseImport(FullyQualifiedObjectType $fullyQualifiedObjectType): void { + // @todo consider using FileNode directly /** @var File $file */ $file = $this->currentFileProvider->getFile(); diff --git a/src/PostRector/Guard/AddUseStatementGuard.php b/src/PostRector/Guard/AddUseStatementGuard.php index 76bb559b093..0247857cd48 100644 --- a/src/PostRector/Guard/AddUseStatementGuard.php +++ b/src/PostRector/Guard/AddUseStatementGuard.php @@ -8,6 +8,7 @@ use PhpParser\Node\Stmt\InlineHTML; use PhpParser\Node\Stmt\Namespace_; use Rector\PhpParser\Node\BetterNodeFinder; +use Rector\PhpParser\Node\FileNode; final class AddUseStatementGuard { @@ -34,6 +35,10 @@ public function shouldTraverse(array $stmts, string $filePath): bool // just loop the first level stmts to locate namespace to improve performance // as namespace is always on first level + if (isset($stmts[0]) && $stmts[0] instanceof FileNode) { + $stmts = $stmts[0]->stmts; + } + foreach ($stmts as $stmt) { if ($stmt instanceof Namespace_) { ++$totalNamespaces; diff --git a/src/PostRector/Rector/ClassRenamingPostRector.php b/src/PostRector/Rector/ClassRenamingPostRector.php index 50f91212e44..32e8251e528 100644 --- a/src/PostRector/Rector/ClassRenamingPostRector.php +++ b/src/PostRector/Rector/ClassRenamingPostRector.php @@ -5,14 +5,11 @@ namespace Rector\PostRector\Rector; use PhpParser\Node; -use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeVisitor; use Rector\CodingStyle\Application\UseImportsRemover; -use Rector\Configuration\Option; -use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Configuration\RenamedClassesDataCollector; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\PostRector\Guard\AddUseStatementGuard; use Rector\Renaming\Collector\RenamedNameCollector; @@ -31,41 +28,37 @@ public function __construct( ) { } - /** - * @param Stmt[] $nodes - * @return Stmt[] - */ - public function beforeTraverse(array $nodes): array + public function enterNode(Node $node): Namespace_|FileNode|int|null { - if (! SimpleParameterProvider::provideBoolParameter(Option::AUTO_IMPORT_NAMES)) { - return $nodes; - } - - foreach ($nodes as $node) { - if ($node instanceof FileWithoutNamespace || $node instanceof Namespace_) { - $removedUses = $this->renamedClassesDataCollector->getOldClasses(); - if ($this->useImportsRemover->removeImportsFromStmts($node, $removedUses)) { - $this->addRectorClassWithLine($node); - } + if ($node instanceof FileNode) { + // handle in Namespace_ node + if ($node->isNamespaced()) { + return null; + } - break; + // handle here + $removedUses = $this->renamedClassesDataCollector->getOldClasses(); + if ($this->useImportsRemover->removeImportsFromStmts($node, $removedUses)) { + $this->addRectorClassWithLine($node); } + + $this->renamedNameCollector->reset(); + + return $node; } - $this->renamedNameCollector->reset(); - return $nodes; - } + if ($node instanceof Namespace_) { + $removedUses = $this->renamedClassesDataCollector->getOldClasses(); + if ($this->useImportsRemover->removeImportsFromStmts($node, $removedUses)) { + $this->addRectorClassWithLine($node); + } - public function enterNode(Node $node): int - { - /** - * We stop the traversal because all the work has already been done in the beforeTraverse() function - * - * Using STOP_TRAVERSAL is usually dangerous as it will stop the processing of all your nodes for all visitors - * but since the PostFileProcessor is using direct new NodeTraverser() and traverse() for only a single - * visitor per execution, using stop traversal here is safe, - * ref https://github.com/rectorphp/rector-src/blob/fc1e742fa4d9861ccdc5933f3b53613b8223438d/src/PostRector/Application/PostFileProcessor.php#L59-L61 - */ + $this->renamedNameCollector->reset(); + + return $node; + } + + // nothing else to handle here, as first 2 nodes we'll hit are handled above return NodeVisitor::STOP_TRAVERSAL; } diff --git a/src/PostRector/Rector/NameImportingPostRector.php b/src/PostRector/Rector/NameImportingPostRector.php index 66ada94a3ba..7c63e54b217 100644 --- a/src/PostRector/Rector/NameImportingPostRector.php +++ b/src/PostRector/Rector/NameImportingPostRector.php @@ -49,6 +49,7 @@ public function enterNode(Node $node): Name|null } $this->addRectorClassWithLine($node); + return $name; } diff --git a/src/PostRector/Rector/UnusedImportRemovingPostRector.php b/src/PostRector/Rector/UnusedImportRemovingPostRector.php index b06f2ad7191..0d933319891 100644 --- a/src/PostRector/Rector/UnusedImportRemovingPostRector.php +++ b/src/PostRector/Rector/UnusedImportRemovingPostRector.php @@ -20,7 +20,7 @@ use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; use Rector\NodeTypeResolver\Node\AttributeKey; use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; final class UnusedImportRemovingPostRector extends AbstractPostRector { @@ -32,7 +32,7 @@ public function __construct( public function enterNode(Node $node): ?Node { - if (! $node instanceof Namespace_ && ! $node instanceof FileWithoutNamespace) { + if (! $node instanceof Namespace_ && ! $node instanceof FileNode) { return null; } @@ -101,11 +101,11 @@ public function enterNode(Node $node): ?Node /** * @return string[] */ - private function findNonUseImportNames(Namespace_|FileWithoutNamespace $namespace): array + private function findNonUseImportNames(Namespace_|FileNode $fileNode): array { $names = []; - $this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace->stmts, static function (Node $node) use ( + $this->simpleCallableNodeTraverser->traverseNodesWithCallable($fileNode->stmts, static function (Node $node) use ( &$names ): int|null|Name { if ($node instanceof Use_) { @@ -136,11 +136,11 @@ private function findNonUseImportNames(Namespace_|FileWithoutNamespace $namespac /** * @return string[] */ - private function findNamesInDocBlocks(Namespace_|FileWithoutNamespace $namespace): array + private function findNamesInDocBlocks(Namespace_|FileNode $rootNode): array { $names = []; - $this->simpleCallableNodeTraverser->traverseNodesWithCallable($namespace, function (Node $node) use ( + $this->simpleCallableNodeTraverser->traverseNodesWithCallable($rootNode, function (Node $node) use ( &$names ) { $comments = $node->getComments(); @@ -180,10 +180,10 @@ private function findNamesInDocBlocks(Namespace_|FileWithoutNamespace $namespace /** * @return string[] */ - private function resolveUsedPhpAndDocNames(Namespace_|FileWithoutNamespace $namespace): array + private function resolveUsedPhpAndDocNames(Namespace_|FileNode $rootNode): array { - $phpNames = $this->findNonUseImportNames($namespace); - $docBlockNames = $this->findNamesInDocBlocks($namespace); + $phpNames = $this->findNonUseImportNames($rootNode); + $docBlockNames = $this->findNamesInDocBlocks($rootNode); $names = [...$phpNames, ...$docBlockNames]; return array_unique($names); diff --git a/src/PostRector/Rector/UseAddingPostRector.php b/src/PostRector/Rector/UseAddingPostRector.php index 2927abba72b..7b9e70784f0 100644 --- a/src/PostRector/Rector/UseAddingPostRector.php +++ b/src/PostRector/Rector/UseAddingPostRector.php @@ -10,7 +10,7 @@ use PhpParser\NodeVisitor; use Rector\CodingStyle\Application\UseImportsAdder; use Rector\NodeTypeResolver\PHPStan\Type\TypeFactory; -use Rector\PhpParser\Node\CustomNode\FileWithoutNamespace; +use Rector\PhpParser\Node\FileNode; use Rector\PostRector\Collector\UseNodesToAddCollector; use Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType; @@ -35,7 +35,7 @@ public function beforeTraverse(array $nodes): array } $rootNode = $this->resolveRootNode($nodes); - if (! $rootNode instanceof FileWithoutNamespace && ! $rootNode instanceof Namespace_) { + if (! $rootNode instanceof FileNode && ! $rootNode instanceof Namespace_) { return $nodes; } @@ -44,6 +44,7 @@ public function beforeTraverse(array $nodes): array $this->getFile() ->getFilePath() ); + $functionUseImportTypes = $this->useNodesToAddCollector->getFunctionImportsByFilePath( $this->getFile() ->getFilePath() @@ -55,7 +56,7 @@ public function beforeTraverse(array $nodes): array /** @var FullyQualifiedObjectType[] $useImportTypes */ $useImportTypes = $this->typeFactory->uniquateTypes($useImportTypes); - $stmts = $rootNode instanceof FileWithoutNamespace ? $rootNode->stmts : $nodes; + $stmts = $rootNode instanceof FileNode ? $rootNode->stmts : $nodes; if ($this->processStmtsWithImportedUses( $stmts, @@ -94,7 +95,7 @@ private function processStmtsWithImportedUses( array $useImportTypes, array $constantUseImportTypes, array $functionUseImportTypes, - FileWithoutNamespace|Namespace_ $namespace + FileNode|Namespace_ $namespace ): bool { // A. has namespace? add under it if ($namespace instanceof Namespace_) { @@ -143,14 +144,23 @@ private function filterOutNonNamespacedNames(array $useImportTypes): array /** * @param Stmt[] $nodes */ - private function resolveRootNode(array $nodes): Namespace_|FileWithoutNamespace|null + private function resolveRootNode(array $nodes): Namespace_|FileNode|null { - foreach ($nodes as $node) { - if ($node instanceof FileWithoutNamespace || $node instanceof Namespace_) { - return $node; + if ($nodes === []) { + return null; + } + + $firstStmt = $nodes[0]; + if (! $firstStmt instanceof FileNode) { + return null; + } + + foreach ($firstStmt->stmts as $stmt) { + if ($stmt instanceof Namespace_) { + return $stmt; } } - return null; + return $firstStmt; } } diff --git a/src/Testing/PHPUnit/AbstractRectorTestCase.php b/src/Testing/PHPUnit/AbstractRectorTestCase.php index 490b0401d37..f0477faa32e 100644 --- a/src/Testing/PHPUnit/AbstractRectorTestCase.php +++ b/src/Testing/PHPUnit/AbstractRectorTestCase.php @@ -149,6 +149,7 @@ protected function doTestFile(string $fixtureFilePath, bool $includeFixtureDirec } $inputFilePath = $this->createInputFilePath($fixtureFilePath); + // to remove later in tearDown() $this->inputFilePath = $inputFilePath; diff --git a/src/Testing/TestingParser/TestingParser.php b/src/Testing/TestingParser/TestingParser.php index 01f7d8d1650..5bace90461a 100644 --- a/src/Testing/TestingParser/TestingParser.php +++ b/src/Testing/TestingParser/TestingParser.php @@ -9,6 +9,7 @@ use Rector\Application\Provider\CurrentFileProvider; use Rector\NodeTypeResolver\NodeScopeAndMetadataDecorator; use Rector\NodeTypeResolver\Reflection\BetterReflection\SourceLocatorProvider\DynamicSourceLocatorProvider; +use Rector\PhpParser\Node\FileNode; use Rector\PhpParser\Parser\RectorParser; use Rector\ValueObject\Application\File; @@ -28,7 +29,6 @@ public function __construct( public function parseFilePathToFile(string $filePath): File { [$file, $stmts] = $this->parseToFileAndStmts($filePath); - return $file; } @@ -38,7 +38,6 @@ public function parseFilePathToFile(string $filePath): File public function parseFileToDecoratedNodes(string $filePath): array { [$file, $stmts] = $this->parseToFileAndStmts($filePath); - return $stmts; } @@ -52,8 +51,10 @@ private function parseToFileAndStmts(string $filePath): array $fileContent = FileSystem::read($filePath); $file = new File($filePath, $fileContent); - $stmts = $this->rectorParser->parseString($fileContent); + + // wrap in FileNode to enable file-level rules + $stmts = [new FileNode($stmts)]; $stmts = $this->nodeScopeAndMetadataDecorator->decorateNodesFromFile($filePath, $stmts); $file->hydrateStmtsAndTokens($stmts, $stmts, []); diff --git a/src/ValueObject/Application/File.php b/src/ValueObject/Application/File.php index 89c163db666..9296b6ddd6e 100644 --- a/src/ValueObject/Application/File.php +++ b/src/ValueObject/Application/File.php @@ -7,10 +7,12 @@ use PhpParser\Node; use PhpParser\Node\Stmt; use PhpParser\Node\Stmt\InlineHTML; +use PhpParser\Node\Stmt\Namespace_; use PhpParser\NodeFinder; use PhpParser\Token; use Rector\ChangesReporting\ValueObject\RectorWithLineChange; use Rector\Exception\ShouldNotHappenException; +use Rector\PhpParser\Node\FileNode; use Rector\ValueObject\Reporting\FileDiff; final class File @@ -151,6 +153,39 @@ public function addRectorClassWithLine(RectorWithLineChange $rectorWithLineChang $this->rectorWithLineChanges[] = $rectorWithLineChange; } + /** + * This node returns top most node, + * that includes use imports + */ + public function getUseImportsRootNode(): Namespace_|FileNode|null + { + if ($this->newStmts === []) { + return null; + } + + $firstStmt = $this->newStmts[0]; + if ($firstStmt instanceof FileNode) { + if (! $firstStmt->isNamespaced()) { + return $firstStmt; + } + + // return sole Namespace, or none + $namespaces = []; + foreach ($firstStmt->stmts as $stmt) { + if ($stmt instanceof Namespace_) { + $namespaces[] = $stmt; + } + } + + if (count($namespaces) === 1) { + return $namespaces[0]; + } + } + + return null; + + } + /** * @return RectorWithLineChange[] */ @@ -170,4 +205,17 @@ public function containsHTML(): bool $this->containsHtml = (bool) $nodeFinder->findFirstInstanceOf($this->oldStmts, InlineHTML::class); return $this->containsHtml; } + + public function getFileNode(): ?FileNode + { + if ($this->newStmts === []) { + return null; + } + + if ($this->newStmts[0] instanceof FileNode) { + return $this->newStmts[0]; + } + + return null; + } } diff --git a/src/functions/node_helper.php b/src/functions/node_helper.php index 1c572018586..0c0b58dba5e 100644 --- a/src/functions/node_helper.php +++ b/src/functions/node_helper.php @@ -6,6 +6,7 @@ use PhpParser\Node; use PhpParser\PrettyPrinter\Standard; use Rector\Console\Style\SymfonyStyleFactory; +use Rector\PhpParser\Node\FileNode; use Rector\Util\NodePrinter; use Symfony\Component\Console\Output\OutputInterface; @@ -18,6 +19,9 @@ function print_node(Node | array $node): void $standard = new Standard(); $nodes = is_array($node) ? $node : [$node]; + if ($nodes[0] instanceof FileNode) { + $nodes = $nodes[0]->stmts; + } foreach ($nodes as $node) { $printedContent = $standard->prettyPrint([$node]); diff --git a/tests/Comments/CommentRemover/CommentRemoverTest.php b/tests/Comments/CommentRemover/CommentRemoverTest.php index 55bb072bbf1..eb5e857bbec 100644 --- a/tests/Comments/CommentRemover/CommentRemoverTest.php +++ b/tests/Comments/CommentRemover/CommentRemoverTest.php @@ -29,6 +29,7 @@ protected function setUp(): void $this->commentRemover = $this->make(CommentRemover::class); $this->testingParser = $this->make(TestingParser::class); + $this->betterStandardPrinter = $this->make(BetterStandardPrinter::class); } @@ -44,11 +45,11 @@ public function test(string $filePath): void $nodesWithoutComments = $this->commentRemover->removeFromNode($nodes); - $fileContent = $this->betterStandardPrinter->print($nodesWithoutComments); - $fileContent = trim($fileContent); + $printedFileContent = $this->betterStandardPrinter->print($nodesWithoutComments); + $printedFileContent = trim($printedFileContent); $expectedContent = trim($expectedOutputContents); - $this->assertSame($fileContent, $expectedContent); + $this->assertSame($printedFileContent, $expectedContent); // original nodes are not touched $originalContent = $this->betterStandardPrinter->print($nodes);