oraios/serena · error

Expression matches {n} occurrences. Please revise the expres

Error message

Expression matches {n} occurrences. Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected.

What it means

replace() expects the expression to match exactly once unless allow_multiple_occurrences is set. When re.subn reports n > 1 matches, the library refuses to replace all of them blindly and asks you to make the expression more specific or opt in explicitly.

Source

Thrown at src/serena/util/text_utils.py:476

        if self.mode == "literal":
            regex = re.escape(needle)
        elif self.mode == "regex":
            regex = needle
        else:
            raise ValueError(f"Invalid mode: '{self.mode}', expected 'literal' or 'regex'.")

        regex_flags = (re.MULTILINE | re.DOTALL) if self.regex_multiline else 0

        # create replacement function with validation and backreference handling
        repl_fn = self._create_replacement_function(regex, repl, regex_flags=regex_flags)

        # perform replacement
        updated_content, n = re.subn(regex, repl_fn, content, flags=regex_flags)

        if n == 0:
            raise ValueError("Error: No matches of search expression found.")
        if not self.allow_multiple_occurrences and n > 1:
            raise ValueError(
                f"Expression matches {n} occurrences. "
                "Please revise the expression to be more specific or enable allow_multiple_occurrences if this is expected."
            )
        return updated_content


@dataclass
class ReplacementOccurrence:
    """A single prospective replacement of a pattern match within one file."""

    occurrence_id: str
    """stable, content-anchored identifier: '<relative_path>:<index_in_file>@<digest>'"""
    relative_path: str
    index_in_file: int
    """0-based index of this match among the matches within its file (in position order)"""
    start: int
    """character offset of the match start within the file content"""
    end: int

View on GitHub (pinned to 7fcbca7e62)

Solutions

  1. Add surrounding unique context to the needle so it matches only the intended occurrence
  2. Set allow_multiple_occurrences=True if replacing all occurrences is intended
  3. Use an index/offset-based replacement or loop with per-occurrence context
  4. Include a larger anchor span (function signature, section header) in the pattern

Example fix

// before
replacer.replace(content, 'timeout=30', 'timeout=60')  # appears 3 times
// after
replacer.replace(content, r'(client_config:.*?timeout=)30', r'\g<1>60')  # scoped, unique
# or: TextReplacement(..., allow_multiple_occurrences=True)
Defensive patterns

Strategy: validation

Validate before calling

n = len(re.findall(regex, content, flags=re.MULTILINE|re.DOTALL))
if n > 1 and not allow_multiple:
    raise ValueError(f'{n} matches; narrow the pattern or set allow_multiple_occurrences')

Try / catch

try:
    updated = editor.replace(content, needle, repl)
except ValueError as e:
    if 'matches' in str(e) and 'occurrences' in str(e):
        editor.allow_multiple_occurrences = True
        updated = editor.replace(content, needle, repl)
    else:
        raise

Prevention

When it happens

Trigger: Calling replace() with a pattern matching 2+ places in content while allow_multiple_occurrences=False (the default), e.g. a generic needle like 'version = ...' repeated in several sections.

Common situations: Editing boilerplate that appears in many files or blocks of the same file; overly broad regexes like '\\d+' or repeated comment markers; literal strings that occur in both header and body.

Related errors


AI-assisted analysis of oraios/serena@7fcbca7e62 (2026-08-29). Data as JSON: /api/errors/04c247793c8723ed. Report an issue: GitHub.