diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4822711 --- /dev/null +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index 5a7f254..7abfe8d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 @@ -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. + +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. + +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