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

  1. 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.
  2. 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.
  3. 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.
  4. 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

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


AI-assisted analysis of rectorphp/rector@408fcb0ff1 (2026-08-21). Data as JSON: /api/errors/1022c5510161e83f. Report an issue: GitHub.