Skip to content

Commit 00549b3

Browse files
committed
[composer-based] Register each rule only once, add --composer-based process filter
A rule registered by multiple sets was tagged repeatedly, so the same singleton was handed out N times: run N times per node, and listed N times in reports. Also adds the --composer-based process option and the composer-bound rule configuration report.
1 parent 7442c9e commit 00549b3

10 files changed

Lines changed: 355 additions & 17 deletions

File tree

‎src/Config/RectorConfig.php‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Rector\ValueObject\Configuration\LevelOverflow;
2626
use Rector\ValueObject\PhpVersion;
2727
use Rector\ValueObject\PolyfillPackage;
28+
use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration;
2829
use Symfony\Component\Console\Command\Command;
2930
use Webmozart\Assert\Assert;
3031

@@ -39,6 +40,16 @@ final class RectorConfig extends Container
3940
*/
4041
private array $ruleConfigurations = [];
4142

43+
/**
44+
* @var array<class-string<RectorInterface>, true>
45+
*/
46+
private array $registeredRectorClasses = [];
47+
48+
/**
49+
* @var array<string, true>
50+
*/
51+
private array $registeredComposerBoundRuleConfigurations = [];
52+
4253
/**
4354
* @var string[]
4455
*/
@@ -194,9 +205,6 @@ public function ruleWithConfiguration(string $rectorClass, array $configuration)
194205
$ruleConfiguration = $this->ruleConfigurations[$rectorClass];
195206
$configurableRector->configure($ruleConfiguration);
196207
});
197-
198-
// for cache invalidation in case of sets change
199-
SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass);
200208
}
201209

