Skip to content

Commit

Permalink
[DX] Introduce set providers, to enable package + version based set r…
Browse files Browse the repository at this point in the history
…egistration (#5976)

* kick of set provider

* should not happen must have string message

* add TwigSetProvider

* register twig set providers on load

* polyfill is internal set

* add InstalledPackageResolver

* add simple test for SetCollector

* add simple InstalledPackageResolverTest

* use SetManager name as more features

* add cache
  • Loading branch information
TomasVotruba authored Jun 20, 2024
1 parent f366a7e commit 2dda748
Show file tree
Hide file tree
Showing 15 changed files with 396 additions and 3 deletions.
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ parameters:
# dev rule
- '#Class "Rector\\Utils\\Rector\\MoveAbstractRectorToChildrenRector" is missing @see annotation with test case class reference#'

# to be extended
- '#Interface "Rector\\Set\\Contract\\SetInterface" has only single implementer\. Consider using the class directly as there is no point in using the interface#'
- '#Interface "Rector\\Set\\Contract\\SetProviderInterface" has only single implementer\. Consider using the class directly as there is no point in using the interface#'

# optional as changes behavior, should be used explicitly outside PHP upgrade
- '#Register "Rector\\Php73\\Rector\\FuncCall\\JsonThrowOnErrorRector" service to "php73\.php" config set#'

Expand Down
2 changes: 2 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
earlyReturn: true,
naming: true,
rectorPreset: true,
// @experimental 2024-06
// twig: false,
)
->withPhpSets()
->withPaths([
Expand Down
59 changes: 59 additions & 0 deletions src/Composer/InstalledPackageResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Rector\Composer;

use Nette\Utils\FileSystem;
use Nette\Utils\Json;
use Rector\Composer\ValueObject\InstalledPackage;
use Rector\Exception\ShouldNotHappenException;
use Webmozart\Assert\Assert;

/**
* @see \Rector\Tests\Composer\InstalledPackageResolverTest
*/
final class InstalledPackageResolver
{
/**
* @var array<string, InstalledPackage[]>
*/
private array $resolvedInstalledPackages = [];


/**
* @return InstalledPackage[]
*/
public function resolve(string $projectDirectory): array
{
// cache
if (isset($this->resolvedInstalledPackages[$projectDirectory])) {
return $this->resolvedInstalledPackages[$projectDirectory];
}

Assert::directory($projectDirectory);

$installedPackagesFilePath = $projectDirectory . '/vendor/composer/installed.json';
if (! file_exists($installedPackagesFilePath)) {
throw new ShouldNotHappenException(
'The installed package json not found. Make sure you run `composer update` and "vendor/composer/installed.json" file exists'
);
}

$installedPackageFileContents = FileSystem::read($installedPackagesFilePath);
$installedPackagesFilePath = Json::decode($installedPackageFileContents, true);

$installedPackages = [];

foreach ($installedPackagesFilePath['packages'] as $installedPackage) {
$installedPackages[] = new InstalledPackage(
$installedPackage['name'],
$installedPackage['version_normalized']
);
}

$this->resolvedInstalledPackages[$projectDirectory] = $installedPackages;

return $installedPackages;
}
}
24 changes: 24 additions & 0 deletions src/Composer/ValueObject/InstalledPackage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace Rector\Composer\ValueObject;

final readonly class InstalledPackage
{
public function __construct(
private string $name,
private string $version,
) {
}

public function getName(): string
{
return $this->name;
}

public function getVersion(): string
{
return $this->version;
}
}
6 changes: 5 additions & 1 deletion src/Config/RectorConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Rector\Contract\Rector\RectorInterface;
use Rector\DependencyInjection\Laravel\ContainerMemento;
use Rector\Exception\ShouldNotHappenException;
use Rector\Set\SetProvider\TwigSetProvider;
use Rector\Skipper\SkipCriteriaResolver\SkippedClassResolver;
use Rector\Validation\RectorConfigValidator;
use Rector\ValueObject\PhpVersion;
Expand Down Expand Up @@ -41,7 +42,10 @@ final class RectorConfig extends Container

public static function configure(): RectorConfigBuilder
{
return new RectorConfigBuilder();
$rectorConfigBuilder = new RectorConfigBuilder();
$rectorConfigBuilder->withSetProviders([new TwigSetProvider()]);

return $rectorConfigBuilder;
}

/**
Expand Down
49 changes: 47 additions & 2 deletions src/Configuration/RectorConfigBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,12 @@
use Rector\Contract\Rector\RectorInterface;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Exception\Configuration\InvalidConfigurationException;
use Rector\Exception\ShouldNotHappenException;
use Rector\Php\PhpVersionResolver\ProjectComposerJsonPhpVersionResolver;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\Enum\SetGroup;
use Rector\Set\SetManager;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\Symfony\Set\FOSRestSetList;
Expand Down Expand Up @@ -137,8 +141,39 @@ final class RectorConfigBuilder
*/
private array $registerServices = [];

/**
* @var SetProviderInterface[]
*/
private array $setProviders = [];

/**
* @var array<SetGroup::*>
*/
private array $setGroups = [];

/**
* @var string[]
*/
private array $groupLoadedSets = [];

public function __invoke(RectorConfig $rectorConfig): void
{
// @experimental 2024-06
if ($this->setGroups !== []) {
if ($this->setProviders === []) {
throw new ShouldNotHappenException(sprintf(
'Register set providers first, as they are required for dynamic sets: "%s"',
implode('", "', $this->setGroups)
));
}

$setManager = new SetManager($this->setProviders);
$this->groupLoadedSets = $setManager->matchBySetGroups($this->setGroups);
}

// merge sets together
$this->sets = array_merge($this->sets, $this->groupLoadedSets);

$uniqueSets = array_unique($this->sets);

if (in_array(SetList::TYPE_DECLARATION, $uniqueSets, true) && $this->isTypeCoverageLevelUsed === true) {
Expand Down Expand Up @@ -550,6 +585,16 @@ public function withPhp74Sets(): self
// there is no withPhp80Sets() and above,
// as we already use PHP 8.0 and should go with withPhpSets() instead

/**
* @param SetProviderInterface[] $setProviders
*/
public function withSetProviders(array $setProviders): self
{
$this->setProviders = array_merge($this->setProviders, $setProviders);

return $this;
}

public function withPreparedSets(
bool $deadCode = false,
bool $codeQuality = false,
Expand Down Expand Up @@ -611,9 +656,9 @@ public function withPreparedSets(
$this->sets[] = SetList::RECTOR_PRESET;
}

// @experimental 2024-06
if ($twig) {
// resolve sets based on composer.json versions
// @todo
$this->setGroups[] = SetGroup::TWIG;
}

return $this;
Expand Down
9 changes: 9 additions & 0 deletions src/Set/Contract/SetInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Rector\Set\Contract;

interface SetInterface
{
}
13 changes: 13 additions & 0 deletions src/Set/Contract/SetProviderInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Rector\Set\Contract;

interface SetProviderInterface
{
/**
* @return SetInterface[]
*/
public function provide(): array;
}
10 changes: 10 additions & 0 deletions src/Set/Enum/SetGroup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Rector\Set\Enum;

final class SetGroup
{
public const TWIG = 'twig';
}
75 changes: 75 additions & 0 deletions src/Set/SetManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Rector\Set;

use Rector\Composer\InstalledPackageResolver;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\ValueObject\ComposerTriggeredSet;
use Webmozart\Assert\Assert;

/**
* @see \Rector\Tests\Set\SetCollector\SetCollectorTest
*/
final readonly class SetManager
{
/**
* @param SetProviderInterface[] $setProviders
*/
public function __construct(
private array $setProviders
) {
Assert::allIsInstanceOf($setProviders, SetProviderInterface::class);
}

/**
* @return ComposerTriggeredSet[]
*/
public function matchComposerTriggered(string $groupName): array
{
$matchedSets = [];

foreach ($this->setProviders as $setProvider) {
foreach ($setProvider->provide() as $set) {
if (! $set instanceof ComposerTriggeredSet) {
continue;
}

if ($set->getGroupName() === $groupName) {
$matchedSets[] = $set;
}
}
}

return $matchedSets;
}

/**
* @param string[] $setGroups
* @return string[]
*/
public function matchBySetGroups(array $setGroups): array
{
$installedPackageResolver = new InstalledPackageResolver();
$installedComposerPackages = $installedPackageResolver->resolve(getcwd());

$groupLoadedSets = [];

foreach ($setGroups as $setGroup) {
$composerTriggeredSets = $this->matchComposerTriggered($setGroup);

foreach ($composerTriggeredSets as $composerTriggeredSet) {
if ($composerTriggeredSet->matchInstalledPackages($installedComposerPackages)) {
// @todo add debug note somewhere
// echo sprintf('Loaded "%s" set as it meets the conditions', $composerTriggeredSet->getSetFilePath());

// it matched composer package + version requirements → load set
$groupLoadedSets[] = $composerTriggeredSet->getSetFilePath();
}
}
}

return $groupLoadedSets;
}
}
27 changes: 27 additions & 0 deletions src/Set/SetProvider/TwigSetProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Rector\Set\SetProvider;

use Rector\Set\Contract\SetInterface;
use Rector\Set\Contract\SetProviderInterface;
use Rector\Set\Enum\SetGroup;
use Rector\Set\ValueObject\ComposerTriggeredSet;
use Rector\Symfony\Set\TwigSetList;

/**
* Temporary location, move to rector-symfony package once this is merged
* @experimental 2024-06
*/
final class TwigSetProvider implements SetProviderInterface
{
/**
* @return SetInterface[]
*/
public function provide(): array
{
// @todo temporary name to test, these will be located in rector-symfony, rector-doctrine, rector-phpunit packages
return [new ComposerTriggeredSet(SetGroup::TWIG, 'twig/twig', '1.12', TwigSetList::TWIG_112)];
}
}
Loading

0 comments on commit 2dda748

Please sign in to comment.