Repository navigation
Releases: kratos0718/codehound
Release list
v1.16.0 — 100 checks
12 new checks (CH089-CH100), bringing the total to 100.
Same discipline as every prior batch: each check verified empirically before implementation, corpus-scanned against ~30 real open-source frameworks, and narrowed against real false positives rather than shipped on first pass.
CH091 (hash-eq-field-mismatch) was the deepest precision chase this project has done — seven distinct, legitimate ways for __hash__ and __eq__ to reference "the same field" without looking alike syntactically (a method call or @property standing in for a field, multi-field equality via a tuple or a wrapping call instead of named pairs, blanket equality via __dict__ or dataclasses.fields() reflection, and a property wrapping a differently-named private attribute). One genuine bug survived all seven rounds: semantic-kernel's ChatHistorySummarizationReducer hashes two fields its __eq__ never compares.
CH096 (post-init-on-non-dataclass) went from 615 false positives to 1 after finding the same "something else calls __post_init__" convention independently reinvented twice (vllm's own @config decorator, transformers' PreTrainedConfig.__init__ hook).
New checks also catch:
str/bytesmixed in the same.join()__exit__unconditionally suppressing every exceptionPath.write_text()/write_bytes()given the wrong typeasyncio.to_thread()given an async function (never runs its body)- The same keyword passed both directly and via
**-dict-unpack - A lambda target on a spawn/forkserver multiprocessing context
raise NotImplementedinstead ofNotImplementedError- Two
__slots__-declaring bases colliding str.maketrans()with mismatched-length arguments__iter__returningselfwith no__next__
Zero runtime dependencies, verified via a fresh pip install into a clean venv.
v1.15.0 — 88 checks
18 new checks (CH071-CH088), bringing the total from 70 to 88.
Same discipline as every prior batch: every check verified empirically before implementation, corpus-scanned against ~30 real open-source frameworks, and narrowed against real false positives rather than shipped on first pass. Two candidate checks (enum member aliasing, bare-except swallowing asyncio.CancelledError) were built, corpus-scanned, and rejected outright — both independently rediscovered false-positive patterns this project had already rejected once before, using different real-world evidence. See docs/FINDINGS.md for the full precision notes.
New checks catch, among others:
- A decorator's wrapper missing
@functools.wraps— real hits across 17 projects @abstractmethodon a class with noABCMetato enforce it — real hits across 17 more- A duplicate method definition silently shadowing the first
- Dataclass/NamedTuple field ordering (
kw_only,init=False,ClassVar, theKW_ONLYsentinel, all handled) __slots__conflicting with a class-level variable- Python 2 special methods (
__nonzero__,__unicode__, ...) Python 3 silently never calls - A
defaultdictsubscript read as anifcondition inserting the key as a side effect json.dumps()on a rawdatetime- A weakref to an object with no other reference
- A regex flag landing in
re.sub'scountslot instead offlags
Zero runtime dependencies, verified via a fresh pip install into a clean venv.
v1.14.0
What's new
10 new checks (CH061-CH070), bringing the total to 70:
- CH061
regex-backspace-escape- a regex pattern with a literal backspace byte, almost always an un-raw\b - CH062
total-ordering-missing-eq-@functools.total_orderingmissing the__eq__it needs to derive correct comparisons - CH063
unbounded-cycle-consumption-itertools.cycle(...)piped into something that tries to consume it in full - CH064
asyncio-wait-bare-coroutine- a bare coroutine insideasyncio.wait([...]), a hardTypeErroron Python 3.11+ - CH065
dict-fromkeys-mutable-default-dict.fromkeys(keys, value)aliasing one value across every key - CH066
os-path-join-absolute-literal-os.path.join's absolute-literal reset (theos.pathsibling of CH056'spathlibversion) - CH067
namedtuple-mutable-default- aNamedTuplefield with a mutable default, shared across instances unlike a pydantic field default - CH068
logging-extra-reserved-key- a loggingextra={}key colliding with aLogRecordattribute - CH069
contextvar-mutable-default- aContextVarmutated in place on.get()instead of copied-then-set - CH070
threading-local-mutable-class-attr- athreading.localsubclass leaking a class-level mutable attribute across every thread
This batch's first corpus scan came back much cleaner than the last one - only 3 checks had any hits, and only CH069 needed real narrowing (all 7 first-pass hits turned out to already use the correct copy-then-set pattern, independently, in 4 unrelated projects). CH067 and CH068 each found one real, unfiled bug on the first pass with no narrowing needed - see docs/FINDINGS.md for the full story.
35 new tests (419 total), self-scan clean, fresh clean-venv wheel install verified.
v1.13.0
What's new
10 new checks (CH051-CH060), bringing the total to 60:
- CH051
mutation-during-iteration- mutating a dict/list/set while a loop iterates directly over it - CH052
forwarded-without-unpacking- a function's own*args/**kwargsforwarded to a call without the unpacking star - CH053
aliased-list-multiplication-[mutable_literal] * nrepeats one object reference, notnindependent copies - CH054
slots-blocks-dict- a__slots__class that still reaches forself.__dict__or@cached_property - CH055
duplicate-with-target- the sameasname bound twice in onewithstatement - CH056
path-absolute-literal-join- joining aPathwith a literal that starts with/resets to absolute - CH057
reused-exhausted-iterator- replaying an already-consumed generator/map/filter/zip - CH058
argparse-store-true-default- an argparse flagdefaultd to its own effect, making it a no-op - CH059
decorator-missing-return- a@functools.wraps-wrapped inner function never referenced again - CH060
falsy-and-or-ternary- the broken pre-ternaryand/oridiom when the middle value is falsy
Every check was verified directly (REPL reproduction) before any AST code was written, then corpus-scanned against ~29 real frameworks. Several needed real precision passes - see docs/FINDINGS.md for the full story, including a first-pass false-positive count over 2,000 on one check, cut to 2 real hits after narrowing.
Also: 4 real, unfiled bugs found in mlflow, litellm, and transformers (CH051 x3, CH058 x2) - see FINDINGS.md's "Real bugs found, not yet filed."
47 new tests (384 total), self-scan clean, fresh clean-venv wheel install verified.
v1.12.1
Precision fixes to three existing checks, found by widening the corpus scan well past the original ~29 frameworks:
- CH011 (lru-cache-on-method): only trust lru_cache/cache when it can actually be functools's, not any decorator that happens to share the name. SQLAlchemy's own @reflection.cache was a false positive on ~94 methods.
- CH009/CH012/CH028 (threads/processes/timers): add the "passed as an argument to any call" escape their siblings CH016/CH027/CH031 already had. Found via a real false positive in uvicorn's multi-worker supervisor.
- CH049 (staticmethod-references-self): two more real false positives fixed - self in a decorator expression (celery), and a locally-defined class legitimately closing over an outer method's self (pytest).
Also documents five new real CH047 hits (SQLAlchemy, Poetry, Flask, Celery) and one CH009 hit (langgraph), none filed yet. Full detail in docs/FINDINGS.md.
v1.12.0 - 50 checks
Eleven new checks, reaching 50 total: CH040-CH050. Mostly flake8-bugbear/pyflakes ports (B003/B015/B016/B017/B018/B022/B023/B025/B029/B035, F631) plus one original idea, CH047 (@contextmanager cleanup skipped when the with-block body raises) - real hits found in litellm, peft, agno, and mlflow.
Three checks were built, corpus-scanned, and rejected outright before this batch shipped (an assignment-from-.sort() check that collided with NumPy/PyMongo/HuggingFace Dataset/custom .sort() methods; an enum-duplicate-value check where every real hit was a deliberate "rolling alias" pattern; an eq-without-hash check where every real hit was an intentional, documented value-object design). Full reasoning in docs/FINDINGS.md.
CH049 and CH050 each needed a real precision fix after their first corpus scan - see docs/FINDINGS.md for the before/after counts.
v1.11.0
- CH037 pointless-comparison-statement (flake8-bugbear B015): a bare comparison discards its result - typo for
=or a missingassert - CH038 useless-expression-statement (flake8-bugbear B018): a literal or known-pure-builtin call used as a bare statement, ported with two precision passes not in B018's own source (constant-only container literals, builtin-name-shadowing detection) plus a marimo
@app.cellnotebook-display exclusion - CH039 lock-constructed-inline: a lock constructed directly in the
withthat acquires it can never be shared - not from bugbear/pylint/Ruff/bandit, this project's own idea
Corpus scan found four more real bugs (mlflow, vllm x2, letta, pydantic) - documented in docs/FINDINGS.md, none filed (AI-disclosure policy conflicts for three, insufficient confidence in the fix for the fourth).
Full findings: docs/FINDINGS.md
v1.10.0
- CH034
raise-literal: raise "x"/None/(1,2)/{...} always fails with an unrelated TypeError instead of the exception you meant to raise (flake8-bugbear B016) - CH035
empty-except-tuple: except () can never match anything — dead code (flake8-bugbear B029) - CH036
environ-reassignment: os.environ = {...} rebinds the name but never syncs the real process environment (flake8-bugbear B003)
All three verified directly in a REPL before any AST code was written. Corpus scan across ~29 frameworks found 2 real bugs (llama_index, HuggingFace datasets) and zero false positives.
Full findings: docs/FINDINGS.md
v1.9.0
- CH010 now also catches a nested
defcapturing a for-loop variable (the same shape as bugbear's B023's lambda case) - CH032: default argument computed from time.time()/datetime.now()/random.random()/uuid.uuid4() etc. — evaluated once at definition time
- CH033: .strip()/.lstrip()/.rstrip() with a multi-character argument that reads as a substring mistake, not a character set — narrowed after a corpus scan showed most multi-char strip calls are deliberate (line endings, bracket/quote pairs)
Full findings and false-positive discipline: docs/FINDINGS.md
v1.8.0
Production-readiness release: closes several gaps versus ruff/pylint
that the README's "How this compares" section already admitted to.
- Inline
# noqa/# noqa: CH001,CH002suppression, same convention
flake8/ruff use, so it doesn't collide with either on a shared line. [tool.codehound]project config in pyproject.toml (select/exclude/
paths), loaded via the standard-library tomllib on 3.11+; silently
skipped on 3.9/3.10 rather than adding a required TOML dependency -
every flag still works identically via the CLI on any version.--fix, deliberately narrow: CH017 always (a pure rename, no risk),
CH004 only insideasync def(outside one,get_running_loop()
raises whereget_event_loop()wouldn't). Every other check needs a
judgment call or an import that may not be in scope, so it stays
detection-only.- Scanning parallelized across a process pool once there are enough
files to make it worth it. Verified byte-identical output against the
sequential path first: a full scan of HuggingFace's transformers went
from 57 seconds to 12.
Still zero required runtime dependencies - confirmed via a fresh install
(pip show codehound reports no Requires).