Skip to content

Releases: kratos0718/codehound

v1.16.0 — 100 checks

Choose a tag to compare

@kratos0718 kratos0718 released this 20 Sep 16:19

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/bytes mixed in the same .join()
  • __exit__ unconditionally suppressing every exception
  • Path.write_text()/write_bytes() given the wrong type
  • asyncio.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 NotImplemented instead of NotImplementedError
  • Two __slots__-declaring bases colliding
  • str.maketrans() with mismatched-length arguments
  • __iter__ returning self with no __next__

Zero runtime dependencies, verified via a fresh pip install into a clean venv.

v1.15.0 — 88 checks

Choose a tag to compare

@kratos0718 kratos0718 released this 20 Sep 15:40

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
  • @abstractmethod on a class with no ABCMeta to enforce it — real hits across 17 more
  • A duplicate method definition silently shadowing the first
  • Dataclass/NamedTuple field ordering (kw_only, init=False, ClassVar, the KW_ONLY sentinel, all handled)
  • __slots__ conflicting with a class-level variable
  • Python 2 special methods (__nonzero__, __unicode__, ...) Python 3 silently never calls
  • A defaultdict subscript read as an if condition inserting the key as a side effect
  • json.dumps() on a raw datetime
  • A weakref to an object with no other reference
  • A regex flag landing in re.sub's count slot instead of flags

Zero runtime dependencies, verified via a fresh pip install into a clean venv.

v1.14.0

Choose a tag to compare

@kratos0718 kratos0718 released this 20 Sep 10:02

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_ordering missing 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 inside asyncio.wait([...]), a hard TypeError on 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 (the os.path sibling of CH056's pathlib version)
  • CH067 namedtuple-mutable-default - a NamedTuple field with a mutable default, shared across instances unlike a pydantic field default
  • CH068 logging-extra-reserved-key - a logging extra={} key colliding with a LogRecord attribute
  • CH069 contextvar-mutable-default - a ContextVar mutated in place on .get() instead of copied-then-set
  • CH070 threading-local-mutable-class-attr - a threading.local subclass 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

Choose a tag to compare

@kratos0718 kratos0718 released this 20 Sep 09:34

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/**kwargs forwarded to a call without the unpacking star
  • CH053 aliased-list-multiplication - [mutable_literal] * n repeats one object reference, not n independent copies
  • CH054 slots-blocks-dict - a __slots__ class that still reaches for self.__dict__ or @cached_property
  • CH055 duplicate-with-target - the same as name bound twice in one with statement
  • CH056 path-absolute-literal-join - joining a Path with 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 flag defaultd 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-ternary and/or idiom 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

Choose a tag to compare

@kratos0718 kratos0718 released this 19 Sep 08:15

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

Choose a tag to compare

@kratos0718 kratos0718 released this 19 Sep 07:06

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

Choose a tag to compare

@kratos0718 kratos0718 released this 18 Sep 16:33
  • CH037 pointless-comparison-statement (flake8-bugbear B015): a bare comparison discards its result - typo for = or a missing assert
  • 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.cell notebook-display exclusion
  • CH039 lock-constructed-inline: a lock constructed directly in the with that 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

Choose a tag to compare

@kratos0718 kratos0718 released this 18 Sep 13:44
  • 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

Choose a tag to compare

@kratos0718 kratos0718 released this 18 Sep 11:58
  • CH010 now also catches a nested def capturing 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

Choose a tag to compare

@kratos0718 kratos0718 released this 18 Sep 05:58

Production-readiness release: closes several gaps versus ruff/pylint
that the README's "How this compares" section already admitted to.

  • Inline # noqa / # noqa: CH001,CH002 suppression, 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 inside async def (outside one, get_running_loop()
    raises where get_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).