Metadata-Version: 2.4
Name: oaklint
Version: 0.1.3
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
License-File: LICENSE
Summary: An opinionated, agent-first Python linter.
Keywords: linter,python,static-analysis,code-quality
Author: Omar Ali Khan
License-Expression: MIT
Requires-Python: >=3.8
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://github.com/omaralikhn/oaklint/blob/main/docs/rules/README.md
Project-URL: Homepage, https://github.com/omaralikhn/oaklint
Project-URL: Issues, https://github.com/omaralikhn/oaklint/issues
Project-URL: Repository, https://github.com/omaralikhn/oaklint

# oaklint

[![CI](https://github.com/omaralikhn/oaklint/actions/workflows/ci.yml/badge.svg)](https://github.com/omaralikhn/oaklint/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/omaralikhn/oaklint/branch/main/graph/badge.svg)](https://codecov.io/gh/omaralikhn/oaklint)
[![PyPI](https://img.shields.io/pypi/v/oaklint.svg)](https://pypi.org/project/oaklint/)
[![Python](https://img.shields.io/pypi/pyversions/oaklint.svg)](https://pypi.org/project/oaklint/)
[![License](https://img.shields.io/pypi/l/oaklint.svg)](https://github.com/omaralikhn/oaklint/blob/main/LICENSE)

`oak` is an opinionated, agent-first Python linter - a cross between Black and Ruff that enforces a curated set of rules targeting common sources of technical debt. These are the patterns that accumulate quietly, from minor style drift up to real structural problems, and they show up most in agent-written code and junior-developer code. `oak` is built to be both a linter and a learning tool: every rule explains why it exists, so the code gets fixed and the author learns the reasoning behind the fix. Built in Rust on the `rustpython-ruff_python_parser` crate, so it parses exactly what a modern Python toolchain does while staying a small standalone binary.

`oak` runs alongside `ruff`, `black`, and whatever else is already in your toolchain rather than replacing any of them. It stays fully compatible and layers its curated rules on top, so you keep your existing formatter and linter and add `oak` for the checks they do not cover.

Some of these rules are hot takes - deliberately more opinionated than a general-purpose linter would risk. In practice I have found they are what keeps medium-to-large teams and their codebases maintainable as they grow.

The set grows by one rule: if a standard can be systematically deduced from the code - checked mechanically rather than by judgment - it gets added here. Anything that needs human taste to adjudicate stays out.

## Installation

`oak` ships as a prebuilt wheel on PyPI, so it installs with no Rust toolchain:

```bash
uv tool install oaklint       # install the oak command globally
uvx oaklint path/to/file.py   # or run it without installing
pip install oaklint           # or with pip
```

The distribution is named `oaklint`; the installed command is `oak`.

## Usage

```bash
oak path/to/file.py src/            # report violations, exit 1 if any
oak --fix src/                      # rewrite files to resolve fixable violations
oak docs OAK005,OAK012              # print the full reasoning for one or more rules
```

Every rule ships with a full documentation page that an agent or a human can read on demand. Running `oak docs <codes>` prints the complete rationale, Good/Bad examples, and the recommended fix for exactly those rules - so the reader learns why the rule exists and how to apply the change, without leaving the terminal or hunting through the repo.

## Output

A run is structured to be read by an agent under a token budget, not to repeat itself once per line:

```text
Run `oak docs OAK007,OAK014,OAK015` for full rule reasoning, or fetch each individually.

  Code    Count  Rule
  OAK007     10  A public function or class is defined below a private function in the same scope.
  OAK014      3  A function returns a fixed-shape dict literal instead of a named type.
  OAK015      5  A function or method name does not lead with an action verb.
  Total      18

OAK007 - Public function or class must be placed above private functions
  tests/conftest.py:106:5: `build_client`
  tests/conftest.py:126:5: `Ledger`

OAK014 - Function returning a record must use a class, not a dict
  tests/conftest.py:69:9

OAK015 - Function or method name must lead with an action verb
  tests/conftest.py:85:5: `gateway`
  tests/conftest.py:89:5: `ledger`

Found 18 violations
```

The shape is deliberately agent-friendly and context-length-aware:

* **The docs command leads.** One line points at the full reasoning for every rule the run hit, and says each can be fetched on its own - the agent pulls the deep explanation only for the rules it decides to act on, instead of paying for it up front.
* **The summary table amortizes the explanation.** Each rule's definition and its violation count appear exactly once, so an agent can triage which rules matter before reading a single location.
* **Locations are grouped, not annotated.** The per-rule message is stated once as a section header, then followed by bare `path:line:column` lines - each suffixed with the specific identifier at fault (a function name, import alias, or offending token) when the rule has one. A file that trips one rule a hundred times costs a hundred short lines, not a hundred repetitions of the same sentence and the same `oak docs` pointer.

This keeps a large run's output roughly proportional to the number of distinct rules plus the number of locations, rather than to the product of the two - so a sweep over a whole codebase stays inside an agent's context window.

## Rules

| Code | Rule |
|------|------|
| [OAK001](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK001.md) | Missing blank line after an indented block (`if`/`for`/`while`/`with`/`try`/`match`) or before a continuation (`elif`/`else`/`except`/`finally`). |
| [OAK002](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK002.md) | Missing blank line before a `return`. |
| [OAK003](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK003.md) | Comment must be a sentence-case NOTE/TODO/XXX ending with a period. |
| [OAK004](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK004.md) | Continuation line must align under the comment's first word. |
| [OAK005](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK005.md) | Import must not use an alias. |
| [OAK006](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK006.md) | Only functions may be private; classes and module- or class-level names must be public. |
| [OAK007](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK007.md) | Private functions must be placed below all public functions and classes in a scope. |
| [OAK008](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK008.md) | Name must not be a single character. |
| [OAK009](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK009.md) | Test must assert observable behavior, not mock calls. |
| [OAK010](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK010.md) | Mock library must not be used, prefer an in-process fake. |
| [OAK011](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK011.md) | `pytest.raises` must not use `match=`; assert the full error message. |
| [OAK012](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK012.md) | Function returning multiple values must use a class, not a tuple. |
| [OAK013](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK013.md) | Empty string must not stand for an absent value; use `None`. |
| [OAK014](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK014.md) | Function returning a record must use a class, not a dict. |
| [OAK015](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK015.md) | Function or method name must lead with an action verb. |
| [OAK016](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/OAK016.md) | Test must not contain conditional logic (`if`/`elif`/`else`). |

OAK001 and OAK002 are fixable with `--fix`, which inserts the missing blank line. OAK003 through OAK016 are report-only. Each rule has a page in [`docs/rules/`](https://github.com/omaralikhn/oaklint/blob/main/docs/rules/README.md) with its rationale and a good/bad example.

A blank line is required before a continuation (`elif`, `else`, `except`, `finally`); only `case` is left tight and never flagged. A guard clause whose block is a single `return` needs no blank line after it. A comment written directly above a statement belongs to it, so the separating blank line is expected above the whole comment run, not between the comment and its statement.

## Configuration

`oak` reads settings from the first of `.oak.toml`, `oak.toml`, or `[tool.oak]` in `pyproject.toml` found by walking up from the current directory. A `pyproject.toml` without a `[tool.oak]` table is skipped and the search continues upward.

```toml
[tool.oak]
select = ["OAK001"]              # when set, only these codes lint (prefixes like "OAK" and "ALL" work)
ignore = ["OAK002"]              # removed from the active set after select
exclude = ["tests/**", "vendor"] # globs skipped entirely
action-verbs = ["yeet", "reconcile"] # extra leading verbs OAK015 accepts

[tool.oak.per-file-ignores]
"tests/**" = ["OAK002"]          # codes silenced only for matching files
```

In a standalone `oak.toml` the same keys are written at the top level (no `[tool.oak]` header). Unknown keys are a hard error and keys are kebab-case.

### Inline suppression

A comment can silence a violation in place, following ruff's `# noqa` model:

```python
import numpy as np  # noak                 # silences every oak rule on this line
import numpy as np  # noak: OAK005         # silences only OAK005 on this line
import numpy as np  # noak: OAK005,OAK008  # silences a comma-separated list
```

A `# oak: noqa` comment silences a whole file, with the same optional code list:

```python
# oak: noqa               # silences every oak rule in this file
# oak: noqa: OAK005,OAK008  # silences only these codes in this file
```

A line directive is anchored to the line the violation is reported on, and the keyword reads case-insensitively (`# NOAK`). A bare directive with no codes blankets its scope; naming codes narrows it to exactly those.

### Default rule set

With no `select` key, `oak` runs only the default-on set - the low-friction hygiene, formatting, and structural rules `OAK001` through `OAK008` plus `OAK012` (no tuple returns), which almost any team accepts. The opinionated house-style rules stay off until you opt into them.

Enable the opinionated rules by naming them in `select` - the testing rules (OAK009, OAK010, OAK011), the record-dict rule (OAK014), the empty-string rule (OAK013), and the action-verb rule (OAK015). Setting `select` replaces the default set, so list every code you want to run, including the default-on ones you want to keep:

```toml
[tool.oak]
# NOTE: The default rules plus the action-verb rule.
select = [
    "OAK001", "OAK002", "OAK003", "OAK004", "OAK005",
    "OAK006", "OAK007", "OAK008", "OAK012", "OAK015",
]
```

`select = ["ALL"]` runs every rule, including the opinionated and heuristic ones. OAK015 is heuristic and fires against a maintained verb allowlist, so expect to tune it before turning it on broadly.

## Development

```bash
make check     # fmt --check + clippy -D warnings + tests (the CI gate)
make format    # cargo fmt
make coverage  # per-file source coverage report
```

`make coverage` uses Rust's built-in `-C instrument-coverage` and the system `llvm-cov`/`llvm-profdata`, so it needs neither `cargo-llvm-cov` nor a rustup component. Point it at other target directories or llvm binaries with `CARGO_TARGET_DIR`, `LLVM_COV`, and `LLVM_PROFDATA`, and pass extra flags straight through (`make coverage -- --show-missing-lines`).

Rules live in `src/rules/`, one module per rule code (named `oak0NN_<domain>_<thing>.rs`), with helpers shared between two codes of a family in `src/rules/util/`. The pipeline is: `config::Config::discover` (resolve settings) → `discovery` (find `.py` files, drop excluded) → `linter::check_source` (parse + run rules) → filter by `select`/`ignore`/`per-file-ignores` → `diagnostics::Violation` (report) → `linter::apply_fixes` (`--fix`).

