A tiny, zero-dependency linter for one specific, real bug:
if path.startswith("/blob/"):
out = path.replace("/blob/", "") # ← removes EVERY "/blob/", not just the prefixstr.replace(p, "") removes every occurrence of p. When it sits behind an
if x.startswith(p): guard, the author almost always meant to strip the leading
marker — so any value that contains the marker again is silently corrupted:
>>> "/blob/main/src/blob/utils.py".replace("/blob/", "")
'main/srcutils.py' # expected 'main/src/blob/utils.py'The fix is str.removeprefix (or str.removesuffix for the endswith case),
or a slice on Python < 3.9.
This bug is invisible to existing linters: ruff's B005 flags
.strip("multichar"), but nothing flags .replace(prefix, ""). This tool fills
that one gap.
Origin: this check was written after finding exactly this bug in a real, widely-used package and fixing it upstream.
pip install git+https://github.com/patchwright/replace-prefix-lintreplace-prefix-lint path/to/code/path/to/file.py:42:19: RPL001 .replace('/blob/', "") guarded by startswith('/blob/') removes every occurrence; use str.removeprefix('/blob/')
Exit code is 1 if anything is found, 0 otherwise — so it drops straight into
CI or a pre-commit hook.
The check is deliberately narrow. It fires only when all of these hold:
- an
iftest of<recv>.startswith(P)or<recv>.endswith(P)with a string literalP, - a
<recv>.replace(P, "")call inside thatifblock, - the same receiver expression, the same literal
P, and an empty replacement.
An unguarded .replace(p, "") (where replace-all may be intended) is never
flagged. Neither is a non-empty replacement, a different literal, or a different
receiver.
MIT