rectorphp/rector · error · InvalidEregException

"{" does not have a matching "}"

Error message

"{" does not have a matching "}"

What it means

processCurlyBracket() is called when an atom is followed by '{' beginning a bound like 'a{2,3}'. It searches for the closing '}' from that position; if none exists in the rest of the pattern, the bound is unterminated and InvalidEregException('"{" does not have a matching "}"') is thrown (EregToPcreTransformer.php:255), aborting the ereg-to-preg conversion of that call.

Source

Thrown at rules/Php70/EregToPcreTransformer.php:255

    }
    private function _ere2pcre_escape(string $content): string
    {
        if ($content === "\x00") {
            throw new InvalidEregException('a literal null byte in the regex');
        }
        if (strpos('\^$.[]|()?*+{}-/', $content) !== \false) {
            return '\\' . $content;
        }
        return $content;
    }
    /**
     * @param array<int, mixed> $r
     */
    private function processCurlyBracket(string $s, int $i, array &$r, int $rr): int
    {
        $ii = strpos($s, '}', $i);
        if ($ii === \false) {
            throw new InvalidEregException('"{" does not have a matching "}"');
        }
        $start = $i + 1;
        $length = $ii - ($i + 1);
        $bound = Strings::substring($s, $start, $length);
        $matches = Strings::match($bound, self::BOUND_REGEX);
        if ($matches === null) {
            throw new InvalidEregException('an invalid bound');
        }
        if (isset($matches[self::MAXIMAL_NUMBER_PART])) {
            if ($matches[self::MINIMAL_NUMBER_PART] > $matches[self::MAXIMAL_NUMBER_PART]) {
                throw new InvalidEregException('an invalid bound');
            }
            $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . ',' . $matches[self::MAXIMAL_NUMBER_PART] . '}';
        } elseif (isset($matches['comma'])) {
            $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . ',}';
        } else {
            $r[$rr] .= '{' . $matches[self::MINIMAL_NUMBER_PART] . '}';
        }

View on GitHub (pinned to 408fcb0ff1)

Solutions

  1. Close the bound in the flagged pattern: 'a{2' -> 'a{2}'.
  2. If the braces are literal placeholders, escape them ('a\{2\}') so they take the literal path.
  3. Re-run rector; preflight literal ereg patterns for '{' with a digit after it and no '}' to the end of the pattern.

Example fix

// before - unterminated bound
ereg('a{2', $subject);

// after - closed bound
ereg('a{2}', $subject); // rector: preg_match('#a{2}#m', $subject)
Defensive patterns

Strategy: try-catch

Validate before calling

// quick lint: digit-led '{' must find a '}' later in the pattern
function boundClosed(string $p): bool {
    return !preg_match('~\{\d~', $p) || (bool) preg_match('~\{\d[^}]*\}~', $p);
}

Try / catch

try {
    $pcre = $eregToPcreTransformer->transform($pattern, $ignoreCase);
} catch (InvalidEregException $e) {
    // '{' bound never closed — add the '}' or escape the brace as literal
    continue;
}

Prevention

When it happens

Trigger: Rector (Php70 set / EregToPregMatchRector, target PHP >= 7.0) processes an ereg-family call whose literal pattern has a '{' bound that is never closed, e.g. `ereg('a{2', $s)`, `split('x{1,2', $s)`, or a literal '{' that follows an atom and a digit but has no '}' anywhere after it (strpos returns false). Note: a literal '{' directly after an atom with no digit following takes the escape path, so this throw specifically needs a digit-led bound without '}'.

Common situations: Truncated quantifiers after edits; patterns mixing template placeholders with real bounds ('a{2{name}') where the first '}' was removed; hand-merged alternations that lost the closing brace.

Related errors


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