From 9a00924ed8286c984ecaeb697213db250fe8abf0 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Tue, 27 Aug 2024 13:17:43 +0200 Subject: [PATCH] [DX] [Experimental] Add withPhpLevel() to raise PHP lele one rule at a time --- src/Bridge/SetRectorsResolver.php | 20 ++++- .../Levels/LevelRulesResolver.php | 5 ++ src/Configuration/RectorConfigBuilder.php | 89 +++++++++++++++---- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/src/Bridge/SetRectorsResolver.php b/src/Bridge/SetRectorsResolver.php index 97003eb225d..b187b5cf460 100644 --- a/src/Bridge/SetRectorsResolver.php +++ b/src/Bridge/SetRectorsResolver.php @@ -5,6 +5,7 @@ namespace Rector\Bridge; use Rector\Config\RectorConfig; +use Rector\Contract\Rector\ConfigurableRectorInterface; use Rector\Contract\Rector\RectorInterface; use ReflectionProperty; use Webmozart\Assert\Assert; @@ -16,6 +17,22 @@ */ final class SetRectorsResolver { + /** + * @return array> + */ + public function resolveFromFilePathForPhpLevel(string $configFilePath): array + { + $rectorClasses = $this->resolveFromFilePath($configFilePath); + + $nonConfigurableRectorClasses = array_filter( + $rectorClasses, + fn (string $rectorClass): bool => ! is_a($rectorClass, ConfigurableRectorInterface::class, true) + ); + + // revert to start from the lowest level + return array_reverse($nonConfigurableRectorClasses); + } + /** * @return array> */ @@ -34,7 +51,8 @@ public function resolveFromFilePath(string $configFilePath): array $tags = $tagsReflectionProperty->getValue($rectorConfig); $rectorClasses = $tags[RectorInterface::class] ?? []; - sort($rectorClasses); + + // avoid sorting to keep original natural order for levels return array_unique($rectorClasses); } diff --git a/src/Configuration/Levels/LevelRulesResolver.php b/src/Configuration/Levels/LevelRulesResolver.php index 9f9e1628c37..8409f297e3d 100644 --- a/src/Configuration/Levels/LevelRulesResolver.php +++ b/src/Configuration/Levels/LevelRulesResolver.php @@ -16,6 +16,9 @@ final class LevelRulesResolver */ public static function resolve(int $level, array $availableRules, string $methodName): array { + Assert::positiveInteger($level); + Assert::allIsAOf($availableRules, RectorInterface::class); + $rulesCount = count($availableRules); if ($availableRules === []) { @@ -40,6 +43,8 @@ public static function resolve(int $level, array $availableRules, string $method $levelRules[] = $availableRules[$i]; } + Assert::allIsAOf($levelRules, RectorInterface::class); + return $levelRules; } } diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index e72b66d021f..4065f3afeaa 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -6,6 +6,7 @@ use Nette\Utils\FileSystem; use Rector\Bridge\SetProviderCollector; +use Rector\Bridge\SetRectorsResolver; use Rector\Caching\Contract\ValueObject\Storage\CacheStorageInterface; use Rector\Config\Level\CodeQualityLevel; use Rector\Config\Level\DeadCodeLevel; @@ -154,6 +155,10 @@ final class RectorConfigBuilder */ private array $groupLoadedSets = []; + private ?bool $isWithPhpSetsUsed = null; + + private ?bool $isWithPhpLevelUsed = null; + public function __invoke(RectorConfig $rectorConfig): void { // @experimental 2024-06 @@ -169,6 +174,13 @@ public function __invoke(RectorConfig $rectorConfig): void $uniqueSets = array_unique($this->sets); + if ($this->isWithPhpLevelUsed && $this->isWithPhpSetsUsed) { + throw new InvalidConfigurationException(sprintf( + 'Your config uses "withPhp*()" and "withPhpLevel()" methods at the same time.%sPick one of them to avoid rules conflicts.', + PHP_EOL + )); + } + if (in_array(SetList::TYPE_DECLARATION, $uniqueSets, true) && $this->isTypeCoverageLevelUsed === true) { throw new InvalidConfigurationException(sprintf( 'Your config already enables type declarations set.%sRemove "->withTypeCoverageLevel()" as it only duplicates it, or remove type declaration set.', @@ -490,6 +502,8 @@ public function withPhpSets( bool $php53 = false, bool $php84 = false, // place on later as BC break when used in php 7.x without named arg ): self { + $this->isWithPhpSetsUsed = true; + $pickedArguments = array_filter(func_get_args()); if ($pickedArguments !== []) { Notifier::notifyWithPhpSetsNotSuitableForPHP80(); @@ -505,21 +519,9 @@ public function withPhpSets( } if ($pickedArguments === []) { - // use composer.json PHP version - $projectComposerJsonFilePath = getcwd() . '/composer.json'; - if (file_exists($projectComposerJsonFilePath)) { - $projectPhpVersion = ProjectComposerJsonPhpVersionResolver::resolve($projectComposerJsonFilePath); - if (is_int($projectPhpVersion)) { - $this->sets[] = PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); - - return $this; - } - } + $this->sets[] = $this->resolvePhpSetsFromComposerJsonPhpVersion(); - throw new InvalidConfigurationException(sprintf( - 'We could not find local "composer.json" to determine your PHP version.%sPlease, fill the PHP version set in withPhpSets() manually.', - PHP_EOL - )); + return $this; } if ($php53) { @@ -561,42 +563,56 @@ public function withPhpSets( */ public function withPhp53Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_53; return $this; } public function withPhp54Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_54; return $this; } public function withPhp55Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_55; return $this; } public function withPhp56Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_56; return $this; } public function withPhp70Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_70; return $this; } public function withPhp71Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_71; return $this; } public function withPhp72Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_72; return $this; } @@ -609,6 +625,8 @@ public function withPhp73Sets(): self public function withPhp74Sets(): self { + $this->isWithPhpSetsUsed = true; + $this->sets[] = LevelSetList::UP_TO_PHP_74; return $this; } @@ -877,7 +895,7 @@ public function withDeadCodeLevel(int $level): self $levelRules = LevelRulesResolver::resolve( $level, DeadCodeLevel::RULES, - 'RectorConfig::withDeadCodeLevel()' + 'RectorConfig::' . __METHOD__ . '()' ); $this->rules = array_merge($this->rules, $levelRules); @@ -896,7 +914,7 @@ public function withTypeCoverageLevel(int $level): self $levelRules = LevelRulesResolver::resolve( $level, TypeDeclarationLevel::RULES, - 'RectorConfig::withTypeCoverageLevel()' + 'RectorConfig::' . __METHOD__ . '()' ); $this->rules = array_merge($this->rules, $levelRules); @@ -904,6 +922,26 @@ public function withTypeCoverageLevel(int $level): self return $this; } + /** + * @experimental Since 1.2.5 Raise your PHP level from, one level at a time + */ + public function withPhpLevel(int $level): self + { + $this->isWithPhpLevelUsed = true; + + $phpLevelSetFilePath = $this->resolvePhpSetsFromComposerJsonPhpVersion(); + + $setRectorsResolver = new SetRectorsResolver(); + $phpRectorRules = $setRectorsResolver->resolveFromFilePathForPhpLevel($phpLevelSetFilePath); + + $levelRules = LevelRulesResolver::resolve($level, $phpRectorRules, 'RectorConfig::' . __METHOD__); + + // @todo this method conflicts with withPhpSets(), withPhpX(...) + $this->rules = array_merge($this->rules, $levelRules); + + return $this; + } + /** * @experimental Raise your code quality from the safest rules * to more affecting ones, one level at a time @@ -915,7 +953,7 @@ public function withCodeQualityLevel(int $level): self $levelRules = LevelRulesResolver::resolve( $level, CodeQualityLevel::RULES, - 'RectorConfig::withCodeQualityLevel()' + 'RectorConfig::' . __METHOD__ . '()' ); $this->rules = array_merge($this->rules, $levelRules); @@ -993,4 +1031,21 @@ public function withRealPathReporting(bool $absolutePath = true): self return $this; } + + private function resolvePhpSetsFromComposerJsonPhpVersion(): string + { + // use composer.json PHP version + $projectComposerJsonFilePath = getcwd() . '/composer.json'; + if (file_exists($projectComposerJsonFilePath)) { + $projectPhpVersion = ProjectComposerJsonPhpVersionResolver::resolve($projectComposerJsonFilePath); + if (is_int($projectPhpVersion)) { + return PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); + } + } + + throw new InvalidConfigurationException(sprintf( + 'We could not find local "composer.json" to determine your PHP version.%sPlease, fill the PHP version set in withPhpSets() manually.', + PHP_EOL + )); + } }