diff --git a/composer.json b/composer.json index 9d4612e6b22..b8ed9e3acea 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,6 @@ "symplify/easy-parallel": "^11.1", "symplify/package-builder": "dev-main", "symplify/rule-doc-generator-contracts": "^11.1", - "symplify/skipper": "^11.1", "symplify/smart-file-system": "^11.1", "webmozart/assert": "^1.11" }, diff --git a/config/config.php b/config/config.php index 25bf06cbb56..e1007b2c890 100644 --- a/config/config.php +++ b/config/config.php @@ -50,12 +50,14 @@ use Symplify\PackageBuilder\Console\Style\SymfonyStyleFactory; use Symplify\PackageBuilder\Parameter\ParameterProvider; use Symplify\PackageBuilder\Php\TypeChecker; +use Symplify\PackageBuilder\Reflection\ClassLikeExistenceChecker; use Symplify\PackageBuilder\Reflection\PrivatesAccessor; use Symplify\PackageBuilder\Reflection\PrivatesCaller; use Symplify\PackageBuilder\Yaml\ParametersMerger; use Symplify\SmartFileSystem\FileSystemFilter; use Symplify\SmartFileSystem\FileSystemGuard; use Symplify\SmartFileSystem\Finder\FinderSanitizer; +use Symplify\SmartFileSystem\Normalizer\PathNormalizer; return static function (RectorConfig $rectorConfig): void { // make use of https://github.com/symplify/easy-parallel @@ -242,4 +244,8 @@ $services->set(\PHPStan\PhpDocParser\Lexer\Lexer::class); $services->set(TypeParser::class); $services->set(ConstExprParser::class); + + // skipper + $services->set(ClassLikeExistenceChecker::class); + $services->set(PathNormalizer::class); }; diff --git a/easy-ci.php b/easy-ci.php index 713b98e6e23..1acd8a7dec4 100644 --- a/easy-ci.php +++ b/easy-ci.php @@ -38,6 +38,7 @@ use Rector\ReadWrite\Contract\ParentNodeReadAnalyzerInterface; use Rector\ReadWrite\Contract\ReadNodeAnalyzerInterface; use Rector\Set\Contract\SetListInterface; +use Rector\Skipper\Contract\SkipVoterInterface; use Rector\StaticTypeMapper\Contract\PhpDocParser\PhpDocTypeMapperInterface; use Rector\StaticTypeMapper\Contract\PhpParser\PhpParserNodeMapperInterface; use Rector\Testing\PHPUnit\AbstractTestCase; @@ -50,6 +51,7 @@ return static function (EasyCIConfig $easyCiConfig): void { $easyCiConfig->typesToSkip([ + SkipVoterInterface::class, AttributeDecoratorInterface::class, ArrayItemNode::class, PhpDocNodeDecoratorInterface::class, diff --git a/packages-tests/Skipper/FileSystem/Fixture/in/it/KeepThisFile.txt b/packages-tests/Skipper/FileSystem/Fixture/in/it/KeepThisFile.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages-tests/Skipper/FileSystem/Fixture/path/in/it/KeepThisFile.txt b/packages-tests/Skipper/FileSystem/Fixture/path/in/it/KeepThisFile.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages-tests/Skipper/FileSystem/Fixture/path/with/KeepThisFile.txt b/packages-tests/Skipper/FileSystem/Fixture/path/with/KeepThisFile.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages-tests/Skipper/FileSystem/FnMatchPathNormalizerTest.php b/packages-tests/Skipper/FileSystem/FnMatchPathNormalizerTest.php new file mode 100644 index 00000000000..dbb30339ada --- /dev/null +++ b/packages-tests/Skipper/FileSystem/FnMatchPathNormalizerTest.php @@ -0,0 +1,42 @@ +create(); + + $this->fnMatchPathNormalizer = $container->get(FnMatchPathNormalizer::class); + } + + /** + * @dataProvider providePaths + */ + public function testPaths(string $path, string $expectedNormalizedPath): void + { + $normalizedPath = $this->fnMatchPathNormalizer->normalizeForFnmatch($path); + $this->assertSame($expectedNormalizedPath, $normalizedPath); + } + + public function providePaths(): Iterator + { + yield ['path/with/no/asterisk', 'path/with/no/asterisk']; + yield ['*path/with/asterisk/begin', '*path/with/asterisk/begin*']; + yield ['path/with/asterisk/end*', '*path/with/asterisk/end*']; + yield ['*path/with/asterisk/begin/and/end*', '*path/with/asterisk/begin/and/end*']; + yield [__DIR__ . '/Fixture/path/with/../in/it', __DIR__ . '/Fixture/path/in/it']; + yield [__DIR__ . '/Fixture/path/with/../../in/it', __DIR__ . '/Fixture/in/it']; + } +} diff --git a/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/Fixture/existing_paths.txt b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/Fixture/existing_paths.txt new file mode 100644 index 00000000000..a0cde59caf0 --- /dev/null +++ b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/Fixture/existing_paths.txt @@ -0,0 +1 @@ +you diff --git a/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/SkippedPathsResolverTest.php b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/SkippedPathsResolverTest.php new file mode 100644 index 00000000000..bc666eefc84 --- /dev/null +++ b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/SkippedPathsResolverTest.php @@ -0,0 +1,30 @@ +createFromConfigs([__DIR__ . '/config/config.php']); + + $this->skippedPathsResolver = $containerBuilder->get(SkippedPathsResolver::class); + } + + public function test(): void + { + $skippedPaths = $this->skippedPathsResolver->resolve(); + $this->assertCount(2, $skippedPaths); + + $this->assertSame('*/Mask/*', $skippedPaths[1]); + } +} diff --git a/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/config/config.php b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/config/config.php new file mode 100644 index 00000000000..3d85314205f --- /dev/null +++ b/packages-tests/Skipper/SkipCriteriaResolver/SkippedPathsResolver/config/config.php @@ -0,0 +1,14 @@ +skip([ + // windows slashes + __DIR__ . '\non-existing-path', + __DIR__ . '/../Fixture', + '*\Mask\*', + ]); +}; diff --git a/packages-tests/Skipper/Skipper/Skip/Fixture/AlwaysSkippedPath/some_file.txt b/packages-tests/Skipper/Skipper/Skip/Fixture/AlwaysSkippedPath/some_file.txt new file mode 100644 index 00000000000..2ef267e25bd --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skip/Fixture/AlwaysSkippedPath/some_file.txt @@ -0,0 +1 @@ +some content diff --git a/packages-tests/Skipper/Skipper/Skip/Fixture/PathSkippedWithMask/another_file.txt b/packages-tests/Skipper/Skipper/Skip/Fixture/PathSkippedWithMask/another_file.txt new file mode 100644 index 00000000000..7d73532226e --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skip/Fixture/PathSkippedWithMask/another_file.txt @@ -0,0 +1 @@ +yes, you can diff --git a/packages-tests/Skipper/Skipper/Skip/Fixture/skip.php.inc b/packages-tests/Skipper/Skipper/Skip/Fixture/skip.php.inc new file mode 100644 index 00000000000..e0c851ce087 --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skip/Fixture/skip.php.inc @@ -0,0 +1,17 @@ +createFromConfigs([__DIR__ . '/config/config.php']); + + $this->skipper = $containerBuilder->get(Skipper::class); + } + + /** + * @dataProvider provideCheckerAndFile() + * @dataProvider provideAnythingAndFilePath() + */ + public function test(string $element, string $filePath, bool $expectedSkip): void + { + $resolvedSkip = $this->skipper->shouldSkipElementAndFileInfo($element, $filePath); + $this->assertSame($expectedSkip, $resolvedSkip); + } + + /** + * @return Iterator[]|class-string[]|class-string[]> + */ + public function provideCheckerAndFile(): Iterator + { + yield [SomeClassToSkip::class, __DIR__ . '/Fixture', true]; + + yield [AnotherClassToSkip::class, __DIR__ . '/Fixture/someFile', true]; + yield [AnotherClassToSkip::class, __DIR__ . '/Fixture/someDirectory/anotherFile.php', true]; + yield [AnotherClassToSkip::class, __DIR__ . '/Fixture/someDirectory/anotherFile.php', true]; + + yield [NotSkippedClass::class, __DIR__ . '/Fixture/someFile', false]; + yield [NotSkippedClass::class, __DIR__ . '/Fixture/someOtherFile', false]; + } + + /** + * @return Iterator + */ + public function provideAnythingAndFilePath(): Iterator + { + yield ['anything', __DIR__ . '/Fixture/AlwaysSkippedPath/some_file.txt', true]; + yield ['anything', __DIR__ . '/Fixture/PathSkippedWithMask/another_file.txt', true]; + } +} diff --git a/packages-tests/Skipper/Skipper/Skip/Source/AnotherClassToSkip.php b/packages-tests/Skipper/Skipper/Skip/Source/AnotherClassToSkip.php new file mode 100644 index 00000000000..89ee9a709de --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skip/Source/AnotherClassToSkip.php @@ -0,0 +1,9 @@ +skip([ + // classes + SomeClassToSkip::class, + + // classes only in specific paths + AnotherClassToSkip::class => ['Fixture/someFile', '*/someDirectory/*'], + + // file paths + __DIR__ . '/../Fixture/AlwaysSkippedPath', + '*\PathSkippedWithMask\*', + ]); +}; diff --git a/packages-tests/Skipper/Skipper/Skipper/Fixture/Element/FifthElement.php b/packages-tests/Skipper/Skipper/Skipper/Fixture/Element/FifthElement.php new file mode 100644 index 00000000000..985bd12c2c4 --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skipper/Fixture/Element/FifthElement.php @@ -0,0 +1,10 @@ +createFromConfigs([__DIR__ . '/config/config.php']); + + $this->skipper = $containerBuilder->get(Skipper::class); + } + + /** + * @dataProvider provideDataShouldSkipFileInfo() + */ + public function testSkipFileInfo(string $filePath, bool $expectedSkip): void + { + $smartFileInfo = new SmartFileInfo($filePath); + + $resultSkip = $this->skipper->shouldSkipFileInfo($smartFileInfo); + $this->assertSame($expectedSkip, $resultSkip); + } + + /** + * @return Iterator + */ + public function provideDataShouldSkipFileInfo(): Iterator + { + yield [__DIR__ . '/Fixture/SomeRandom/file.txt', false]; + yield [__DIR__ . '/Fixture/SomeSkipped/any.txt', true]; + } + + /** + * @param object|class-string $element + * @dataProvider provideDataShouldSkipElement() + */ + public function testSkipElement(string|object $element, bool $expectedSkip): void + { + $resultSkip = $this->skipper->shouldSkipElement($element); + $this->assertSame($expectedSkip, $resultSkip); + } + + /** + * @return Iterator[]|class-string[]|FifthElement[]> + */ + public function provideDataShouldSkipElement(): Iterator + { + yield [ThreeMan::class, false]; + yield [SixthSense::class, true]; + yield [new FifthElement(), true]; + } +} diff --git a/packages-tests/Skipper/Skipper/Skipper/config/config.php b/packages-tests/Skipper/Skipper/Skipper/config/config.php new file mode 100644 index 00000000000..8c819b46530 --- /dev/null +++ b/packages-tests/Skipper/Skipper/Skipper/config/config.php @@ -0,0 +1,18 @@ +skip([ + // windows like path + '*\SomeSkipped\*', + + // elements + FifthElement::class, + SixthSense::class, + ]); +}; diff --git a/packages/PostRector/Application/PostFileProcessor.php b/packages/PostRector/Application/PostFileProcessor.php index 6f92623e848..8cf1f5d8b7b 100644 --- a/packages/PostRector/Application/PostFileProcessor.php +++ b/packages/PostRector/Application/PostFileProcessor.php @@ -11,7 +11,7 @@ use Rector\Core\Provider\CurrentFileProvider; use Rector\Core\ValueObject\Application\File; use Rector\PostRector\Contract\Rector\PostRectorInterface; -use Symplify\Skipper\Skipper\Skipper; +use Rector\Skipper\Skipper\Skipper; final class PostFileProcessor { diff --git a/packages/Skipper/Contract/SkipVoterInterface.php b/packages/Skipper/Contract/SkipVoterInterface.php new file mode 100644 index 00000000000..60071310f51 --- /dev/null +++ b/packages/Skipper/Contract/SkipVoterInterface.php @@ -0,0 +1,14 @@ +normalizePath($matchingPath); + $normalizedFilePath = $this->normalizePath($filePath); + if (\fnmatch($normalizedMatchingPath, $normalizedFilePath)) { + return \true; + } + + // in case of relative compare + return \fnmatch('*/' . $normalizedMatchingPath, $normalizedFilePath); + } + + private function normalizePath(string $path): string + { + return \str_replace('\\', '/', $path); + } +} diff --git a/packages/Skipper/Matcher/FileInfoMatcher.php b/packages/Skipper/Matcher/FileInfoMatcher.php new file mode 100644 index 00000000000..63eed2beaf1 --- /dev/null +++ b/packages/Skipper/Matcher/FileInfoMatcher.php @@ -0,0 +1,60 @@ +doesFileMatchPattern($file, $filePattern)) { + return true; + } + } + + return false; + } + + /** + * Supports both relative and absolute $file path. They differ for PHP-CS-Fixer and PHP_CodeSniffer. + */ + private function doesFileMatchPattern(SmartFileInfo | string $file, string $ignoredPath): bool + { + $filePath = $file instanceof SmartFileInfo ? $file->getRealPath() : $file; + + // in ecs.php, the path can be absolute + if ($filePath === $ignoredPath) { + return true; + } + + $ignoredPath = $this->fnMatchPathNormalizer->normalizeForFnmatch($ignoredPath); + if ($ignoredPath === '') { + return false; + } + + if (str_starts_with($filePath, $ignoredPath)) { + return true; + } + + if (str_ends_with($filePath, $ignoredPath)) { + return true; + } + + return $this->fnmatcher->match($ignoredPath, $filePath); + } +} diff --git a/packages/Skipper/SkipCriteriaResolver/SkippedClassResolver.php b/packages/Skipper/SkipCriteriaResolver/SkippedClassResolver.php new file mode 100644 index 00000000000..301348df963 --- /dev/null +++ b/packages/Skipper/SkipCriteriaResolver/SkippedClassResolver.php @@ -0,0 +1,55 @@ + + */ + private array $skippedClasses = []; + + public function __construct( + private readonly ParameterProvider $parameterProvider, + private readonly ClassLikeExistenceChecker $classLikeExistenceChecker + ) { + } + + /** + * @return array + */ + public function resolve(): array + { + if ($this->skippedClasses !== []) { + return $this->skippedClasses; + } + + $skip = $this->parameterProvider->provideArrayParameter(Option::SKIP); + + foreach ($skip as $key => $value) { + // e.g. [SomeClass::class] → shift values to [SomeClass::class => null] + if (is_int($key)) { + $key = $value; + $value = null; + } + + if (! is_string($key)) { + continue; + } + + if (! $this->classLikeExistenceChecker->doesClassLikeExist($key)) { + continue; + } + + $this->skippedClasses[$key] = $value; + } + + return $this->skippedClasses; + } +} diff --git a/packages/Skipper/SkipCriteriaResolver/SkippedPathsResolver.php b/packages/Skipper/SkipCriteriaResolver/SkippedPathsResolver.php new file mode 100644 index 00000000000..b42b7058152 --- /dev/null +++ b/packages/Skipper/SkipCriteriaResolver/SkippedPathsResolver.php @@ -0,0 +1,56 @@ +skippedPaths !== []) { + return $this->skippedPaths; + } + + $skip = $this->parameterProvider->provideArrayParameter(Option::SKIP); + + foreach ($skip as $key => $value) { + if (! is_int($key)) { + continue; + } + + if (file_exists($value)) { + $this->skippedPaths[] = $this->pathNormalizer->normalizePath($value); + continue; + } + + if (\str_contains((string) $value, '*')) { + $this->skippedPaths[] = $this->pathNormalizer->normalizePath($value); + continue; + } + } + + return $this->skippedPaths; + } +} diff --git a/packages/Skipper/SkipVoter/ClassSkipVoter.php b/packages/Skipper/SkipVoter/ClassSkipVoter.php new file mode 100644 index 00000000000..3ad3430f428 --- /dev/null +++ b/packages/Skipper/SkipVoter/ClassSkipVoter.php @@ -0,0 +1,36 @@ +classLikeExistenceChecker->doesClassLikeExist($element); + } + + public function shouldSkip(string | object $element, SmartFileInfo | string $file): bool + { + $skippedClasses = $this->skippedClassResolver->resolve(); + return $this->skipSkipper->doesMatchSkip($element, $file, $skippedClasses); + } +} diff --git a/packages/Skipper/SkipVoter/PathSkipVoter.php b/packages/Skipper/SkipVoter/PathSkipVoter.php new file mode 100644 index 00000000000..3dd9c49ac4c --- /dev/null +++ b/packages/Skipper/SkipVoter/PathSkipVoter.php @@ -0,0 +1,30 @@ +skippedPathsResolver->resolve(); + return $this->fileInfoMatcher->doesFileInfoMatchPatterns($file, $skippedPaths); + } +} diff --git a/packages/Skipper/Skipper/SkipSkipper.php b/packages/Skipper/Skipper/SkipSkipper.php new file mode 100644 index 00000000000..ae312da6ec5 --- /dev/null +++ b/packages/Skipper/Skipper/SkipSkipper.php @@ -0,0 +1,39 @@ + $skippedClasses + */ + public function doesMatchSkip(object | string $checker, SmartFileInfo | string $file, array $skippedClasses): bool + { + foreach ($skippedClasses as $skippedClass => $skippedFiles) { + if (! is_a($checker, $skippedClass, true)) { + continue; + } + + // skip everywhere + if (! is_array($skippedFiles)) { + return true; + } + + if ($this->fileInfoMatcher->doesFileInfoMatchPatterns($file, $skippedFiles)) { + return true; + } + } + + return false; + } +} diff --git a/packages/Skipper/Skipper/Skipper.php b/packages/Skipper/Skipper/Skipper.php new file mode 100644 index 00000000000..6a8f0df4a95 --- /dev/null +++ b/packages/Skipper/Skipper/Skipper.php @@ -0,0 +1,54 @@ +shouldSkipElementAndFileInfo($element, $fileInfo); + } + + public function shouldSkipFileInfo(SmartFileInfo $smartFileInfo): bool + { + return $this->shouldSkipElementAndFileInfo(self::FILE_ELEMENT, $smartFileInfo); + } + + public function shouldSkipElementAndFileInfo(string | object $element, SmartFileInfo|string $smartFileInfo): bool + { + foreach ($this->skipVoters as $skipVoter) { + if (! $skipVoter->match($element)) { + continue; + } + + // dump(get_class($skipVoter)); + + return $skipVoter->shouldSkip($element, $smartFileInfo); + } + + return false; + } +} diff --git a/phpstan.neon b/phpstan.neon index 5e9a073fd57..82cd131d624 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -778,3 +778,9 @@ parameters: - message: '#Content of method "(.*?)" is duplicated\. Use unique content or service instead#' path: rules/DeadCode/Rector/ConstFetch/RemovePhpVersionIdCheckRector.php + + # known existing class + - + message: '#Instead of "instanceof/is_a\(\)" use ReflectionProvider service or "\(new ObjectType\(\)\)\->isSuperTypeOf\(\)" for static reflection to work#' + path: packages/Skipper/Skipper/SkipSkipper.php + diff --git a/src/Configuration/Option.php b/src/Configuration/Option.php index 0f2537d79ab..891b5599c9a 100644 --- a/src/Configuration/Option.php +++ b/src/Configuration/Option.php @@ -6,7 +6,6 @@ use Rector\Caching\Contract\ValueObject\Storage\CacheStorageInterface; use Rector\Caching\ValueObject\Storage\FileCacheStorage; -use Symplify\Skipper\ValueObject\Option as SkipperOption; final class Option { @@ -109,7 +108,7 @@ final class Option * @deprecated Use @see \Rector\Config\RectorConfig::skip() instead * @var string */ - public const SKIP = SkipperOption::SKIP; + public const SKIP = 'skip'; /** * @deprecated Use RectorConfig::fileExtensions() instead diff --git a/src/FileSystem/FilesFinder.php b/src/FileSystem/FilesFinder.php index 9180d43f7bb..a949892d71e 100644 --- a/src/FileSystem/FilesFinder.php +++ b/src/FileSystem/FilesFinder.php @@ -6,9 +6,10 @@ use Rector\Caching\UnchangedFilesFilter; use Rector\Core\Util\StringUtils; +use Rector\Skipper\Enum\AsteriskMatch; +use Rector\Skipper\SkipCriteriaResolver\SkippedPathsResolver; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; -use Symplify\Skipper\SkipCriteriaResolver\SkippedPathsResolver; use Symplify\SmartFileSystem\SmartFileInfo; /** @@ -16,18 +17,6 @@ */ final class FilesFinder { - /** - * @var string - * @see https://regex101.com/r/e1jm7v/1 - */ - private const STARTS_WITH_ASTERISK_REGEX = '#^\*(.*?)[^*]$#'; - - /** - * @var string - * @see https://regex101.com/r/EgJQyZ/1 - */ - private const ENDS_WITH_ASTERISK_REGEX = '#^[^*](.*?)\*$#'; - public function __construct( private readonly FilesystemTweaker $filesystemTweaker, private readonly SkippedPathsResolver $skippedPathsResolver, @@ -139,12 +128,12 @@ private function addFilterWithExcludedPaths(Finder $finder): void private function normalizeForFnmatch(string $path): string { // ends with * - if (StringUtils::isMatch($path, self::ENDS_WITH_ASTERISK_REGEX)) { + if (StringUtils::isMatch($path, AsteriskMatch::ONLY_ENDS_WITH_ASTERISK_REGEX)) { return '*' . $path; } // starts with * - if (StringUtils::isMatch($path, self::STARTS_WITH_ASTERISK_REGEX)) { + if (StringUtils::isMatch($path, AsteriskMatch::ONLY_STARTS_WITH_ASTERISK_REGEX)) { return $path . '*'; } diff --git a/src/Kernel/RectorKernel.php b/src/Kernel/RectorKernel.php index e7623fa4536..da865e0300e 100644 --- a/src/Kernel/RectorKernel.php +++ b/src/Kernel/RectorKernel.php @@ -17,7 +17,6 @@ use Symplify\AutowireArrayParameter\DependencyInjection\CompilerPass\AutowireArrayParameterCompilerPass; use Symplify\PackageBuilder\DependencyInjection\CompilerPass\AutowireInterfacesCompilerPass; use Symplify\PackageBuilder\ValueObject\ConsoleColorDiffConfig; -use Symplify\Skipper\ValueObject\SkipperConfig; final class RectorKernel { @@ -96,6 +95,6 @@ private function createCompilerPasses(): array */ private function createDefaultConfigFiles(): array { - return [__DIR__ . '/../../config/config.php', SkipperConfig::FILE_PATH, ConsoleColorDiffConfig::FILE_PATH]; + return [__DIR__ . '/../../config/config.php', ConsoleColorDiffConfig::FILE_PATH]; } } diff --git a/src/Rector/AbstractRector.php b/src/Rector/AbstractRector.php index 8a2a6aacd2b..9f77bceedfe 100644 --- a/src/Rector/AbstractRector.php +++ b/src/Rector/AbstractRector.php @@ -41,9 +41,9 @@ use Rector\NodeTypeResolver\NodeTypeResolver; use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; use Rector\PostRector\Collector\NodesToRemoveCollector; +use Rector\Skipper\Skipper\Skipper; use Rector\StaticTypeMapper\StaticTypeMapper; use Symfony\Contracts\Service\Attribute\Required; -use Symplify\Skipper\Skipper\Skipper; abstract class AbstractRector extends NodeVisitorAbstract implements PhpRectorInterface {