Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions scripts/generate-composer-based-trigger-packages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the script should be on github action with weekly run?

see https://github.com/rectorphp/rector-src/blob/main/.github/workflows/weekly_pull_requests.yaml

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can run it per commit, to fail if new package is there.
How fast it is?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run locally, seems fast enough: 0.253 seconds

➜  rector-src git:(composer-based-auto-detect) time php scripts/generate-composer-based-trigger-packages.php
twig: 1 packages
doctrine: 9 packages
phpunit: 1 packages
symfony: 56 packages
laravel: 2 packages
drupal: 1 packages

                                                                                                                        
 [OK] File "/Users/samsonasik/www/rector-src/src/Configuration/ComposerBasedTriggerPackages.php" was generated          
                                                                                                                        

0.19s user 0.03s system 87% cpu 0.253 total

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Configuration\Option;
use Rector\Configuration\Parameter\SimpleParameterProvider;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\Symfony\Set\TwigSetList;
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Style\SymfonyStyle;

require __DIR__ . '/../vendor/autoload.php';

// regenerates src/Configuration/ComposerBasedTriggerPackages.php from the packages the composer-based sets are bound to
$composerBasedSetFiles = [
'twig' => 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 = <<<PHP
<?php

declare(strict_types=1);

namespace Rector\Configuration;

/**
* Generated by "php scripts/generate-composer-based-trigger-packages.php", do not edit manually
*/
final class ComposerBasedTriggerPackages
{
/**
* Any of these installed packages loads the composer-based set of withComposerBased() argument
*
* @var array<string, string[]>
*/
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('<fg=green>%s</>: %d packages', $argumentName, count($packageNames)));
}

$symfonyStyle->success(sprintf('File "%s" was generated', realpath($targetFilePath)));
102 changes: 102 additions & 0 deletions src/Configuration/ComposerBasedTriggerPackages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<?php

declare(strict_types=1);

namespace Rector\Configuration;

/**
* Generated by "php scripts/generate-composer-based-trigger-packages.php", do not edit manually
*/
final class ComposerBasedTriggerPackages
{
/**
* Any of these installed packages loads the composer-based set of withComposerBased() argument
*
* @var array<string, string[]>
*/
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',
],
];
}
31 changes: 31 additions & 0 deletions src/Configuration/RectorConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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;
}
Expand Down Expand Up @@ -1219,4 +1228,26 @@ private function addLevelRules(

return $this;
}

/**
* @return array<string, bool> 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;
}
}
104 changes: 104 additions & 0 deletions tests/Configuration/ComposerBasedAutoDetectTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Configuration;

use PHPUnit\Framework\TestCase;
use Rector\Configuration\ComposerBasedTriggerPackages;
use Rector\Configuration\RectorConfigBuilder;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\Symfony\Set\TwigSetList;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;

final class ComposerBasedAutoDetectTest extends TestCase
{
private string $originalWorkingDirectory;

protected function setUp(): void
{
$this->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;
}
}
Loading
Loading