Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Changelog

All notable changes to CodeSnake are documented here. The format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.2.0] - 2026-09-01

A correctness release. Three defects made the tool quietly under-deliver, and six rules
missed legitimate spellings of patterns they already detected. 212 tests, up from 190.

### Fixed

- **`--format github` emits workspace-relative paths.** A directory argument produced
absolute paths in the `file=` property, and GitHub silently drops annotations whose
path does not match the workspace — so no annotation from `codesnake check --format
github src/` ever attached to a pull request diff.
- **`.gitignore` patterns holding a separator are anchored to their own directory,**
matching git. `sub/drop.py` no longer also ignores `other/sub/drop.py`, which git
keeps. Real source files were being skipped with nothing to indicate a short scan.
- **`codesnake config` refuses to overwrite an existing file** without `--force`,
instead of replacing tuned thresholds with defaults and reporting success.
- **A file that cannot be analyzed no longer sinks the run.** Analysis recurses per AST
node, so a long chained expression could exhaust the stack; the `RecursionError`
escaped as a traceback and no file in the run was reported. It is now contained per
file as a single `IO001`.
- **Overlapping targets are analyzed once.** `codesnake check pkg/a.py pkg/` analyzed
the same file twice and doubled every finding and count.
- **The banner honors `--no-color` and `NO_COLOR`,** so escape codes no longer leak
into CI logs and pre-commit output.
- **`max_*` thresholds must be 1 or greater.** `max_complexity: -1` was accepted and
flagged every function in the tree; it is now a config error naming the key.

### Added

- **Taint reaches sinks through keyword arguments.** `subprocess.run(args=input(),
shell=True)` is now the `error` the positional form always was. Shell sinks read
their command by name as well as by position.
- **EXC002** matches `builtins.Exception` and tuple clauses such as
`except (ValueError, Exception)`.
- **EXC005** covers a `raise` in the `else` of a `try` nested inside a handler.
- **`__all__` re-export forms** — `+=`, `.extend()`, and `.append()` now count as uses,
so a package `__init__.py` no longer reports every re-exported import as unused.
- **BUG002** detects duplicate tuple keys.

### Changed

- Documentation corrections across `docs/INTEGRATIONS.md` and
`docs/BASH_SCRIPTS_GUIDE.md`: per-format path behavior, `--staged` reading the working
tree rather than staged blobs, baselines being written and read from the same
directory, what fingerprints normalize (digits only), the VS Code `problemMatcher`
`fileLocation`, Python 3.10 config discovery, and which launcher scripts fall back to
`python`.
- CI actions moved past the Node.js 20 deprecation and are pinned to commit SHAs.
- The text report splits each file's source once instead of once per issue.

### Security

- `SECURITY.md` documents the threat model and a private reporting path.
- Workflow permissions are deny-by-default; the release workflow publishes to PyPI via
Trusted Publishing (OIDC) with no stored credential.

## [1.1.0] - 2026-09-01

A correctness, precision, and packaging release. 189 tests, up from 91.

### Added

- **Configuration discovery** — `.codesnake.json` or a `pyproject.toml`
`[tool.codesnake]` table, found by walking up to the repository root.
- **Parallel analysis** — `-j/--jobs N`, automatic once 8 or more files are checked.
- **Stable baselines** with fingerprints that survive line-number drift.
- `SEC003` coverage for `os.system`, `os.popen`, and `subprocess.check_output`,
`check_call`, and `getoutput`; `SEC002` coverage for `dill`, `cloudpickle`,
`jsonpickle`, `marshal`, `shelve`, `pickle.Unpickler(...).load()`, and `yaml.load`
without a safe `Loader`.
- `check_reliability` category covering `REL002` and `ASY001`.

### Fixed

- Closures no longer produce false "unused variable" warnings; function bodies are
analyzed after their enclosing scope is fully bound.
- Constants are invalidated on reassignment.
- `x is 0` no longer triggers `STYLE001`; only real booleans do.
- `--staged` works from any subdirectory of the repository.
- Columns are character offsets rather than UTF-8 byte offsets, 1-based in `text`,
`github`, and `sarif` output.
- `except*` handlers and `match` bodies are checked.

### Changed

- Restructured into a `src/codesnake/` package with a single CLI; `codesnake FILES...`
is shorthand for `codesnake check FILES...`.
- Version single-sourced from `codesnake/_version.py`.
- MIT `LICENSE` added, with PEP 639 metadata; CI on Python 3.10 through 3.13.

[Unreleased]: https://github.com/bitWarrior/codesnake/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/bitWarrior/codesnake/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/bitWarrior/codesnake/releases/tag/v1.1.0
79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
# CodeSnake