202210
/**
@@ -214,11 +222,29 @@ public function ruleWithConfigurationComposerVersionBound(
214222
string $versionConstraint
215223
): void {
216224
$packageVersion = $this->resolveInstalledPackageVersion($packageName);
217-
if ($packageVersion === null) {
218-
return;
225+
$isActive = $packageVersion !== null && Semver::satisfies($packageVersion, $versionConstraint);
226+
227+
// the same rule configuration can be registered by multiple sets, report it only once
228+
$configurationKey = $rectorClass . '|' . $packageName . '|' . $versionConstraint . '|' . serialize(
229+
$configuration
230+
);
231+
232+
if (! isset($this->registeredComposerBoundRuleConfigurations[$configurationKey])) {
233+
$this->registeredComposerBoundRuleConfigurations[$configurationKey] = true;
234+
235+
// reported by the "composer-based" command, the inactive ones as well
236+
SimpleParameterProvider::addParameter(Option::COMPOSER_BOUND_RULE_CONFIGURATIONS, [
237+
new ComposerBoundRuleConfiguration(
238+
$rectorClass,
239+
$packageName,
240+
$versionConstraint,
241+
$configuration,
242+
$isActive
243+
),
244+
]);
219245
}
220246

221-
if (! Semver::satisfies($packageVersion, $versionConstraint)) {
247+
if (! $isActive) {
222248
return;
223249
}
224250

@@ -234,10 +260,17 @@ public function rule(string $rectorClass): void
234260
Assert::isAOf($rectorClass, RectorInterface::class);
235261

236262
$this->singleton($rectorClass);
237-
$this->tag($rectorClass, RectorInterface::class);
238263

239-
// for cache invalidation in case of change
240-
SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass);
264+
// the same rule can be registered by multiple sets, tag it only once,
265+
// otherwise it is run twice on every node and listed twice in the reports
266+
if (! isset($this->registeredRectorClasses[$rectorClass])) {
267+
$this->registeredRectorClasses[$rectorClass] = true;
268+
269+
$this->tag($rectorClass, RectorInterface::class);
270+
271+
// for cache invalidation in case of change
272+
SimpleParameterProvider::addParameter(Option::REGISTERED_RECTOR_RULES, $rectorClass);
273+
}
241274

242275
if (is_a($rectorClass, RelatedConfigInterface::class, true)) {
243276
$configFile = $rectorClass::getConfigFile();
@@ -432,6 +465,8 @@ public function indent(string $character, int $count): void
432465
public function resetRuleConfigurations(): void
433466
{
434467
$this->ruleConfigurations = [];
468+
$this->registeredRectorClasses = [];
469+
$this->registeredComposerBoundRuleConfigurations = [];
435470
}
436471

437472
/**

‎src/Configuration/ConfigurationFactory.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ public function createFromInput(InputInterface $input): Configuration
9595

9696
$showRulesSummary = (bool) $input->getOption(Option::RULES_SUMMARY);
9797

98+
$isComposerBased = (bool) $input->getOption(Option::COMPOSER_BASED);
99+
100+
// "--composer-based" narrows the run the same way "--only" does
101+
if ($isComposerBased) {
102+
SimpleParameterProvider::setParameter(Option::IS_RUN_NARROWED, true);
103+
}
104+
98105
return new Configuration(
99106
$isDryRun,
100107
$showProgressBar,
@@ -113,6 +120,7 @@ public function createFromInput(InputInterface $input): Configuration
113120
$onlySuffix,
114121
$levelOverflows,
115122
$showRulesSummary,
123+
$isComposerBased,
116124
);
117125
}
118126

‎src/Configuration/ConfigurationRuleFilter.php‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace Rector\Configuration;
66

7+
use Rector\Configuration\Parameter\SimpleParameterProvider;
78
use Rector\Contract\Rector\RectorInterface;
89
use Rector\ValueObject\Configuration;
10+
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
11+
use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration;
912

1013
/**
1114
* Modify available rector rules based on the configuration options
@@ -34,6 +37,10 @@ public function filter(array $rectors): array
3437
return $this->filterOnlyRule($rectors, $onlyRule);
3538
}
3639

40+
if ($this->configuration->isComposerBased()) {
41+
return $this->filterComposerBased($rectors);
42+
}
43+
3744
return $rectors;
3845
}
3946

@@ -52,4 +59,55 @@ public function filterOnlyRule(array $rectors, string $onlyRule): array
5259

5360
return $activeRectors;
5461
}
62+
63+
/**
64+
* Keeps rules that declare a composer package constraint themselves, and rules whose configuration
65+
* was registered with a composer package constraint.
66+
*
67+
* @param list<RectorInterface> $rectors
68+
* @return list<RectorInterface>
69+
*/
70+
private function filterComposerBased(array $rectors): array
71+
{
72+
$composerBoundRectorClasses = $this->resolveComposerBoundRectorClasses();
73+
74+
$activeRectors = [];
75+
foreach ($rectors as $rector) {
76+
if ($rector instanceof ComposerPackageConstraintInterface) {
77+
$activeRectors[] = $rector;
78+
continue;
79+
}
80+
81+
if (in_array($rector::class, $composerBoundRectorClasses, true)) {
82+
$activeRectors[] = $rector;
83+
}
84+
}
85+
86+
return $activeRectors;
87+
}
88+
89+
/**
90+
* @return string[]
91+
*/
92+
private function resolveComposerBoundRectorClasses(): array
93+
{
94+
$composerBoundRuleConfigurations = SimpleParameterProvider::provideArrayParameter(
95+
Option::COMPOSER_BOUND_RULE_CONFIGURATIONS
96+
);
97+
98+
$rectorClasses = [];
99+
foreach ($composerBoundRuleConfigurations as $composerBoundRuleConfiguration) {
100+
if (! $composerBoundRuleConfiguration instanceof ComposerBoundRuleConfiguration) {
101+
continue;
102+
}
103+
104+
if (! $composerBoundRuleConfiguration->isActive()) {
105+
continue;
106+
}
107+
108+
$rectorClasses[] = $composerBoundRuleConfiguration->getRectorClass();
109+
}
110+
111+
return $rectorClasses;
112+
}
55113
}

