Metadata-Version: 2.4
Name: safelint
Version: 1.5.0
Summary: Engineering safety lint rules and pre-commit integration for modern Python codebases
License-Expression: MIT
Project-URL: Homepage, https://github.com/shelkesays/safelint
Project-URL: Repository, https://github.com/shelkesays/safelint
Project-URL: Issues, https://github.com/shelkesays/safelint/issues
Keywords: lint,pre-commit,static-analysis,safety,python
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: tree-sitter>=0.23.0
Requires-Dist: tree-sitter-python>=0.23.0
Provides-Extra: dev
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
Requires-Dist: pytest>=9.0.2; extra == "dev"
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
Requires-Dist: ruff>=0.15.8; extra == "dev"
Requires-Dist: ty>=0.0.26; extra == "dev"
Dynamic: license-file

# SafeLint

[![CI](https://github.com/shelkesays/safelint/actions/workflows/ci.yml/badge.svg)](https://github.com/shelkesays/safelint/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/safelint)](https://pypi.org/project/safelint/)
[![Python](https://img.shields.io/pypi/pyversions/safelint)](https://pypi.org/project/safelint/)

SafeLint is a configurable static analysis tool that enforces safety-critical coding practices inspired by Gerard J. Holzmann's "Power of Ten" rules at NASA/JPL.

Originally designed for mission-critical systems, these principles apply to any modern Python codebase - and are especially valuable when code is written fast, reviewed quickly, or generated by AI.

SafeLint integrates with pre-commit and CI pipelines to prevent unsafe code from entering your codebase.

## Why SafeLint?

Fast-moving codebases - whether written by humans under pressure or generated by AI tools - tend to drift toward the same failure patterns:

- Unbounded loops
- Silent error handling
- Hidden side effects
- Poor resource management

SafeLint catches these early, automatically, regardless of who wrote the code.

## Philosophy

> "When it really counts, it may be worth going the extra mile and living within stricter limits than may be desirable."
> - Gerard J. Holzmann, NASA/JPL

---

## Power of Ten - adapted for Python

In 1987, Holzmann wrote ten rules for spacecraft software at NASA/JPL. Nearly four decades later, the same failure patterns appear in every Python codebase. SafeLint is those ten rules, adapted for Python and automated.

| # | Holzmann's Rule | SafeLint Rule | Code |
|---|---|---|---|
| 1 | No complex control flow - no `goto`, no deep recursion | `nesting_depth`, `complexity` | [SAFE102](CONFIGURATION.md#safe102----nesting_depth), [SAFE104](CONFIGURATION.md#safe104----complexity) |
| 2 | All loops must have a fixed upper bound | `unbounded_loops` | [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) |
| 3 | No dynamic memory allocation after startup | - | *(not applicable to Python)* |
| 4 | Functions must fit on one printed page | `function_length` | [SAFE101](CONFIGURATION.md#safe101----function_length) |
| 5 | Use at least two assertions per function | `missing_assertions` | [SAFE601](CONFIGURATION.md#safe601----missing_assertions) |
| 6 | Declare variables at the smallest scope | - | *(Python handles this)* |
| 7 | Check the return value of every non-void function | `return_value_ignored`, `bare_except`, `empty_except` | [SAFE802](CONFIGURATION.md#safe802----return_value_ignored), [SAFE201](CONFIGURATION.md#safe201----bare_except), [SAFE202](CONFIGURATION.md#safe202----empty_except) |
| 8 | Limit preprocessor use | - | *(not applicable to Python)* |
| 9 | Restrict pointer use - no chained indirection | `null_dereference` | [SAFE803](CONFIGURATION.md#safe803----null_dereference) |
| 10 | Compile with all warnings; use static analysis | SafeLint itself | - |

Original paper: [spinroot.com/gerard/pdf/P10.pdf](https://spinroot.com/gerard/pdf/P10.pdf)

---

## Installation

```bash
pip install safelint
```

---

## Usage

**Check modified files** (default — only files changed since last commit):

```bash
safelint check src/
```

**Check all files** (full scan, e.g. in CI):

```bash
safelint check src/ --all-files
```

**Check specific files** (pre-commit style):

```bash
safelint src/mymodule.py src/utils.py
```

**Fail on warnings too** (useful in CI):

```bash
safelint check src/ --all-files --fail-on=warning
```

**Run in CI mode** (warnings become blocking):

```bash
safelint check src/ --all-files --mode=ci
```

**Ignore specific rules for one run:**

```bash
safelint check src/ --ignore SAFE203 --ignore side_effects
```

**Machine-readable output for tooling consumers (editors, CI, the Claude Code skill):**

```bash
safelint check src/ --format=json     # stable JSON schema
safelint check src/ --format=sarif    # SARIF 2.1.0 (GitHub code scanning, etc.)
```

**Lint un-saved buffer contents from stdin (editor mode):**

```bash
cat my_module.py | safelint --stdin --stdin-filename my_module.py --format=json
```

`--stdin-filename` drives language detection by extension and is shown as the violation file path. Combine with `--format=json` so the editor can parse the result.

**Disable the lint-result cache:**

```bash
safelint check src/ --no-cache       # otherwise: ~instant re-runs on unchanged files
```

By default safelint memoises rule output keyed on `sha256(source + engine config + filepath)` in a `.safelint_cache/` directory next to your config file (mirroring `.pytest_cache`). The filepath is folded in so two files with identical contents under different paths get separate entries, and `Violation.filepath` always reflects the current call. Add `.safelint_cache/` to `.gitignore`.

---

## Pre-commit integration

Add this to your `.pre-commit-config.yaml`:

```yaml
repos:
  - repo: https://github.com/shelkesays/safelint
    rev: v1.0.0  # replace with the latest release tag
    hooks:
      - id: safelint
        args: [--fail-on=error]  # use --fail-on=warning for stricter CI
        files: ^src/
```

Then install the hooks:

```bash
pre-commit install
```

SafeLint will now run on every `git commit` and block the commit if it finds errors.

---

## What it checks

| Code | Rule | What it flags |
|---|---|---|
| [SAFE101](CONFIGURATION.md#safe101----function_length) | `function_length` | Functions longer than 60 lines |
| [SAFE102](CONFIGURATION.md#safe102----nesting_depth) | `nesting_depth` | Control flow nested more than 2 levels deep |
| [SAFE103](CONFIGURATION.md#safe103----max_arguments) | `max_arguments` | Functions with more than 7 parameters |
| [SAFE104](CONFIGURATION.md#safe104----complexity) | `complexity` | Functions with high cyclomatic complexity |
| [SAFE201](CONFIGURATION.md#safe201----bare_except) | `bare_except` | `except:` with no exception type |
| [SAFE202](CONFIGURATION.md#safe202----empty_except) | `empty_except` | `except` blocks that do nothing (`pass`) |
| [SAFE203](CONFIGURATION.md#safe203----logging_on_error) | `logging_on_error` | Except blocks that swallow errors silently |
| [SAFE301](CONFIGURATION.md#safe301----global_state) | `global_state` | Use of the `global` keyword inside functions |
| [SAFE302](CONFIGURATION.md#safe302----global_mutation) | `global_mutation` | Writing to global variables inside functions |
| [SAFE303](CONFIGURATION.md#safe303----side_effects_hidden) | `side_effects_hidden` | Pure-looking functions that secretly do I/O |
| [SAFE304](CONFIGURATION.md#safe304----side_effects) | `side_effects` | Functions that call `print`, `open`, etc. without signalling intent |
| [SAFE401](CONFIGURATION.md#safe401----resource_lifecycle) | `resource_lifecycle` | Files or connections opened outside a `with` block |
| [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) | `unbounded_loops` | `while True` loops with no `break` |

**Dataflow rules** (opt-in, disabled by default):

| Code | Rule | What it flags |
|---|---|---|
| [SAFE801](CONFIGURATION.md#safe801----tainted_sink) | `tainted_sink` | User input flowing into `eval`, `exec`, `subprocess`, etc. without sanitization |
| [SAFE802](CONFIGURATION.md#safe802----return_value_ignored) | `return_value_ignored` | Discarding the return value of calls like `subprocess.run` or `file.write` |
| [SAFE803](CONFIGURATION.md#safe803----null_dereference) | `null_dereference` | Chaining methods directly on calls that can return `None`, e.g. `d.get("key").strip()` |

For opt-in rules (`SAFE601`, `SAFE701`, `SAFE702`) and full configuration options for every rule, see [CONFIGURATION.md](CONFIGURATION.md).

---

## Suppressing violations inline

Add a `# nosafe` comment to suppress a violation on a specific line without changing global config.

**Suppress all violations on a line:**
```python
result = eval(user_input)  # nosafe
```

**Suppress a specific rule by code:**
```python
while True:  # nosafe: SAFE501
    ...
```

**Suppress by rule name:**
```python
while True:  # nosafe: unbounded_loops
    ...
```

**Suppress multiple rules at once:**
```python
def get_data(conn, q, p1, p2, p3, p4, p5, p6):  # nosafe: SAFE101, SAFE103
    ...
```

When at least one violation is suppressed, the CLI summary reports a per-code breakdown (e.g. `(2 SAFE501, 1 SAFE304 suppressed)`) so suppressions remain visible and auditable. Use `# nosafe` sparingly — it's for line-level exceptions only. For broader suppression use the config-level options:

```toml
# pyproject.toml
[tool.safelint]
ignore = ["SAFE203", "side_effects"]          # suppress project-wide

[tool.safelint.per_file_ignores]
"tests/**" = ["SAFE101", "SAFE103"]           # suppress only for matching files
```

See [CONFIGURATION.md — Inline suppression](CONFIGURATION.md#inline-suppression), [CONFIGURATION.md — Global ignore list](CONFIGURATION.md#global-ignore-list), and [CONFIGURATION.md — Per-file ignore list](CONFIGURATION.md#per-file-ignore-list) for full reference.

---

## Configuration

SafeLint is configured via `[tool.safelint]` in your `pyproject.toml`, or a standalone `safelint.toml` file at your project root. When both exist in the same directory, **`safelint.toml` wins** — its values override anything in `[tool.safelint]` — matching ruff's `ruff.toml` / `pyproject.toml` precedence. See [CONFIGURATION.md](CONFIGURATION.md) for all options, defaults, and examples.

Ready-to-copy samples:

- [examples/sample.pyproject.toml](examples/sample.pyproject.toml) — `[tool.safelint]` block for an existing pyproject.toml
- [examples/sample.safelint.toml](examples/sample.safelint.toml) — standalone `safelint.toml` (no `[tool.safelint]` wrapper)

---

## Development

```bash
# Install with dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Run the linter on itself
safelint check src/
```

## Releasing to PyPI (Trusted Publishing)

This project publishes to PyPI via GitHub Actions using PyPI Trusted Publishing (OIDC). Do not use local `uv publish` username/password auth.

One-time setup:

1. In PyPI, open your project → **Manage** → **Publishing** → **Add a trusted publisher**.
2. Use:
   - Owner: `shelkesays`
   - Repository: `safelint`
   - Workflow: `publish.yml`
   - Environment: `pypi`
3. In GitHub, create an environment named `pypi` in **Settings → Environments**.

Release flow:

```bash
# 1) bump version in pyproject.toml
# 2) commit and push
git tag vX.Y.Z
git push origin vX.Y.Z
```

Pushing the version tag triggers `.github/workflows/publish.yml`, which builds and publishes to PyPI.

---

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on bug reports, adding new rules, and opening pull requests.
