rectorphp/rector · error · ShouldNotHappenException
Following rules are registered twice:
Error message
Following rules are registered twice:
What it means
Rector validates the rule list passed to RectorConfig::rules() (called from rector.php via $rectorConfig->rules([...]) or the fluent ->withRules([...]) builder) and throws a ShouldNotHappenException when the same rule class string appears more than once. Duplicates are always a config mistake: registering a rule twice does not make it run twice, it only hides that the config contains copy-paste or merge artifacts. The exception is thrown at config load time (src/Config/RectorConfig.php:254), before any file is analysed, so the whole run aborts immediately.
Source
Thrown at src/Validation/RectorConfigValidator.php:22
namespace Rector\Validation;
use Rector\Configuration\Option;
use Rector\Configuration\Parameter\SimpleParameterProvider;
use Rector\Contract\Rector\RectorInterface;
use Rector\Exception\ShouldNotHappenException;
use Rector\PostRector\Contract\Rector\PostRectorInterface;
final class RectorConfigValidator
{
/**
* @param string[] $rectorClasses
*/
public static function ensureNoDuplicatedClasses(array $rectorClasses): void
{
$duplicatedRectorClasses = self::resolveDuplicatedValues($rectorClasses);
if ($duplicatedRectorClasses === []) {
return;
}
throw new ShouldNotHappenException('Following rules are registered twice: ' . implode(', ', $duplicatedRectorClasses));
}
/**
* @param mixed[] $skip
*/
public static function ensureRectorRulesExist(array $skip): void
{
$nonExistingRules = [];
$skippedRectorRules = [];
$skippedNonRectorClasses = [];
foreach ($skip as $key => $value) {
if (is_string($key) && self::isNonRectorClass($key)) {
$skippedNonRectorClasses[] = $key;
continue;
}
if (self::isRectorClassValue($key)) {
if (class_exists($key)) {
$skippedRectorRules[] = $key;
} else {View on GitHub (pinned to 408fcb0ff1)
Solutions
- Open rector.php and search the rules([...]/withRules([...]) array for the exact class name printed in the message; remove the duplicate line so the class appears exactly once.
- If the rule list is assembled dynamically, de-duplicate before passing it: wrap the array with array_unique($rules) (or array_values(array_unique($rules))) at the call site.
- If you merge rule arrays from multiple sources (e.g. $rulesA + $rulesB, array_merge), switch to union ($a += $b) or array_unique on the merged result so keys/classes cannot repeat.
- Make sure a rule added explicitly is not also pulled in twice through a duplicated ->withRules()/->withPreparedSets() builder call in the same fluent chain.
Example fix
// before
$rectorConfig->rules([
\Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyAssignmentRector::class,
\Rector\CodeQuality\Rector\If_\CombinedIfRector::class,
\Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyAssignmentRector::class, // duplicate
]);
// after
$rectorConfig->rules([
\Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyAssignmentRector::class,
\Rector\CodeQuality\Rector\If_\CombinedIfRector::class,
]); Defensive patterns
Strategy: validation
Validate before calling
// before calling $rectorConfig->rules() in rector.php
$rules = [
RectorA::class,
RectorB::class,
RectorA::class, // would throw
];
$duplicates = array_keys(array_filter(array_count_values($rules), fn (int $count): bool => $count > 1));
if ($duplicates !== []) {
throw new InvalidArgumentException('Duplicate rules, fix config first: ' . implode(', ', $duplicates));
}
$rectorConfig->rules($rules); // guaranteed duplicate-free Type guard
/** @param list<class-string> $rules */
function hasUniqueRules(array $rules): bool
{
return count($rules) === count(array_unique($rules));
} Prevention
- Build rule arrays with array_unique() (or array keys as class names) so duplicates collapse automatically.
- In code review, treat any rector.php diff that only adds a rule as worth a duplicate scan: `php -r '$r = require 'rector.php';'` or a simple grep for the class name before adding it.
- When merging config arrays from multiple files/packages, use union (+) keyed by class-string instead of array_merge with numeric keys.
When it happens
Trigger: Calling $rectorConfig->rules([...]) or ->withRules([...]) with the same class string listed twice in one array (e.g. CodeQualityRectorClass::class appearing at two positions); merging several rule arrays with array_merge/array concatenation where a rule was already present in both; copy-pasting a rule line when extending a shared base config; upgrading configs by hand where an imported set already includes a rule you also add explicitly in the same rules() call.
Common situations: Teams maintaining a large rector.php that grew over years and a rule got added twice by different PRs; configs assembled dynamically (e.g. rules from multiple packages merged in a loop); migrating from the oldLevel()/sets() config style where the same rule ends up registered via both the old and new entry; resolving git merge conflicts in rector.php by keeping both duplicated lines.
Related errors
- "%s" rule is deprecated, as inverting nested ifs to early re
- "%s" rule is deprecated, as splitting a single condition int
- "%s" rule is deprecated, as splitting a single return into m
- "%s" is deprecated as risky change with little value. Use ma
- Bootstrap file "%s" does not exist.
AI-assisted analysis of rectorphp/rector@408fcb0ff1 (2026-08-21).
Data as JSON: /api/errors/1022c5510161e83f.
Report an issue: GitHub.