‎src/Configuration/Option.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,17 @@ final class Option
261261
*/
262262
public const string COMPOSER_BASED_SETS = 'composer_based_sets';
263263

264+
/**
265+
* @internal To report rule configuration bound to an installed package version
266+
* @see \Rector\Config\RectorConfig::ruleWithConfigurationComposerVersionBound()
267+
*/
268+
public const string COMPOSER_BOUND_RULE_CONFIGURATIONS = 'composer_bound_rule_configurations';
269+
270+
/**
271+
* Run only rules bound to an installed composer package version
272+
*/
273+
public const string COMPOSER_BASED = 'composer-based';
274+
264275
/**
265276
* @internal To filter files by specific suffix
266277
*/

‎src/Console/Command/ComposerBasedCommand.php‎

Lines changed: 105 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
namespace Rector\Console\Command;
66

77
use Composer\Semver\Semver;
8+
use Nette\Utils\Strings;
89
use Rector\Composer\InstalledPackageResolver;
10+
use Rector\Configuration\Option;
11+
use Rector\Configuration\Parameter\SimpleParameterProvider;
912
use Rector\Contract\Rector\RectorInterface;
1013
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
14+
use Rector\VersionBonding\ValueObject\ComposerBoundRuleConfiguration;
15+
use ReflectionObject;
1116
use Symfony\Component\Console\Command\Command;
1217
use Symfony\Component\Console\Input\InputInterface;
1318
use Symfony\Component\Console\Output\OutputInterface;
@@ -38,20 +43,32 @@ protected function configure(): void
3843
protected function execute(InputInterface $input, OutputInterface $output): int
3944
{
4045
$tableRows = $this->createTableRows();
46+
$configurationTableRows = $this->createConfigurationTableRows();
4147

42-
if ($tableRows === []) {
48+
if ($tableRows === [] && $configurationTableRows === []) {
4349
$this->symfonyStyle->warning('No composer package bound rule is loaded');
4450

4551
return Command::SUCCESS;
4652
}
4753

48-
$this->symfonyStyle->title('Composer package bound rules');
49-
$this->symfonyStyle->table(['Rule', 'Package', 'Requires', 'Installed', 'Active'], $tableRows);
54+
if ($tableRows !== []) {
55+
$this->symfonyStyle->title('Composer package bound rules');
56+
$this->symfonyStyle->table(['Rule', 'Package', 'Requires', 'Installed', 'Active'], $tableRows);
57+
}
58+
59+
if ($configurationTableRows !== []) {
60+
$this->symfonyStyle->title('Composer package bound rule configuration');
61+
$this->symfonyStyle->table(
62+
['Rule', 'Package', 'Requires', 'Installed', 'Active', 'Configuration'],
63+
$configurationTableRows
64+
);
65+
}
5066

51-
$activeCount = count(array_filter($tableRows, static fn (array $tableRow): bool => $tableRow[4] === 'yes'));
67+
$allTableRows = [...$tableRows, ...$configurationTableRows];
68+
$activeCount = count(array_filter($allTableRows, static fn (array $tableRow): bool => $tableRow[4] === 'yes'));
5269

5370
$this->symfonyStyle->note(
54-
sprintf('%d of %d composer package bound rules are active', $activeCount, count($tableRows))
71+
sprintf('%d of %d composer package bound items are active', $activeCount, count($allTableRows))
5572
);
5673

5774
return Command::SUCCESS;
@@ -77,7 +94,7 @@ private function createTableRows(): array
7794
$isActive = $installedVersion !== null && Semver::satisfies($installedVersion, $constraint);
7895

7996
$tableRows[] = [
80-
$rector::class,
97+
$this->printShortClassName($rector::class),
8198
$packageName,
8299
$constraint,
83100
$installedVersion ?? '-',
@@ -93,4 +110,86 @@ private function createTableRows(): array
93110

94111
return $tableRows;
95112
}
113+
114+
/**
115+
* @return array<array{string, string, string, string, string, string}>
116+
*/
117+
private function createConfigurationTableRows(): array
118+
{
119+
$composerBoundRuleConfigurations = SimpleParameterProvider::provideArrayParameter(
120+
Option::COMPOSER_BOUND_RULE_CONFIGURATIONS
121+
);
122+
123+
$tableRows = [];
124+
125+
foreach ($composerBoundRuleConfigurations as $composerBoundRuleConfiguration) {
126+
if (! $composerBoundRuleConfiguration instanceof ComposerBoundRuleConfiguration) {
127+
continue;
128+
}
129+
130+
$packageName = $composerBoundRuleConfiguration->getPackageName();
131+
$installedVersion = $this->installedPackageResolver->resolvePackageVersion($packageName);
132+
133+
$tableRows[] = [
134+
$this->printShortClassName($composerBoundRuleConfiguration->getRectorClass()),
135+
$packageName,
136+
$composerBoundRuleConfiguration->getVersionConstraint(),
137+
$installedVersion ?? '-',
138+
$composerBoundRuleConfiguration->isActive() ? 'yes' : 'no',
139+
$this->printConfiguration($composerBoundRuleConfiguration->getConfiguration()),
140+
];
141+
}
142+
143+
return $tableRows;
144+
}
145+
146+
/**
147+
* @param mixed[] $configuration
148+
*/
149+
private function printConfiguration(array $configuration): string
150+
{
151+
$printedItems = [];
152+
153+
foreach ($configuration as $key => $value) {
154+
$printedValue = $this->printConfigurationValue($value);
155+
156+
$printedItems[] = is_string($key) ? $key . ': ' . $printedValue : $printedValue;
157+
}
158+
159+
return implode(PHP_EOL, $printedItems);
160+
}
161+
162+
private function printConfigurationValue(mixed $value): string
163+
{
164+
if (is_object($value)) {
165+
$printedPropertyValues = [];
166+
167+
$reflectionObject = new ReflectionObject($value);
168+
foreach ($reflectionObject->getProperties() as $reflectionProperty) {
169+
$printedPropertyValues[] = $this->printConfigurationValue($reflectionProperty->getValue($value));
170+
}
171+
172+
return $this->printShortClassName($value::class) . '(' . implode(', ', $printedPropertyValues) . ')';
173+
}
174+
175+
if (is_array($value)) {
176+
$printedItems = array_map(
177+
$this->printConfigurationValue(...),
178+
$value
179+
);
180+
181+
return '[' . implode(', ', $printedItems) . ']';
182+
}
183+
184+
if (is_bool($value)) {
185+
return $value ? 'true' : 'false';
186+
}
187+
188+
return (string) $value;
189+
}
190+
191+
private function printShortClassName(string $className): string
192+
{
193+
return Strings::after($className, '\\', -1) ?? $className;
194+
}
96195
}

‎src/Console/ProcessConfigureDecorator.php‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,13 @@ public static function decorate(Command $command): void
5959
// filter by rule and path
6060
$command->addOption(Option::ONLY, null, InputOption::VALUE_REQUIRED, 'Fully qualified rule class name');
6161

62+
$command->addOption(
63+
Option::COMPOSER_BASED,
64+
null,
65+
InputOption::VALUE_NONE,
66+
'Run only rules bound to an installed composer package version'
67+
);
68+
6269
$command->addOption(
6370
Option::ONLY_SUFFIX,
6471
null,

‎src/Parallel/Command/WorkerCommandLineFactory.php‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ public function create(
123123
}
124124
}
125125

126+
if ((bool) $input->getOption(Option::COMPOSER_BASED)) {
127+
$workerCommandArray[] = self::OPTION_DASHES . Option::COMPOSER_BASED;
128+
}
129+
126130
if ($input->getOption(Option::ONLY) !== null) {
127131
$workerCommandArray[] = self::OPTION_DASHES . Option::ONLY;
128132
$workerCommandArray[] = escapeshellarg((string) $input->getOption(Option::ONLY));

0 commit comments

Comments
 (0)