diff --git a/phpstan.neon b/phpstan.neon index b9bc6261d07..faf09550219 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -124,7 +124,7 @@ parameters: - '#Callable callable\(PHPStan\\Type\\Type\)\: PHPStan\\Type\\Type invoked with 2 parameters, 1 required#' # known value - - '#Method Rector\\Php\\PhpVersionProvider\:\:provide\(\) should return 50200\|50300\|50400\|50500\|50600\|70000\|70100\|70200\|70300\|70400\|80000\|80100\|80200\|80300\|80400\|100000 but returns int#' + - '#Method (.*?) should return 50200\|50300\|50400\|50500\|50600\|70000\|70100\|70200\|70300\|70400\|80000\|80100\|80200\|80300\|80400\|100000 but returns int#' - message: '#Function "class_exists\(\)" cannot be used/left in the code#' @@ -295,9 +295,6 @@ parameters: - '#Register "Rector\\Php71\\Rector\\ClassConst\\PublicConstantVisibilityRector" service to "php71\.php" config set#' - '#Public method "Rector\\ValueObject\\Error\\SystemError\:\:getFile\(\)" is never used#' - # known values - - '#Method Rector\\Util\\PhpVersionFactory\:\:createIntVersion\(\) should return 50200\|50300\|50400\|50500\|50600\|70000\|70100\|70200\|70300\|70400\|80000\|80100\|80200\|80300\|80400\|100000 but returns int#' - # soon to be used - '#Property Rector\\Configuration\\RectorConfigBuilder\:\:\$isWithPhpSetsUsed is never read, only written#' diff --git a/src/Bridge/SetRectorsResolver.php b/src/Bridge/SetRectorsResolver.php index 97003eb225d..eaa3a1a264c 100644 --- a/src/Bridge/SetRectorsResolver.php +++ b/src/Bridge/SetRectorsResolver.php @@ -6,20 +6,68 @@ use Rector\Config\RectorConfig; use Rector\Contract\Rector\RectorInterface; -use ReflectionProperty; use Webmozart\Assert\Assert; /** * @api * @experimental since 1.1.2 * Utils class to ease building bridges by 3rd-party tools + * + * @see \Rector\Tests\Bridge\SetRectorsResolverTest */ final class SetRectorsResolver { /** - * @return array> + * @param string[] $configFilePaths + * @return array|array, mixed[]>> */ - public function resolveFromFilePath(string $configFilePath): array + public function resolveFromFilePathsIncludingConfiguration(array $configFilePaths): array + { + Assert::allString($configFilePaths); + Assert::allFileExists($configFilePaths); + + $combinedRectorRulesWithConfiguration = []; + + foreach ($configFilePaths as $configFilePath) { + $rectorRulesWithConfiguration = $this->resolveFromFilePathIncludingConfiguration($configFilePath); + + $combinedRectorRulesWithConfiguration = array_merge( + $combinedRectorRulesWithConfiguration, + $rectorRulesWithConfiguration + ); + } + + return $combinedRectorRulesWithConfiguration; + } + + /** + * @return array|array, mixed[]>> + */ + public function resolveFromFilePathIncludingConfiguration(string $configFilePath): array + { + $rectorConfig = $this->loadRectorConfigFromFilePath($configFilePath); + + $rectorClassesWithOptionalConfiguration = $rectorConfig->getRectorClasses(); + + foreach ($rectorConfig->getRuleConfigurations() as $rectorClass => $configuration) { + // remove from non-configurable, if added again with better config + if (in_array($rectorClass, $rectorClassesWithOptionalConfiguration)) { + $rectorRulePosition = array_search($rectorClass, $rectorClassesWithOptionalConfiguration, true); + if (is_int($rectorRulePosition)) { + unset($rectorClassesWithOptionalConfiguration[$rectorRulePosition]); + } + } + + $rectorClassesWithOptionalConfiguration[] = [ + $rectorClass => $configuration, + ]; + } + + // sort keys + return array_values($rectorClassesWithOptionalConfiguration); + } + + private function loadRectorConfigFromFilePath(string $configFilePath): RectorConfig { Assert::fileExists($configFilePath); @@ -29,13 +77,6 @@ public function resolveFromFilePath(string $configFilePath): array $configCallable = require $configFilePath; $configCallable($rectorConfig); - // get tagged class-names - $tagsReflectionProperty = new ReflectionProperty($rectorConfig, 'tags'); - $tags = $tagsReflectionProperty->getValue($rectorConfig); - - $rectorClasses = $tags[RectorInterface::class] ?? []; - sort($rectorClasses); - - return array_unique($rectorClasses); + return $rectorConfig; } } diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 99476d431a1..6157d6ae430 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; @@ -160,6 +161,8 @@ final class RectorConfigBuilder */ private ?bool $isWithPhpSetsUsed = null; + private ?bool $isWithPhpLevelUsed = null; + public function __invoke(RectorConfig $rectorConfig): void { // @experimental 2024-06 @@ -175,6 +178,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 rule 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.', @@ -962,6 +972,41 @@ 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 + { + Assert::natural($level); + + $this->isWithPhpLevelUsed = true; + + $phpVersion = ComposerJsonPhpVersionResolver::resolveFromCwdOrFail(); + + $setRectorsResolver = new SetRectorsResolver(); + $setFilePaths = PhpLevelSetResolver::resolveFromPhpVersion($phpVersion); + + $rectorRulesWithConfiguration = $setRectorsResolver->resolveFromFilePathsIncludingConfiguration($setFilePaths); + + foreach ($rectorRulesWithConfiguration as $position => $rectorRuleWithConfiguration) { + // add rules untill level is reached + if ($position > $level) { + continue; + } + + if (is_string($rectorRuleWithConfiguration)) { + $this->rules[] = $rectorRuleWithConfiguration; + } elseif (is_array($rectorRuleWithConfiguration)) { + foreach ($rectorRuleWithConfiguration as $rectorRule => $rectorRuleConfiguration) { + /** @var class-string $rectorRule */ + $this->withConfiguredRule($rectorRule, $rectorRuleConfiguration); + } + } + } + + return $this; + } + /** * @experimental Raise your code quality from the safest rules * to more affecting ones, one level at a time diff --git a/tests/Bridge/Fixture/some-composer.json b/tests/Bridge/Fixture/some-composer.json new file mode 100644 index 00000000000..7f681ca9f0d --- /dev/null +++ b/tests/Bridge/Fixture/some-composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "php": "^7.3" + } +} diff --git a/tests/Bridge/SetRectorsResolverTest.php b/tests/Bridge/SetRectorsResolverTest.php new file mode 100644 index 00000000000..2ae8365e9c7 --- /dev/null +++ b/tests/Bridge/SetRectorsResolverTest.php @@ -0,0 +1,74 @@ +setRectorsResolver = new SetRectorsResolver(); + } + + public function testResolveFromFilePathForPhpVersion(): void + { + $configFilePaths = PhpLevelSetResolver::resolveFromPhpVersion(PhpVersion::PHP_70); + $this->assertCount(6, $configFilePaths); + $this->assertContainsOnly('string', $configFilePaths); + + foreach ($configFilePaths as $configFilePath) { + $this->assertFileExists($configFilePath); + } + } + + public function testResolveFromFilePathForPhpLevel(): void + { + $projectPhpVersion = ComposerJsonPhpVersionResolver::resolve(__DIR__ . '/Fixture/some-composer.json'); + + $this->assertIsInt($projectPhpVersion); + $this->assertSame(PhpVersion::PHP_73, $projectPhpVersion); + + $configFilePaths = PhpLevelSetResolver::resolveFromPhpVersion($projectPhpVersion); + $this->assertCount(9, $configFilePaths); + + $rectorRulesWithConfiguration = $this->setRectorsResolver->resolveFromFilePathsIncludingConfiguration( + $configFilePaths + ); + $this->assertCount(63, $rectorRulesWithConfiguration); + } + + public function testResolveWithConfiguration(): void + { + $rectorRulesWithConfiguration = $this->setRectorsResolver->resolveFromFilePathIncludingConfiguration( + SetList::PHP_73 + ); + $this->assertCount(10, $rectorRulesWithConfiguration); + + $this->assertArrayHasKey(0, $rectorRulesWithConfiguration); + $this->assertArrayHasKey(9, $rectorRulesWithConfiguration); + + foreach ($rectorRulesWithConfiguration as $rectorRuleWithConfiguration) { + if (is_string($rectorRuleWithConfiguration)) { + $this->assertTrue(is_a($rectorRuleWithConfiguration, RectorInterface::class, true)); + } + + if (is_array($rectorRuleWithConfiguration)) { + foreach ($rectorRuleWithConfiguration as $rectorRule => $rectorRuleConfiguration) { + $this->assertTrue(is_a($rectorRule, RectorInterface::class, true)); + $this->assertIsArray($rectorRuleConfiguration); + } + } + } + } +}