# Why is find-and-replace a bad rename for coding agents?

> Rule AR-28: Rename through the language server's rename, not text replace.

A text replace changes every spelling of a name. A language-server rename changes every reference to one symbol. When two things share a name, or a name is imported under another, the two differ, and the difference is the bug. Ask the server for the edit, apply the edit it returns, then check the result.

## Why it matters
Two modules each define `total`:

```python
# billing/cart.py
def total(items): ...

# billing/report.py
def total(rows): ...          # unrelated

# billing/checkout.py
from billing.cart import total
from billing.report import total as report_total
...
charge(total(cart.items))
```

The task: rename `cart.total` to `cart_total`. `sed -i 's/\btotal\b/cart_total/g'` renames the report's function too, rewrites local variables named `total` in unrelated functions, and edits a docstring that says "total of the order". Then `from billing.report import total as report_total` breaks, because `total` no longer exists in `report.py`.

A rename request is scoped by symbol, not by spelling. The server resolves the name at the cursor to one definition and returns edits for the references it can resolve to that definition.

## What the protocol and the servers give you
- **LSP.** The Language Server Protocol (LSP 3.17) defines `textDocument/rename` as a request "sent from the client to the server to ask the server to compute a workspace change so that the client can perform a workspace-wide rename of a symbol." The result is a `WorkspaceEdit` or `null`, and `null` means no change was required. `textDocument/prepareRename` (added in LSP 3.12.0) lets the client ask whether a rename is valid at a position first; `null` there means a rename is not valid at that position.
- **Pyright.** Its documented language server features include "Find References to find all references to a symbol within a code base" and "Rename Symbol to rename all references to a symbol within a code base".
- **TypeScript.** The language service runs behind `tsserver`, whose protocol has `rename` and `references` commands ("Rename request; value of command field is "rename"" and "Find references request; value of command field is "references"" in `protocol.ts`).

What the sources do not say is that a rename reaches every spelling of the name. The server returns the edits for what it resolves. Strings, comments, config files, and dynamic access are outside what a symbol-level rename knows about (see Edge cases).

## The steps
1. **Check a rename is valid here.** In an editor or an LSP client: `textDocument/prepareRename` at the symbol, if the server supports it. A `null` answer means don't rename from this position.
2. **Request the rename** (`textDocument/rename` with the new name). Read the `WorkspaceEdit`: count files and edits.
3. **Apply the edit it returns**, as one change, not file by file in your own words.
4. **Search for the old name as text afterwards.** What remains is what the server did not rename:
   ```bash
   rg -n -w "total" billing/ tests/ docs/ config/
   ```
5. **Decide each leftover**: string key, comment, docs, config, an unrelated symbol that happened to share the name, or a missed reference. Fix or explain every one.
6. **Run the type checker and tests** (AR-29).
   ```bash
   pyright && pytest -q
   npx tsc --noEmit && npm test
   ```
7. **Report the leftovers** that you left on purpose (AR-30).

If no language server is available for the file type, a text rename is the fallback. In that case say it was text, give the search you used, and treat the whole rename as unverified until the checker and tests pass.

## Worked example
Illustrative session, not a captured run:

```text
task: rename billing.cart.total -> cart_total
prepareRename at cart.py:3   -> range "total", placeholder "total"
rename "cart_total"          -> WorkspaceEdit: 3 files, 4 edits (cart.py, checkout.py x2, tests/test_cart.py)
applied as one change
rg -n -w "total" billing/ tests/ docs/
  billing/report.py:5     def total(rows)           -> different symbol, correct to leave
  docs/cart.md:14         `total(items)`            -> docs, update
  billing/export.py:61    COLUMNS["total"]          -> string key into a table, read: refers to report total, leave
pyright: 0 errors   pytest: 41 passed
note for report: export.py string key checked by reading, not by test
```

The `sed` version would have produced the same first line and none of the rest.

## Rule block for AGENTS.md / CLAUDE.md
```markdown
## Renames
- Rename symbols with the language server (LSP `textDocument/rename`, or the editor's Rename Symbol).
  Do not use find-and-replace, sed, or a regex across files.
- After the rename, search the old name as text (`rg -n -w <old>`) and account for every remaining hit:
  fix it, or write down why it stays.
- Check string keys, config files, docs and tests for the old name; the server does not rename those.
- Run the type checker and the tests after a rename.
- If no language server is available, say the rename was text-based and list the search you used.
```

## Edge cases
- **Strings, comments, docs.** A symbol-level rename isn't expected to touch them. Search for them afterwards. The LSP rename request returns a `WorkspaceEdit` computed by the server, so what it covers depends on the server. After a rename, run a text search for the old name to find leftovers in comments, strings and docs.
- **Dynamic access.** `getattr(obj, "total")`, `obj["total"]`, serialised field names, and ORM column names are strings. Find them with text search and update by hand (AR-07).
- **Public names used outside the repo.** Other packages and consumers aren't in the server's workspace. Plan a deprecation path, not a rename.
- **Files outside the server's project.** Excluded directories and untyped files may not be in the edit. Read the file count in the `WorkspaceEdit` against what you expected.
- **Renaming a file or module** is a separate operation, since imports and package paths change. Move it with the editor's refactor or `git mv`, then run step 4 on the old module name.
- **Partially typed Python.** Where a value's type is unknown, the server may not resolve an attribute access to the symbol and the rename can skip it. Step 4 is how you find those.

## References
- LSP 3.17 specification, Rename Request and Prepare Rename Request (`WorkspaceEdit | null`, `prepareRename` added in LSP 3.12.0): https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/
- Pyright features, Language Server Support: https://github.com/microsoft/pyright/blob/main/docs/features.md
- TypeScript server protocol (`CommandTypes.Rename`, `CommandTypes.References`, `RenameRequest`, `ReferencesRequest`): https://github.com/microsoft/TypeScript/blob/v5.6.3/src/server/protocol.ts
- TypeScript standalone server (tsserver): https://github.com/microsoft/TypeScript/wiki/Standalone-Server-(tsserver) (page links to `ts.server.protocol.CommandTypes` for the command list)
- Pyright command line: https://github.com/microsoft/pyright/blob/main/docs/command-line.md

## Changelog
- 2026-10-30: draft 1.
- 2026-10-30: remaining checks closed (claims reduced to documented behaviour)

## Related

- [AR-25: What to check before changing a function signature?](https://beta.llmgraph.in/agent-readable/ar-25.md)
- [AR-26: How does an agent know an edit is finished?](https://beta.llmgraph.in/agent-readable/ar-26.md)
- [AR-27: Is a list of callers ever the full list?](https://beta.llmgraph.in/agent-readable/ar-27.md)
- [AR-31: Why does my coding agent miss Django signal receivers?](https://beta.llmgraph.in/agent-readable/ar-31.md)

Last verified 2026-10-30
