diff --git a/bin/compile b/bin/compile new file mode 100755 index 000000000..33c3feea6 --- /dev/null +++ b/bin/compile @@ -0,0 +1,16 @@ +#!/usr/bin/env php +create(); + +/** @var RectorCompilerConsoleApplication $application */ +$application = $container->get(RectorCompilerConsoleApplication::class); +exit($application->run()); diff --git a/bin/typo3-rector b/bin/typo3-rector new file mode 100755 index 000000000..eb2009077 --- /dev/null +++ b/bin/typo3-rector @@ -0,0 +1,185 @@ +#!/usr/bin/env php +includeCwdVendorAutoloadIfExists(); +$autoloadIncluder->autoloadProjectAutoloaderFile('/../../autoload.php'); +$autoloadIncluder->includeDependencyOrRepositoryVendorAutoloadIfExists(); +$autoloadIncluder->autoloadFromCommandLine(); + +$symfonyStyleFactory = new SymfonyStyleFactory(new PrivatesCaller()); +$symfonyStyle = $symfonyStyleFactory->create(); + +try { + $composerJsonReader = new ComposerJsonReader(__DIR__ . '/../composer.json'); + $versionChecker = new MinimalVersionChecker( + PHP_VERSION, + new ComposerJsonParser($composerJsonReader->read()) + ); + $versionChecker->check(); + + $rectorConfigsResolver = new Typo3RectorConfigsResolver(); + $configFileInfos = $rectorConfigsResolver->provide(); + + // Build DI container + $rectorContainerFactory = new RectorContainerFactory(); + + // shift configs as last so parameters with main config have higher priority + $configShifter = new ConfigShifter(); + $firstResolvedConfig = $rectorConfigsResolver->getFirstResolvedConfig(); + if ($firstResolvedConfig !== null) { + $configFileInfos = $configShifter->shiftInputConfigAsLast($configFileInfos, $firstResolvedConfig); + } + + $container = $rectorContainerFactory->createFromConfigs($configFileInfos); + + if ($rectorConfigsResolver->getFirstResolvedConfig()) { + /** @var Configuration $configuration */ + $configuration = $container->get(Configuration::class); + $configuration->setFirstResolverConfigFileInfo($rectorConfigsResolver->getFirstResolvedConfig()); + + /** @var ChangedFilesDetector $changedFilesDetector */ + $changedFilesDetector = $container->get(ChangedFilesDetector::class); + $changedFilesDetector->setFirstResolvedConfigFileInfo($rectorConfigsResolver->getFirstResolvedConfig()); + } +} catch (SetNotFoundException $setNotFoundException) { + (new InvalidSetReporter())->report($setNotFoundException); + exit(ShellCode::ERROR); +} catch (Throwable $throwable) { + $symfonyStyle->error($throwable->getMessage()); + exit(ShellCode::ERROR); +} + +/** @var Application $application */ +$application = $container->get(Application::class); +exit($application->run()); + +final class AutoloadIncluder +{ + /** + * @var string[] + */ + private $alreadyLoadedAutoloadFiles = []; + + public function includeCwdVendorAutoloadIfExists(): void + { + $cwdVendorAutoload = getcwd() . '/vendor/autoload.php'; + if (!is_file($cwdVendorAutoload)) { + return; + } + + $this->loadIfNotLoadedYet($cwdVendorAutoload, __METHOD__ . '()" on line ' . __LINE__); + } + + public function includeDependencyOrRepositoryVendorAutoloadIfExists(): void + { + // Rector's vendor is already loaded + if (class_exists('Rector\HttpKernel\RectorKernel')) { + return; + } + + $devOrPharVendorAutoload = __DIR__ . '/../vendor/autoload.php'; + if (! is_file($devOrPharVendorAutoload)) { + return; + } + + $this->loadIfNotLoadedYet($devOrPharVendorAutoload, __METHOD__ . '()" on line ' . __LINE__); + } + + /** + * Inspired by https://github.com/phpstan/phpstan-src/blob/e2308ecaf49a9960510c47f5a992ce7b27f6dba2/bin/phpstan#L19 + */ + public function autoloadProjectAutoloaderFile(string $file): void + { + $path = dirname(__DIR__) . $file; + if (!extension_loaded('phar')) { + if (is_file($path)) { + $this->loadIfNotLoadedYet($path, __METHOD__ . '()" on line ' . __LINE__); + } + return; + } + + $pharPath = Phar::running(false); + if ($pharPath === '') { + if (is_file($path)) { + $this->loadIfNotLoadedYet($path, __METHOD__ . '()" on line ' . __LINE__); + } + } else { + $path = dirname($pharPath) . $file; + if (is_file($path)) { + $this->loadIfNotLoadedYet($path, __METHOD__ . '()" on line ' . __LINE__); + } + } + } + + public function autoloadFromCommandLine(): void + { + $cliArgs = $_SERVER['argv']; + + $autoloadOptionPosition = array_search('-a', $cliArgs) ?: array_search('--autoload-file', $cliArgs); + if (! $autoloadOptionPosition) { + return; + } + + $autoloadFileValuePosition = $autoloadOptionPosition + 1; + $fileToAutoload = $cliArgs[$autoloadFileValuePosition] ?? null; + if ($fileToAutoload=== null) { + return; + } + + $this->loadIfNotLoadedYet($fileToAutoload, __METHOD__); + } + + private function loadIfNotLoadedYet(string $file, string $location): void + { + if (in_array($file, $this->alreadyLoadedAutoloadFiles, true)) { + return; + } + + if ($this->isDebugOption()) { + echo sprintf(sprintf( + 'File "%s" is about to be loaded in "%s"' . PHP_EOL, + $file, + $location + )); + } + + $this->alreadyLoadedAutoloadFiles[] = realpath($file); + + require_once $file; + } + + private function isDebugOption(): bool + { + return in_array('--debug', $_SERVER['argv'], true); + } +} diff --git a/build/box.json b/build/box.json new file mode 100644 index 000000000..244088d6e --- /dev/null +++ b/build/box.json @@ -0,0 +1,18 @@ +{ + "alias": "typo3-rector.phar", + "banner": false, + "base-path": "..", + "check-requirements": false, + "directories": [ + "vendor/rector/rector/config", + "vendor/rector/rector/src", + "vendor/rector/rector/rules", + "vendor/rector/rector/packages", + "config", + "src", + "Migrations" + ], + "exclude-composer-files": false, + "force-autodiscovery": true, + "output": "tmp/typo3-rector.phar" +} diff --git a/build/box.phar b/build/box.phar new file mode 100644 index 000000000..7e6fa253f Binary files /dev/null and b/build/box.phar differ diff --git a/composer.json b/composer.json index 25577d86e..40a7269ff 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "require": { "php": "^7.2", "nette/utils": "^3.1", + "phpstan/phpstan": "^0.12.52", "rector/rector": "0.8.48" }, "require-dev": { @@ -24,6 +25,9 @@ "Ssch\\TYPO3Rector\\": "src" } }, + "bin": [ + "bin/typo3-rector" + ], "autoload-dev": { "psr-4": { "Ssch\\TYPO3Rector\\Tests\\": "tests", diff --git a/config/compiler.php b/config/compiler.php new file mode 100644 index 000000000..2057e4aea --- /dev/null +++ b/config/compiler.php @@ -0,0 +1,37 @@ +parameters(); + + $parameters->set(Option::DATA_DIR, __DIR__ . '/../build'); + $parameters->set(Option::BUILD_DIR, __DIR__ . '/..'); + + $services = $containerConfigurator->services(); + + $services->defaults() + ->public() + ->autowire(); + + $services->load('Ssch\TYPO3Rector\Compiler\\', __DIR__ . '/../src/Compiler') + ->exclude([ + __DIR__ . '/../src/Compiler/Exception', + __DIR__ . '/../src/Compiler/DependencyInjection', + __DIR__ . '/../src/Compiler/HttpKernel', + __DIR__ . '/../src/Compiler/PhpScoper', + __DIR__ . '/../src/Compiler/ValueObject', + ]); + + $services->set(SmartFileSystem::class); + + $services->set(CiDetector::class); + + $services->set(ParameterProvider::class); +}; diff --git a/config/services.php b/config/services.php index f7f712be2..47ccbd03e 100644 --- a/config/services.php +++ b/config/services.php @@ -2,7 +2,9 @@ declare(strict_types=1); +use Ssch\TYPO3Rector\Console\Application; use Ssch\TYPO3Rector\Helper\Database\Refactorings\DatabaseConnectionToDbalRefactoring; +use Symfony\Component\Console\Application as SymfonyApplication; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { @@ -16,6 +18,15 @@ ->tag('database.dbal.refactoring') ->share(false); - $services->load('Ssch\TYPO3Rector\\', __DIR__ . '/../src/') - ->exclude([__DIR__ . '/../src/Rector']); + $services->alias(SymfonyApplication::class, Application::class); + + $services->load('Ssch\\TYPO3Rector\\', __DIR__ . '/../src') + ->exclude( + [ + __DIR__ . '/../src/Rector', + __DIR__ . '/../src/Set', + __DIR__ . '/../src/Bootstrap', + __DIR__ . '/../src/Compiler', + ] + ); }; diff --git a/config/typo3-8.7.php b/config/typo3-8.7.php index e5b442849..d700474d2 100644 --- a/config/typo3-8.7.php +++ b/config/typo3-8.7.php @@ -5,5 +5,5 @@ use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { - $containerConfigurator->import(__DIR__ . '/*'); + $containerConfigurator->import(__DIR__ . '/v8/*'); }; diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index acbeab9d2..ca5fc32b2 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -5,4 +5,8 @@ parameters: count: 1 path: src/Rector/v10/v2/InjectEnvironmentServiceIfNeededInResponseRector.php + - + message: "#^Method Ssch\\\\TYPO3Rector\\\\Set\\\\Typo3RectorSetProvider\\:\\:hydrateSetsFromConstants\\(\\) has parameter \\$setListReflectionClass with generic class ReflectionClass but does not specify its types\\: T$#" + count: 1 + path: src/Set/Typo3RectorSetProvider.php diff --git a/rector.php b/rector.php index 77c96762e..a5e15fa38 100644 --- a/rector.php +++ b/rector.php @@ -25,6 +25,10 @@ $services->set(AddCodeCoverageIgnoreToMethodRectorDefinitionRector::class); $parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']); + $parameters->set( + Option::EXCLUDE_PATHS, + [__DIR__ . '/src/Bootstrap', __DIR__ . '/src/Set', __DIR__ . '/src/Compiler'] + ); # so Rector code is still PHP 7.2 compatible $parameters->set(Option::PHP_VERSION_FEATURES, '7.2'); }; diff --git a/src/Bootstrap/Typo3RectorConfigsResolver.php b/src/Bootstrap/Typo3RectorConfigsResolver.php new file mode 100644 index 000000000..5566376f4 --- /dev/null +++ b/src/Bootstrap/Typo3RectorConfigsResolver.php @@ -0,0 +1,94 @@ +setResolver = new SetResolver(); + $this->configResolver = new ConfigResolver(); + $rectorSetProvider = new Typo3RectorSetProvider(new RectorSetProvider()); + $this->setAwareConfigResolver = new SetAwareConfigResolver($rectorSetProvider); + } + + /** + * @noRector + */ + public function getFirstResolvedConfig(): ?SmartFileInfo + { + return $this->configResolver->getFirstResolvedConfigFileInfo(); + } + + /** + * @param SmartFileInfo[] $configFileInfos + * @return SmartFileInfo[] + */ + public function resolveSetFileInfosFromConfigFileInfos(array $configFileInfos): array + { + return $this->setAwareConfigResolver->resolveFromParameterSetsFromConfigFiles($configFileInfos); + } + + /** + * @return SmartFileInfo[] + */ + public function provide(): array + { + $configFileInfos = []; + + // Detect configuration from --set + $argvInput = new ArgvInput(); + + $set = $this->setResolver->resolveSetFromInput($argvInput); + if (null !== $set) { + $configFileInfos[] = $set->getSetFileInfo(); + } + + // And from --config or default one + $inputOrFallbackConfigFileInfo = $this->configResolver->resolveFromInputWithFallback( + $argvInput, + ['rector.php'] + ); + + if (null !== $inputOrFallbackConfigFileInfo) { + $configFileInfos[] = $inputOrFallbackConfigFileInfo; + } + + $setFileInfos = $this->resolveSetFileInfosFromConfigFileInfos($configFileInfos); + + if (in_array($argvInput->getFirstArgument(), ['generate', 'g', 'create', 'c'], true)) { + // autoload rector recipe file if present, just for \Rector\RectorGenerator\Command\GenerateCommand + $rectorRecipeFilePath = getcwd() . '/rector-recipe.php'; + if (file_exists($rectorRecipeFilePath)) { + $configFileInfos[] = new SmartFileInfo($rectorRecipeFilePath); + } + } + + return array_merge($configFileInfos, $setFileInfos); + } +} diff --git a/src/Compiler/Composer/ComposerJsonManipulator.php b/src/Compiler/Composer/ComposerJsonManipulator.php new file mode 100644 index 000000000..8aa713e98 --- /dev/null +++ b/src/Compiler/Composer/ComposerJsonManipulator.php @@ -0,0 +1,166 @@ +consoleDiffer = $consoleDiffer; + $this->smartFileSystem = $smartFileSystem; + } + + public function fixComposerJson(string $composerJsonFile): void + { + $fileContent = $this->smartFileSystem->readFile($composerJsonFile); + $this->originalComposerJsonFileContent = $fileContent; + + $json = Json::decode($fileContent, Json::FORCE_ARRAY); + $json = $this->removeDevKeys($json); + $json = $this->replacePHPStanWithPHPStanSrc($json); + + $encodedJson = Json::encode($json, Json::PRETTY); + + // show diff + if ($encodedJson !== $this->originalComposerJsonFileContent) { + $this->consoleDiffer->diff($this->originalComposerJsonFileContent, $encodedJson); + } + + $this->smartFileSystem->dumpFile($composerJsonFile, $encodedJson); + } + + /** + * This prevent root composer.json constant override + */ + public function restoreComposerJson(string $composerJsonFile): void + { + $this->smartFileSystem->dumpFile($composerJsonFile, $this->originalComposerJsonFileContent); + } + + /** + * @param mixed[] $json + * @return mixed[] + */ + private function removeDevKeys(array $json): array + { + foreach (self::KEYS_TO_REMOVE as $keyToRemove) { + unset($json[$keyToRemove]); + } + return $json; + } + + /** + * Use phpstan/phpstan-src, because the phpstan.phar cannot be packed into rector.phar + * @param mixed[] $json + * @return mixed[] + */ + private function replacePHPStanWithPHPStanSrc(array $json): array + { + // already replaced + if (! isset($json[self::REQUIRE][self::PHPSTAN_PHPSTAN])) { + return $json; + } + + $phpstanVersion = $json[self::REQUIRE][self::PHPSTAN_PHPSTAN]; + $phpstanVersion = ltrim($phpstanVersion, '^'); + // use dev-master till this get's to tagged: https://github.com/phpstan/phpstan-src/commit/535c0e25429c1e3dd0dd05f61b43a34830da2a09 + $json[self::REQUIRE]['phpstan/phpstan-src'] = 'dev-master'; + unset($json[self::REQUIRE][self::PHPSTAN_PHPSTAN]); + + $json['repositories'][] = [ + 'type' => 'vcs', + 'url' => 'https://github.com/phpstan/phpstan-src.git', + ]; + + $json = $this->addDevDependenciesFromPHPStan($json, $phpstanVersion); + + return $this->allowDevDependencies($json); + } + + /** + * @param mixed[] $json + * @return mixed[] + */ + private function addDevDependenciesFromPHPStan(array $json, string $phpstanVersion): array + { + // add dev dependencies from PHPStan composer.json + $phpstanComposerJsonFilePath = sprintf(self::PHPSTAN_COMPOSER_JSON, $phpstanVersion); + $phpstanComposerJson = $this->readRemoteFileToJson($phpstanComposerJsonFilePath); + + if (isset($phpstanComposerJson[self::REQUIRE])) { + foreach ($phpstanComposerJson[self::REQUIRE] as $package => $version) { + if (! Strings::startsWith($version, 'dev-master')) { + continue; + } + + $json[self::REQUIRE][$package] = $version; + } + } + + return $json; + } + + /** + * @param mixed[] $json + * @return mixed[] + */ + private function allowDevDependencies(array $json): array + { + $json['minimum-stability'] = 'dev'; + $json['prefer-stable'] = true; + + return $json; + } + + /** + * @return mixed[] + */ + private function readRemoteFileToJson(string $jsonFilePath): array + { + $jsonFileContent = $this->smartFileSystem->readFile($jsonFilePath); + + return (array) Json::decode($jsonFileContent, Json::FORCE_ARRAY); + } +} diff --git a/src/Compiler/Console/Command/CompileCommand.php b/src/Compiler/Console/Command/CompileCommand.php new file mode 100644 index 000000000..7c17fcf58 --- /dev/null +++ b/src/Compiler/Console/Command/CompileCommand.php @@ -0,0 +1,210 @@ +dataDir = (string) $parameterProvider->provideParameter(Option::DATA_DIR); + $this->buildDir = (string) $parameterProvider->provideParameter(Option::BUILD_DIR); + + $this->composerJsonManipulator = $composerJsonManipulator; + $this->jetbrainsStubsRenamer = $jetbrainsStubsRenamer; + $this->symfonyStyle = $symfonyStyle; + $this->ciDetector = $ciDetector; + $this->smartFileSystem = $smartFileSystem; + + parent::__construct(); + } + + protected function configure(): void + { + $this->setName(self::class); + $this->setDescription('Compile prefixed typo3-rector.phar'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $composerJsonFile = $this->buildDir . '/composer.json'; + + $title = sprintf('1. Adding "phpstan/phpstan-src" to "%s"', $composerJsonFile); + $this->symfonyStyle->title($title); + + $this->composerJsonManipulator->fixComposerJson($composerJsonFile); + + $this->symfonyStyle->newLine(1); + + $this->symfonyStyle->success(self::DONE); + + $this->symfonyStyle->newLine(1); + + $this->symfonyStyle->title('2. Running "composer update" without dev'); + + $process = new Process([ + 'composer', + 'update', + '--no-dev', + '--prefer-dist', + '--no-interaction', + '--classmap-authoritative', + self::ANSI, + ], $this->buildDir, null, null, null); + $process->mustRun(static function (string $type, string $buffer) use ($output): void { + $output->write($buffer); + }); + + $this->symfonyStyle->success(self::DONE); + + $this->symfonyStyle->newLine(1); + + $this->symfonyStyle->title('3. Downgrading PHPStan code to PHP 7.1'); + + $this->downgradePHPStanCodeToPHP71($output); + + $this->symfonyStyle->success(self::DONE); + $this->symfonyStyle->newLine(1); + + $this->symfonyStyle->title('4. Renaming PHPStorm stubs from "*.php" to ".stub"'); + + $this->jetbrainsStubsRenamer->renamePhpStormStubs($this->buildDir); + + $this->symfonyStyle->success(self::DONE); + $this->symfonyStyle->newLine(1); + + // the '--no-parallel' is needed, so "scoper.php.inc" can "require __DIR__ ./vendor/autoload.php" + // and "Nette\Neon\Neon" class can be used there + $this->symfonyStyle->title('5. Packing and prefixing rector.phar with Box and PHP Scoper'); + + $process = new Process([ + 'php', + 'box.phar', + 'compile', + '--no-parallel', + self::ANSI, + ], $this->dataDir, null, null, null); + $process->mustRun(static function (string $type, string $buffer) use ($output): void { + $output->write($buffer); + }); + + $this->symfonyStyle->success(self::DONE); + + $this->symfonyStyle->newLine(1); + + $this->symfonyStyle->title('6. Restoring root composer.json with "require-dev"'); + + $this->composerJsonManipulator->restoreComposerJson($composerJsonFile); + $this->restoreDependenciesLocallyIfNotCi($output); + + $this->symfonyStyle->success(self::DONE); + + return ShellCode::SUCCESS; + } + + private function downgradePHPStanCodeToPHP71(OutputInterface $output): void + { + // downgrade phpstan-src code from PHP 7.4 to PHP 7.1, see https://github.com/phpstan/phpstan-src/pull/202/files + $this->fixRequirePath(); + + $process = new Process(['php', 'vendor/phpstan/phpstan-src/bin/transform-source.php'], $this->buildDir); + $process->mustRun(static function (string $type, string $buffer) use ($output): void { + $output->write($buffer); + }); + } + + private function restoreDependenciesLocallyIfNotCi(OutputInterface $output): void + { + if ($this->ciDetector->isCiDetected()) { + return; + } + + $process = new Process(['composer', 'install', self::ANSI], $this->buildDir, null, null, null); + $process->mustRun(static function (string $type, string $buffer) use ($output): void { + $output->write($buffer); + }); + } + + private function fixRequirePath(): void + { + // fix require path first + $filePath = __DIR__ . '/../../../../vendor/phpstan/phpstan-src/bin/transform-source.php'; + $fileContent = $this->smartFileSystem->readFile($filePath); + + $fileContent = str_replace( + "__DIR__ . '/../vendor/autoload.php'", + "__DIR__ . '/../../../../vendor/autoload.php'", + $fileContent + ); + + $this->smartFileSystem->dumpFile($filePath, $fileContent); + } +} diff --git a/src/Compiler/Console/RectorCompilerConsoleApplication.php b/src/Compiler/Console/RectorCompilerConsoleApplication.php new file mode 100644 index 000000000..3b856afef --- /dev/null +++ b/src/Compiler/Console/RectorCompilerConsoleApplication.php @@ -0,0 +1,20 @@ +add($compileCommand); + $commandClass = get_class($compileCommand); + $this->setDefaultCommand($commandClass, true); + } +} diff --git a/src/Compiler/DependencyInjection/ContainerFactory.php b/src/Compiler/DependencyInjection/ContainerFactory.php new file mode 100644 index 000000000..1f08b3f35 --- /dev/null +++ b/src/Compiler/DependencyInjection/ContainerFactory.php @@ -0,0 +1,20 @@ +boot(); + + return $rectorCompilerKernel->getContainer(); + } +} diff --git a/src/Compiler/Exception/CompilerShouldNotHappenException.php b/src/Compiler/Exception/CompilerShouldNotHappenException.php new file mode 100644 index 000000000..a699cfb67 --- /dev/null +++ b/src/Compiler/Exception/CompilerShouldNotHappenException.php @@ -0,0 +1,11 @@ +load(__DIR__ . '/../../../config/compiler.php'); + } + + /** + * @return BundleInterface[] + */ + public function registerBundles(): array + { + return [new ConsoleColorDiffBundle()]; + } +} diff --git a/src/Compiler/Renaming/JetbrainsStubsRenamer.php b/src/Compiler/Renaming/JetbrainsStubsRenamer.php new file mode 100644 index 000000000..5da605387 --- /dev/null +++ b/src/Compiler/Renaming/JetbrainsStubsRenamer.php @@ -0,0 +1,101 @@ +symfonyStyle = $symfonyStyle; + $this->smartFileSystem = $smartFileSystem; + } + + public function renamePhpStormStubs(string $buildDir): void + { + $directory = $buildDir . '/vendor/jetbrains/phpstorm-stubs'; + if (! is_dir($directory)) { + return; + } + + $this->renameStubFileSuffixes($directory); + $this->renameFilesSuffixesInPhpStormStubsMapFile($directory); + } + + private function renameStubFileSuffixes(string $directory): void + { + $stubFileInfos = $this->getStubFileInfos($directory); + $message = sprintf( + 'Renaming "%d" stub files from "%s"', + count($stubFileInfos), + 'vendor/jetbrains/phpstorm-stubs' + ); + $this->symfonyStyle->note($message); + + foreach ($stubFileInfos as $stubFileInfo) { + $path = $stubFileInfo->getPathname(); + + $filenameWithStubSuffix = dirname($path) . '/' . $stubFileInfo->getBasename('.php') . '.stub'; + $this->smartFileSystem->rename($path, $filenameWithStubSuffix); + } + } + + private function renameFilesSuffixesInPhpStormStubsMapFile(string $phpStormStubsDirectory): void + { + $stubsMapPath = $phpStormStubsDirectory . '/PhpStormStubsMap.php'; + + if (! file_exists($stubsMapPath)) { + throw new CompilerShouldNotHappenException(sprintf('File "%s" was not found', $stubsMapPath)); + } + + $stubsMapContents = $this->smartFileSystem->readFile($stubsMapPath); + $stubsMapContents = Strings::replace($stubsMapContents, self::PHP_SUFFIX_COMMA_REGEX, ".stub',"); + + $this->smartFileSystem->dumpFile($stubsMapPath, $stubsMapContents); + } + + /** + * @return SplFileInfo[] + */ + private function getStubFileInfos(string $phpStormStubsDirectory): array + { + if (! is_dir($phpStormStubsDirectory)) { + throw new CompilerShouldNotHappenException(sprintf( + 'Directory "%s" was not found', + $phpStormStubsDirectory + )); + } + + $stubFinder = Finder::create() + ->files() + ->name('*.php') + ->in($phpStormStubsDirectory) + ->notName('#PhpStormStubsMap\.php$#'); + + return iterator_to_array($stubFinder->getIterator()); + } +} diff --git a/src/Compiler/ValueObject/Option.php b/src/Compiler/ValueObject/Option.php new file mode 100644 index 000000000..61f811bc9 --- /dev/null +++ b/src/Compiler/ValueObject/Option.php @@ -0,0 +1,20 @@ +getPrettyVersion(); + } catch (OutOfBoundsException $outOfBoundsException) { + $version = 'Unknown'; + } + + parent::__construct(self::NAME, $version); + + $this->addCommands($commands); + $this->configuration = $configuration; + $this->noRectorsLoadedReporter = $noRectorsLoadedReporter; + } + + public function doRun(InputInterface $input, OutputInterface $output): int + { + // @fixes https://github.com/rectorphp/rector/issues/2205 + $isXdebugAllowed = $input->hasParameterOption('--xdebug'); + if (! $isXdebugAllowed) { + $xdebugHandler = new XdebugHandler('typo3-rector', '--ansi'); + $xdebugHandler->check(); + unset($xdebugHandler); + } + + $shouldFollowByNewline = false; + + // switch working dir + $newWorkDir = $this->getNewWorkingDir($input); + if ('' !== $newWorkDir) { + $oldWorkingDir = getcwd(); + chdir($newWorkDir); + $output->isDebug() && $output->writeln('Changed CWD form ' . $oldWorkingDir . ' to ' . getcwd()); + } + + if ($this->shouldPrintMetaInformation($input)) { + $output->writeln($this->getLongVersion()); + $shouldFollowByNewline = true; + + $configFilePath = $this->configuration->getConfigFilePath(); + if ($configFilePath) { + $configFileInfo = new SmartFileInfo($configFilePath); + $relativeConfigPath = $configFileInfo->getRelativeFilePathFromDirectory(getcwd()); + + $output->writeln('Config file: ' . $relativeConfigPath); + $shouldFollowByNewline = true; + } + } + + if ($shouldFollowByNewline) { + $output->write(PHP_EOL); + } + + return parent::doRun($input, $output); + } + + public function renderThrowable(Throwable $throwable, OutputInterface $output): void + { + if (is_a($throwable, NoRectorsLoadedException::class)) { + $this->noRectorsLoadedReporter->report(); + return; + } + + // TODO: Change the autogenerated stub + parent::renderThrowable($throwable, $output); + } + + protected function getDefaultInputDefinition(): InputDefinition + { + $defaultInputDefinition = parent::getDefaultInputDefinition(); + + $this->removeUnusedOptions($defaultInputDefinition); + $this->addCustomOptions($defaultInputDefinition); + + return $defaultInputDefinition; + } + + private function getNewWorkingDir(InputInterface $input): string + { + $workingDir = $input->getParameterOption(['--working-dir', '-d']); + if (false !== $workingDir && ! is_dir($workingDir)) { + throw new InvalidConfigurationException( + 'Invalid working directory specified, ' . $workingDir . ' does not exist.' + ); + } + + return (string) $workingDir; + } + + private function shouldPrintMetaInformation(InputInterface $input): bool + { + $hasNoArguments = null === $input->getFirstArgument(); + if ($hasNoArguments) { + return false; + } + + $hasVersionOption = $input->hasParameterOption('--version'); + if ($hasVersionOption) { + return false; + } + + $outputFormat = $input->getParameterOption(['-o', '--output-format']); + return ! in_array($outputFormat, [JsonOutputFormatter::NAME, CheckstyleOutputFormatter::NAME], true); + } + + private function removeUnusedOptions(InputDefinition $inputDefinition): void + { + $options = $inputDefinition->getOptions(); + + unset($options['quiet'], $options['no-interaction']); + + $inputDefinition->setOptions($options); + } + + private function addCustomOptions(InputDefinition $inputDefinition): void + { + $inputDefinition->addOption(new InputOption( + Option::OPTION_CONFIG, + 'c', + InputOption::VALUE_REQUIRED, + 'Path to config file', + $this->getDefaultConfigPath() + )); + + $inputDefinition->addOption(new InputOption( + Option::OPTION_DEBUG, + null, + InputOption::VALUE_NONE, + 'Enable debug verbosity (-vvv)' + )); + + $inputDefinition->addOption(new InputOption( + Option::XDEBUG, + null, + InputOption::VALUE_NONE, + 'Allow running xdebug' + )); + + $inputDefinition->addOption(new InputOption( + Option::OPTION_CLEAR_CACHE, + null, + InputOption::VALUE_NONE, + 'Clear cache' + )); + + $inputDefinition->addOption(new InputOption( + '--working-dir', + '-d', + InputOption::VALUE_REQUIRED, + 'If specified, use the given directory as working directory.' + )); + } + + private function getDefaultConfigPath(): string + { + return getcwd() . '/rector.php'; + } +} diff --git a/src/Rector/Migrations/RenameClassMapAliasRector.php b/src/Rector/Migrations/RenameClassMapAliasRector.php index 5e63f1a4f..6cc6153bb 100644 --- a/src/Rector/Migrations/RenameClassMapAliasRector.php +++ b/src/Rector/Migrations/RenameClassMapAliasRector.php @@ -17,6 +17,7 @@ use Rector\Core\RectorDefinition\ConfiguredCodeSample; use Rector\Core\RectorDefinition\RectorDefinition; use Rector\Renaming\NodeManipulator\ClassRenamer; +use Symplify\SmartFileSystem\SmartFileInfo; final class RenameClassMapAliasRector extends AbstractRector implements ConfigurableRectorInterface { @@ -113,14 +114,10 @@ public function configure(array $configuration): void { $classAliasMaps = $configuration[self::CLASS_ALIAS_MAPS] ?? []; foreach ($classAliasMaps as $file) { - $filePath = realpath($file); - - if (false !== $filePath && file_exists($filePath)) { - $classAliasMap = require $filePath; - - foreach ($classAliasMap as $oldClass => $newClass) { - $this->oldToNewClasses[$oldClass] = $newClass; - } + $filePath = new SmartFileInfo($file); + $classAliasMap = require $filePath->getRealPath(); + foreach ($classAliasMap as $oldClass => $newClass) { + $this->oldToNewClasses[$oldClass] = $newClass; } } diff --git a/src/Set/Typo3RectorSetProvider.php b/src/Set/Typo3RectorSetProvider.php new file mode 100644 index 000000000..dfe2b5a0e --- /dev/null +++ b/src/Set/Typo3RectorSetProvider.php @@ -0,0 +1,91 @@ +hydrateSetsFromConstants($setListReflectionClass); + $this->rectorSetProvider = $rectorSetProvider; + } + + public function provide(): array + { + return array_merge($this->sets, $this->rectorSetProvider->provide()); + } + + public function provideByName(string $desiredSetName): ?Set + { + try { + $foundSet = parent::provideByName($desiredSetName); + if ($foundSet instanceof Set) { + return $foundSet; + } + + // sencond approach by set path + foreach ($this->sets as $set) { + if (! file_exists($desiredSetName)) { + continue; + } + + $desiredSetFileInfo = new SmartFileInfo($desiredSetName); + if ($set->getSetFileInfo()->getRealPath() !== $desiredSetFileInfo->getRealPath()) { + continue; + } + + return $set; + } + + $message = sprintf('Set "%s" was not found', $desiredSetName); + throw new SetNotFoundException($message, $desiredSetName, $this->provideSetNames()); + } catch (SetNotFoundException $setNotFoundException) { + return $this->rectorSetProvider->provideByName($desiredSetName); + } + } + + private function hydrateSetsFromConstants(ReflectionClass $setListReflectionClass): void + { + foreach ($setListReflectionClass->getConstants() as $name => $setPath) { + if (! file_exists($setPath)) { + $message = sprintf('Set path "%s" was not found', $name); + throw new ShouldNotHappenException($message); + } + + $setName = StaticRectorStrings::constantToDashes($name); + + // remove `-` before numbers + $setName = Strings::replace($setName, self::DASH_NUMBER_REGEX, '$1'); + $this->sets[] = new Set($setName, new SmartFileInfo($setPath)); + } + } +} diff --git a/src/Set/Typo3SetList.php b/src/Set/Typo3SetList.php new file mode 100644 index 000000000..71e33d521 --- /dev/null +++ b/src/Set/Typo3SetList.php @@ -0,0 +1,38 @@ +