[![CI](https://github.com/bitWarrior/codesnake/actions/workflows/ci.yml/badge.svg)](https://github.com/bitWarrior/codesnake/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/codesnake.svg)](https://pypi.org/project/codesnake/)
[![Python](https://img.shields.io/pypi/pyversions/codesnake.svg)](https://pypi.org/project/codesnake/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

Semantic code checker for Python 3. It parses files into an AST, walks them, and reports security problems, common bugs, unused names, and complexity smells.

Requires **Python 3.10+**. The checker itself has **no runtime dependencies** beyond the standard library.
Two properties set it apart from the fast general-purpose linters:

- **It never imports or executes the code it analyzes.** Everything runs on the AST from `ast.parse`, so pointing it at untrusted Python — a fork's pull request, a submitted plugin — does not run that Python.
- **It has no runtime dependencies.** Standard library only, so it vendors cleanly, works air-gapped, and adds nothing to your supply chain.

It also does light **taint tracking**: `eval()` on a literal is `info`, `eval()` on something derived from `input()` or `request.args` is an `error`. See [how it compares](#how-it-compares) to Ruff, Bandit, and pylint.

Requires **Python 3.10+**.

## Install

Expand All @@ -28,6 +40,28 @@ PYTHONPATH=src python -m codesnake file.py # straight from a checkout
./codesnake.sh file.py # creates/activates codesnake-venv/
```

## First run on an existing codebase

CodeSnake reports complexity, length, and unused-name findings by default, so the
first run on a mature codebase is loud — expect roughly ten warnings per file. That
is a backlog, not an emergency: only `error` severity fails the run. Start narrow and
widen when you are ready.

```bash
# 1. What would actually fail CI. Start here.
codesnake check --severity error src/

# 2. Snapshot everything else, so CI only fails on NEW findings.
codesnake check --update-baseline .codesnake-baseline.json src/
git add .codesnake-baseline.json

# 3. From now on, this is your CI command.
codesnake check --baseline .codesnake-baseline.json src/
```

Then tune thresholds in `.codesnake.json` and shrink the baseline as you go. The full
adoption path is in [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md#adopting-codesnake-on-an-existing-codebase).

## Usage

```bash
Expand Down Expand Up @@ -212,10 +246,53 @@ rc = run_check(

Each `Issue` includes `line`, `col`, `end_line`, `end_col`, `suggestion`, and `source` (`codesnake` or `bandit`). `col` / `end_col` are **0-based character offsets** (AST byte offsets are converted), and JSON output reports them as-is. The `text`, `github`, and `sarif` formats print **1-based** columns.

## How it compares

CodeSnake is not trying to replace Ruff. Use both.

| | CodeSnake | Ruff | Bandit | pylint |
|---|---|---|---|---|
| Speed, 167 stdlib files | 1.7s | **0.14s** | 9.1s | 17.4s |
| Rules | ~25 | 800+ | ~70 security | 400+ |
| Runtime dependencies | **none** | none (Rust binary) | several | several |
| Imports the analyzed code | **never** | never | never | in some modes |
| Taint tracking | **yes** | no | limited | no |
| Autofix | no | **yes** | no | no |
| SARIF output | **yes** | no | **yes** | no |
| Baselines | **yes** | no | via `--baseline` | no |

**Ruff is roughly 13x faster and has 30x the rules.** If you want one fast
general-purpose linter with autofix, use Ruff — CodeSnake is not competing for that job.
Among the Python-implemented checkers, though, CodeSnake is the quick one: about 5x
faster than Bandit and 10x faster than pylint on the same files.

<sub>Measured on Python 3.12, best of 2–3 runs over the same 167 files from the standard
library, each tool using its own parallelism where it has any (`codesnake` auto,
`pylint -j 0`). pylint ran with `--disable=all --enable=W,E`, a reduced rule set in its
favor. Your numbers will differ; the ranking is the point, not the digits.</sub>

CodeSnake is worth adding when you want one of these:

- **Taint tracking.** `eval(x)` where `x` came from `input()` or `request.args` is an
`error`; `eval("1+1")` is `info`. The fast linters flag the call site without asking
where the data came from.
- **Analysis of untrusted code.** No import, no execution, no dependencies — safe to
run over a fork's PR or a user-submitted plugin.
- **A vendorable checker.** One pure-Python package with an empty dependency list,
auditable in an afternoon, no toolchain.
- **SARIF plus stable baselines**, for GitHub code scanning on a codebase with an
existing backlog.

Bandit has far broader security coverage; `--bandit` merges its findings into the same
report if you want both.

## Performance

Files are analyzed in a process pool once there are 8 or more of them (one worker per CPU); pass `--jobs 1` for a strictly sequential run or `--jobs N` to pin the count. Output order is always the input order.

Roughly 100 files/second single-process on a modern laptop. CodeSnake is pure Python
doing a full AST walk per file; if analysis time dominates your CI, reach for Ruff.

## Tests

```bash
Expand Down
Loading