From 05acd4f845773bd335ec6089ce460ed9f7508c02 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 10 Nov 2026 00:19:12 +0100 Subject: [PATCH 1/2] Drop file positions on reprinted nodes copied from another file Fixes rectorphp/rector#9963 --- src/PhpParser/Node/NodeFactory.php | 20 ++++++- ...erited_constructor_from_other_file.php.inc | 27 ++++++++++ .../InheritedConstructorFromOtherFileTest.php | 28 ++++++++++ .../Source/AddConstructorDependencyRector.php | 52 +++++++++++++++++++ .../Source/SomeParentWithDeepConstructor.php | 46 ++++++++++++++++ .../Source/SomeService.php | 9 ++++ .../config/configured_rule.php | 9 ++++ 7 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/Fixture/inherited_constructor_from_other_file.php.inc create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/InheritedConstructorFromOtherFileTest.php create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/Source/AddConstructorDependencyRector.php create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/Source/SomeParentWithDeepConstructor.php create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/Source/SomeService.php create mode 100644 tests/Issues/InheritedConstructorFromOtherFile/config/configured_rule.php diff --git a/src/PhpParser/Node/NodeFactory.php b/src/PhpParser/Node/NodeFactory.php index ed54bdcaa04..46e7f199912 100644 --- a/src/PhpParser/Node/NodeFactory.php +++ b/src/PhpParser/Node/NodeFactory.php @@ -351,11 +351,13 @@ public function createReprintedNode(Node $node): Node { // reset original node, to allow the printer to re-use the node $node->setAttribute(AttributeKey::ORIGINAL_NODE, null); + $this->removeFilePositions($node); $this->simpleCallableNodeTraverser->traverseNodesWithCallable( $node, - static function (Node $subNode): Node { + function (Node $subNode): Node { $subNode->setAttribute(AttributeKey::ORIGINAL_NODE, null); + $this->removeFilePositions($subNode); return $subNode; } ); @@ -363,6 +365,22 @@ static function (Node $subNode): Node { return $node; } + /** + * Positions may come from another file, e.g. a parent class constructor + */ + private function removeFilePositions(Node $node): void + { + $attributes = $node->getAttributes(); + unset( + $attributes['startFilePos'], + $attributes['endFilePos'], + $attributes['startTokenPos'], + $attributes['endTokenPos'] + ); + + $node->setAttributes($attributes); + } + private function createArrayItem(mixed $item, string|int|null $key = null): ArrayItem { $arrayItem = null; diff --git a/tests/Issues/InheritedConstructorFromOtherFile/Fixture/inherited_constructor_from_other_file.php.inc b/tests/Issues/InheritedConstructorFromOtherFile/Fixture/inherited_constructor_from_other_file.php.inc new file mode 100644 index 00000000000..b82ac1f072a --- /dev/null +++ b/tests/Issues/InheritedConstructorFromOtherFile/Fixture/inherited_constructor_from_other_file.php.inc @@ -0,0 +1,27 @@ + +----- + diff --git a/tests/Issues/InheritedConstructorFromOtherFile/InheritedConstructorFromOtherFileTest.php b/tests/Issues/InheritedConstructorFromOtherFile/InheritedConstructorFromOtherFileTest.php new file mode 100644 index 00000000000..aafae2ea5eb --- /dev/null +++ b/tests/Issues/InheritedConstructorFromOtherFile/InheritedConstructorFromOtherFileTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/tests/Issues/InheritedConstructorFromOtherFile/Source/AddConstructorDependencyRector.php b/tests/Issues/InheritedConstructorFromOtherFile/Source/AddConstructorDependencyRector.php new file mode 100644 index 00000000000..ab226a82e07 --- /dev/null +++ b/tests/Issues/InheritedConstructorFromOtherFile/Source/AddConstructorDependencyRector.php @@ -0,0 +1,52 @@ +> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Class_ + { + if ($node->getMethod(MethodName::CONSTRUCT) !== null) { + return null; + } + + $this->classDependencyManipulator->addConstructorDependency( + $node, + new PropertyMetadata('someService', new ObjectType(SomeService::class)) + ); + + return $node; + } +} diff --git a/tests/Issues/InheritedConstructorFromOtherFile/Source/SomeParentWithDeepConstructor.php b/tests/Issues/InheritedConstructorFromOtherFile/Source/SomeParentWithDeepConstructor.php new file mode 100644 index 00000000000..bfa180d31e6 --- /dev/null +++ b/tests/Issues/InheritedConstructorFromOtherFile/Source/SomeParentWithDeepConstructor.php @@ -0,0 +1,46 @@ +withRules([AddConstructorDependencyRector::class]); From 2f6c0e9e7465da36c3a29bc2871f5308cc95be35 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 10 Nov 2026 00:30:47 +0100 Subject: [PATCH 2/2] Give ChangedFilesDetectorPortableCacheTest its own cache directory The shared Cache service keeps the directory it was first built with, while the entry count read the current cache_dir parameter. Depending on test order, the two pointed at different directories. --- .../ChangedFilesDetectorPortableCacheTest.php | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/tests/Caching/Detector/ChangedFilesDetectorPortableCacheTest.php b/tests/Caching/Detector/ChangedFilesDetectorPortableCacheTest.php index dedc766c714..07c321e4f9b 100644 --- a/tests/Caching/Detector/ChangedFilesDetectorPortableCacheTest.php +++ b/tests/Caching/Detector/ChangedFilesDetectorPortableCacheTest.php @@ -6,13 +6,19 @@ use FilesystemIterator; use Nette\Utils\FileSystem; +use Rector\Caching\Cache; +use Rector\Caching\Config\FileHashComputer; use Rector\Caching\Detector\ChangedFilesDetector; +use Rector\Caching\ValueObject\Storage\FileCacheStorage; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; +use Rector\FileSystem\FilePathHelper; use Rector\Testing\PHPUnit\AbstractLazyTestCase; +use Rector\Util\FileHasher; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; use SplFileInfo; +use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; use Webmozart\Assert\Assert; final class ChangedFilesDetectorPortableCacheTest extends AbstractLazyTestCase @@ -51,8 +57,6 @@ protected function setUp(): void { parent::setUp(); - $this->changedFilesDetector = $this->make(ChangedFilesDetector::class); - $workingDirectory = getcwd(); Assert::string($workingDirectory); $this->originalWorkingDirectory = $workingDirectory; @@ -65,6 +69,14 @@ protected function setUp(): void $this->firstCheckoutDirectory = $this->rootDirectory . '/first/project'; $this->secondCheckoutDirectory = $this->rootDirectory . '/second/project'; + // own cache directory, as the shared Cache service keeps whichever directory it was first built with + $this->changedFilesDetector = new ChangedFilesDetector( + $this->make(FileHashComputer::class), + new Cache(new FileCacheStorage($this->getCacheDirectory(), $this->make(SymfonyFilesystem::class))), + $this->make(FileHasher::class), + $this->make(FilePathHelper::class) + ); + $this->createCheckout($this->firstCheckoutDirectory); $this->createCheckout($this->secondCheckoutDirectory); @@ -281,9 +293,14 @@ private function cacheProjectFilesIn(string $directory): void } } + private function getCacheDirectory(): string + { + return $this->rootDirectory . '/cache'; + } + private function countCacheEntries(): int { - $cacheDirectory = SimpleParameterProvider::provideStringParameter(Option::CACHE_DIR); + $cacheDirectory = $this->getCacheDirectory(); if (! is_dir($cacheDirectory)) { return 0; }