oraios/serena · error · ValueError

expected_count= , but the pattern matches occurrence(s) -…

Error message

expected_count={expected_count}, but the pattern matches {len(occurrences)} occurrence(s) - NO changes were applied. Review the prospective changes below; re-issue with the corrected expectation, a refined pattern, or occurrence_ids selecting the intended subset.
{listing}

What it means

apply() supports an expected_count safety check: if specified (>= 0) and the number of pattern matches differs from it, no changes are applied and a ValueError including a dry-run-style listing of the prospective changes is raised. This protects against unintended bulk replacements when the file drifted or the pattern is broader than expected.

Solutions

  1. Inspect the listing in the error to see every match, then re-issue with the correct expected_count
  2. Refine the pattern (or add path/glob restrictions) so it matches exactly the intended occurrences
  3. Use occurrence_ids from a dry run to select exactly the intended subset
  4. Set expected_count=-1 (omit) if the count is genuinely variable and the replacement is safe for all matches

Example fix

// before
agent.replace_content(needle="oldName", repl="newName", expected_count=1)
// after
listing = agent.replace_content(needle="oldName", repl="newName", dry_run=True)
# review matches, then select subset:
agent.replace_content(needle="oldName", repl="newName",
                      occurrence_ids=[listing.occurrences[0].id])
Defensive patterns

Strategy: try-catch

Validate before calling

dry = agent.replace_content(needle, repl, dry_run=True)
assert len(dry.occurrences) == expected_count, \
    f"{len(dry.occurrences)} matches, expected {expected_count}"

Type guard

def count_matches(needle, path) -> int:
    return len(agent.search_for_pattern(needle, path=path))

Try / catch

try:
    agent.replace_content(needle, repl, expected_count=n)
except ValueError as e:
    if "expected_count" in str(e):
        # listing of matches is embedded in the message; refine pattern or ids
        dry = agent.replace_content(needle, repl, dry_run=True)
        agent.replace_content(needle, repl, occurrence_ids=[o.id for o in dry.occurrences[:n]])
    else:
        raise

Prevention

When it happens

Trigger: Calling replace_content with expected_count=N where the pattern matches M != N occurrences; the target text appears more times than expected (e.g. in comments, tests, or generated code); a previous edit already changed the count.

Common situations: Refactoring scripts written against an earlier file version; needles that also match inside strings/comments/docstrings; case-insensitive or wildcard patterns matching more than the single intended site.

Related errors


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

Appendix: source

Thrown at src/serena/tools/file_tools.py:305

                    f"{len(problems)} of the given occurrence_ids could not be resolved - NO changes were applied:\n"
                    f"{problem_lines}\n"
                    "Re-run with dry_run=True to obtain current occurrence ids."
                )
            if not selected:
                raise ValueError("occurrence_ids is empty - pass at least one id from a dry run, or omit the parameter to replace all.")
            return self._apply_occurrences(replacer, selected, contents, needle, repl)

        # blind apply (no ids)
        if not occurrences:
            raise ValueError(
                "No occurrences of the pattern were found - NO changes were applied. "
                "Check the mode (a literal needle containing regex metacharacters must use mode 'literal'; "
                "wildcards require mode 'regex') and the path/glob restrictions, "
                "or locate the content with search_for_pattern first."
            )
        if expected_count >= 0 and len(occurrences) != expected_count:
            listing = self._render_listing(replacer, occurrences, contents, max_answer_chars, dry_run=False)
            raise ValueError(
                f"expected_count={expected_count}, but the pattern matches {len(occurrences)} occurrence(s) - "
                f"NO changes were applied. Review the prospective changes below; re-issue with the corrected "
                f"expectation, a refined pattern, or occurrence_ids selecting the intended subset.\n{listing}"
            )
        ambiguous = [o for o in occurrences if o.is_ambiguous]
        if ambiguous:
            listing = self._render_listing(replacer, occurrences, contents, max_answer_chars, dry_run=False)
            raise ValueError(
                f"{len(ambiguous)} occurrence(s) are ambiguous (the pattern matches again inside the matched text, "
                f"indicating possible over-matching) - NO changes were applied. Review the prospective changes below "
                f"and either refine the pattern or explicitly select occurrences via occurrence_ids.\n{listing}"
            )
        return self._apply_occurrences(replacer, occurrences, contents, needle, repl)

    def _collect_files(self, relative_path: str, paths_include_glob: str, paths_exclude_glob: str) -> list[tuple[str, str]]:
        """Collects (relative_path, content) pairs of the non-ignored files in scope, in sorted path order."""
        relative_path = relative_path.strip()
        if relative_path:

View on GitHub (pinned to 7fcbca7e62)