diff --git a/scripts/generate-composer-based-trigger-packages.php b/scripts/generate-composer-based-trigger-packages.php new file mode 100644 index 00000000000..986a9f59c25 --- /dev/null +++ b/scripts/generate-composer-based-trigger-packages.php @@ -0,0 +1,132 @@ + TwigSetList::COMPOSER_BASED, + 'doctrine' => DoctrineSetList::COMPOSER_BASED, + 'phpunit' => PHPUnitSetList::COMPOSER_BASED, + 'symfony' => SymfonySetList::COMPOSER_BASED, +]; + +// rector-laravel and drupal-rector are not required here, so their sets cannot be read +$extensionTriggerPackages = [ + 'laravel' => ['laravel/framework', 'illuminate/support'], + 'drupal' => ['drupal/core'], +]; + +$triggerPackages = []; + +foreach ($composerBasedSetFiles as $argumentName => $composerBasedSetFile) { + // the parameter provider is static, start each set from scratch + SimpleParameterProvider::setParameter(Option::COMPOSER_BOUND_RULE_CONFIGURATIONS, []); + + $rectorConfig = new RectorConfig(); + + /** @var callable $configCallable */ + $configCallable = require $composerBasedSetFile; + $configCallable($rectorConfig); + + $rectorClasses = array_merge( + $rectorConfig->getMainRectorClasses(), + array_keys($rectorConfig->getRuleConfigurations()) + ); + + $packageNames = []; + + foreach (array_unique($rectorClasses) as $rectorClass) { + // the package constraint does not depend on constructor dependencies, skip them + $rector = new ReflectionClass($rectorClass)->newInstanceWithoutConstructor(); + if (! $rector instanceof ComposerPackageConstraintInterface) { + continue; + } + + $composerPackageConstraint = $rector->provideComposerPackageConstraint(); + $composerPackageConstraints = is_array($composerPackageConstraint) + ? $composerPackageConstraint + : [$composerPackageConstraint]; + + foreach ($composerPackageConstraints as $composerPackageConstraint) { + $packageNames[] = $composerPackageConstraint->getPackageName(); + } + } + + // rule configurations registered via ruleWithConfigurationComposerVersionBound(), the inactive ones included + $composerBoundRuleConfigurations = SimpleParameterProvider::provideArrayParameter( + Option::COMPOSER_BOUND_RULE_CONFIGURATIONS + ); + + foreach ($composerBoundRuleConfigurations as $composerBoundRuleConfiguration) { + if ($composerBoundRuleConfiguration instanceof ComposerBoundRuleConfiguration) { + $packageNames[] = $composerBoundRuleConfiguration->getPackageName(); + } + } + + $packageNames = array_values(array_unique($packageNames)); + sort($packageNames); + + $triggerPackages[$argumentName] = $packageNames; +} + +$triggerPackages = array_merge($triggerPackages, $extensionTriggerPackages); + +$printedTriggerPackages = ''; +foreach ($triggerPackages as $argumentName => $packageNames) { + $printedTriggerPackages .= sprintf(" '%s' => [\n", $argumentName); + + foreach ($packageNames as $packageName) { + $printedTriggerPackages .= sprintf(" '%s',\n", $packageName); + } + + $printedTriggerPackages .= " ],\n"; +} + +$fileContents = << + */ + public const array PACKAGES_BY_ARGUMENT = [ +{$printedTriggerPackages} ]; +} + +PHP; + +$targetFilePath = __DIR__ . '/../src/Configuration/ComposerBasedTriggerPackages.php'; +file_put_contents($targetFilePath, $fileContents); + +$symfonyStyle = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); + +foreach ($triggerPackages as $argumentName => $packageNames) { + $symfonyStyle->writeln(sprintf('%s: %d packages', $argumentName, count($packageNames))); +} + +$symfonyStyle->success(sprintf('File "%s" was generated', realpath($targetFilePath))); diff --git a/src/Configuration/ComposerBasedTriggerPackages.php b/src/Configuration/ComposerBasedTriggerPackages.php new file mode 100644 index 00000000000..ae1c4e57ff7 --- /dev/null +++ b/src/Configuration/ComposerBasedTriggerPackages.php @@ -0,0 +1,102 @@ + + */ + public const array PACKAGES_BY_ARGUMENT = [ + 'twig' => [ + 'twig/twig', + ], + 'doctrine' => [ + 'doctrine/collections', + 'doctrine/common', + 'doctrine/data-fixtures', + 'doctrine/dbal', + 'doctrine/doctrine-bundle', + 'doctrine/mongodb-odm', + 'doctrine/mongodb-odm-bundle', + 'doctrine/orm', + 'gedmo/doctrine-extensions', + ], + 'phpunit' => [ + 'phpunit/phpunit', + ], + 'symfony' => [ + 'jms/serializer', + 'symfony/bridge-swift-mailer', + 'symfony/browser-kit', + 'symfony/cache', + 'symfony/class-loader', + 'symfony/config', + 'symfony/console', + 'symfony/contracts', + 'symfony/debug', + 'symfony/dependency-injection', + 'symfony/doctrine-bridge', + 'symfony/dom-crawler', + 'symfony/error-handler', + 'symfony/event-dispatcher', + 'symfony/expression-language', + 'symfony/filesystem', + 'symfony/finder', + 'symfony/form', + 'symfony/framework-bundle', + 'symfony/http-client', + 'symfony/http-foundation', + 'symfony/http-kernel', + 'symfony/inflector', + 'symfony/intl', + 'symfony/json-streamer', + 'symfony/mail-pace-mailer', + 'symfony/mailer', + 'symfony/messenger', + 'symfony/mime', + 'symfony/monolog-bridge', + 'symfony/notifier', + 'symfony/options-resolver', + 'symfony/process', + 'symfony/property-access', + 'symfony/property-info', + 'symfony/routing', + 'symfony/scheduler', + 'symfony/security', + 'symfony/security-bundle', + 'symfony/security-core', + 'symfony/security-http', + 'symfony/security-mailer', + 'symfony/serializer', + 'symfony/symfony', + 'symfony/templating', + 'symfony/translation', + 'symfony/translation-contracts', + 'symfony/twig-bridge', + 'symfony/twig-bundle', + 'symfony/type-info', + 'symfony/uid', + 'symfony/validator', + 'symfony/var-dumper', + 'symfony/web-link', + 'symfony/workflow', + 'symfony/yaml', + 'twig/twig', + ], + 'laravel' => [ + 'laravel/framework', + 'illuminate/support', + ], + 'drupal' => [ + 'drupal/core', + ], + ]; +} diff --git a/src/Configuration/RectorConfigBuilder.php b/src/Configuration/RectorConfigBuilder.php index 622a72fc529..6de1e3de3ef 100644 --- a/src/Configuration/RectorConfigBuilder.php +++ b/src/Configuration/RectorConfigBuilder.php @@ -10,6 +10,7 @@ use PhpParser\NodeVisitor; use Rector\Bridge\SetRectorsResolver; use Rector\Caching\Contract\ValueObject\Storage\CacheStorageInterface; +use Rector\Composer\InstalledPackageResolver; use Rector\Config\Level\CodeQualityLevel; use Rector\Config\Level\CodingStyleLevel; use Rector\Config\Level\DeadCodeLevel; @@ -704,6 +705,10 @@ public function withPreparedSets( return $this; } + /** + * Call it with no argument to load the sets of every package installed in the project. + * Any argument switches the detection off, and only the passed sets are loaded. + */ public function withComposerBased( bool $twig = false, bool $doctrine = false, @@ -713,6 +718,10 @@ public function withComposerBased( bool $laravel = false, bool $drupal = false, ): self { + if (func_num_args() === 0) { + return $this->withComposerBased(...$this->resolveInstalledComposerBasedPackages()); + } + if ($laravel && class_exists('RectorLaravel\Set\LaravelSetList') && constant('RectorLaravel\Set\LaravelSetList::COMPOSER_BASED')) { $this->sets[] = LaravelSetList::COMPOSER_BASED; } @@ -1219,4 +1228,26 @@ private function addLevelRules( return $this; } + + /** + * @return array withComposerBased() argument name => is any of its packages installed + */ + private function resolveInstalledComposerBasedPackages(): array + { + $installedPackageResolver = new InstalledPackageResolver(); + + $installedPackages = []; + foreach (ComposerBasedTriggerPackages::PACKAGES_BY_ARGUMENT as $argumentName => $packageNames) { + $installedPackages[$argumentName] = false; + + foreach ($packageNames as $packageName) { + if ($installedPackageResolver->resolvePackageVersion($packageName) !== null) { + $installedPackages[$argumentName] = true; + break; + } + } + } + + return $installedPackages; + } } diff --git a/tests/Configuration/ComposerBasedAutoDetectTest.php b/tests/Configuration/ComposerBasedAutoDetectTest.php new file mode 100644 index 00000000000..5b58a87e762 --- /dev/null +++ b/tests/Configuration/ComposerBasedAutoDetectTest.php @@ -0,0 +1,104 @@ +originalWorkingDirectory = (string) getcwd(); + $this->changeProjectDirectory('installed_packages'); + } + + protected function tearDown(): void + { + chdir($this->originalWorkingDirectory); + } + + /** + * doctrine/dbal and symfony/console are enough to load their sets, no doctrine/orm or symfony/framework-bundle needed + */ + public function testNoArgumentLoadsSetsOfInstalledPackages(): void + { + $rectorConfigBuilder = new RectorConfigBuilder() + ->withComposerBased(); + + $this->assertSame( + [PHPUnitSetList::COMPOSER_BASED, DoctrineSetList::COMPOSER_BASED, SymfonySetList::COMPOSER_BASED], + $this->readSets($rectorConfigBuilder) + ); + } + + public function testNoArgumentWithoutTriggerPackagesLoadsNothing(): void + { + $this->changeProjectDirectory('no_trigger_packages'); + + $rectorConfigBuilder = new RectorConfigBuilder() + ->withComposerBased(); + + $this->assertSame([], $this->readSets($rectorConfigBuilder)); + } + + public function testExplicitArgumentSkipsDetection(): void + { + $rectorConfigBuilder = new RectorConfigBuilder() + ->withComposerBased(twig: true); + + $this->assertSame([TwigSetList::COMPOSER_BASED], $this->readSets($rectorConfigBuilder)); + } + + public function testExplicitFalseArgumentSkipsDetection(): void + { + $rectorConfigBuilder = new RectorConfigBuilder() + ->withComposerBased(phpunit: false); + + $this->assertSame([], $this->readSets($rectorConfigBuilder)); + } + + public function testEveryArgumentHasTriggerPackages(): void + { + $argumentNames = array_map( + static fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(), + new ReflectionMethod(RectorConfigBuilder::class, 'withComposerBased')->getParameters() + ); + + // deprecated, no longer applied + $argumentNames = array_values(array_diff($argumentNames, ['netteUtils'])); + + $this->assertSame($argumentNames, array_keys(ComposerBasedTriggerPackages::PACKAGES_BY_ARGUMENT)); + } + + private function changeProjectDirectory(string $projectName): void + { + chdir(__DIR__ . '/Fixture/ComposerBasedAutoDetect/' . $projectName); + } + + /** + * @return string[] + */ + private function readSets(RectorConfigBuilder $rectorConfigBuilder): array + { + $sets = new ReflectionClass($rectorConfigBuilder) + ->getProperty('sets') + ->getValue($rectorConfigBuilder); + + $this->assertIsArray($sets); + + return $sets; + } +} diff --git a/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/composer.json b/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/composer.json new file mode 100644 index 00000000000..4d26cee8ce3 --- /dev/null +++ b/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/composer.json @@ -0,0 +1,8 @@ +{ + "type": "project", + "require": { + "doctrine/dbal": "^4.0", + "phpunit/phpunit": "^12.0", + "symfony/console": "^7.0" + } +} diff --git a/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/vendor/composer/installed.json b/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/vendor/composer/installed.json new file mode 100644 index 00000000000..2f377da8d21 --- /dev/null +++ b/tests/Configuration/Fixture/ComposerBasedAutoDetect/installed_packages/vendor/composer/installed.json @@ -0,0 +1,19 @@ +{ + "packages": [ + { + "name": "doctrine/dbal", + "version": "4.2.0", + "version_normalized": "4.2.0.0" + }, + { + "name": "phpunit/phpunit", + "version": "12.1.0", + "version_normalized": "12.1.0.0" + }, + { + "name": "symfony/console", + "version": "v7.2.0", + "version_normalized": "7.2.0.0" + } + ] +} diff --git a/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/composer.json b/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/composer.json new file mode 100644 index 00000000000..408b1c1d95e --- /dev/null +++ b/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/composer.json @@ -0,0 +1,6 @@ +{ + "type": "project", + "require": { + "webmozart/assert": "^1.11" + } +} diff --git a/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/vendor/composer/installed.json b/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/vendor/composer/installed.json new file mode 100644 index 00000000000..8d81601582a --- /dev/null +++ b/tests/Configuration/Fixture/ComposerBasedAutoDetect/no_trigger_packages/vendor/composer/installed.json @@ -0,0 +1,9 @@ +{ + "packages": [ + { + "name": "webmozart/assert", + "version": "1.11.0", + "version_normalized": "1.11.0.0" + } + ] +}