Metadata-Version: 2.5
Name: silentbugs
Version: 0.1.0
Summary: Find the bugs that pass your tests and your linter: code that fails by returning something plausible instead of raising.
Project-URL: Homepage, https://github.com/luandv92/silentbugs
Project-URL: Issues, https://github.com/luandv92/silentbugs/issues
Project-URL: Changelog, https://github.com/luandv92/silentbugs/blob/main/CHANGELOG.md
Author: luandv92
License: MIT License
        
        Copyright (c) 2026 luandv92
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: ast,bugs,ci,code-quality,linter,ruff,silent-failure,static-analysis,subprocess
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
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: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# silentbugs

[![ci](https://github.com/luandv92/silentbugs/actions/workflows/ci.yml/badge.svg)](https://github.com/luandv92/silentbugs/actions/workflows/ci.yml)
[![pypi](https://img.shields.io/pypi/v/silentbugs)](https://pypi.org/project/silentbugs/)
[![python](https://img.shields.io/pypi/pyversions/silentbugs)](https://pypi.org/project/silentbugs/)

**Find the bugs that pass your tests *and* your linter.**

Not the ones that crash. The ones where a command fails, returns an empty string, and your
code reads that empty string as a legitimate answer — then deletes the directory.

```console
pip install silentbugs
silentbugs .
```

```
scripts/repo_guard.py:118:19: SB001 unreachable-returncode
    `subprocess.run(...).stdout` reads the output with no way left to check the return
    code: a command that fails returns empty, and the empty value is indistinguishable
    from a legitimate answer. Assign the result and check `.returncode`, or pass check=True.
    | unmerged = subprocess.run(["git", "log", f"master..{folder}"], capture_output=True).stdout
```

That line is from a real incident. The folder was not a branch, so git exited **128** with
empty stdout, and the guard read the empty string as *"zero unmerged commits"* and moved on
to delete the folder. The safety check had never worked once. Nothing raised, no test failed,
and no linter said a word.

## Why not just use ruff

You should use ruff. This runs *next to* it. Measured against `ruff --select ALL` (every
stable rule ruff has, not a default subset), on the exact shapes below:

| code | `ruff --select ALL` | `silentbugs` |
|---|---|---|
| `subprocess.run(cmd, check=False).stdout` | — | **SB001** |
| `subprocess.run(cmd, text=True)` — no `encoding` | — | **SB002** |
| `ast.parse(open(p, encoding="utf-8").read())` | — | **SB003** |
| `while not ready(): time.sleep(20)` | — | **SB004** |

Ruff's `PLW1510` flags a *missing* `check=` argument — which is why the first row writes
`check=False` explicitly. The argument is now present, the linter is satisfied, and the
return code is still unreachable. That gap is the whole point of this tool.

## The rules

Each one comes from a production incident, not from a style opinion.

### `SB001` unreachable-returncode
Reading `.stdout` or `.stderr` straight off a `subprocess` call leaves no result object to
ask about the return code, so *failed* and *empty* become the same value — and code always
picks the optimistic reading. Quiet when `check=True`, `check_output` or `check_call` is
used, since those raise. Quiet when the call takes `**kwargs`, because guessing would be
worse than missing one.

### `SB002` text-without-encoding
`text=True` without `encoding=` decodes using the locale codepage — cp1252 on Windows, not
UTF-8. One accented character or emoji in the output raises `UnicodeDecodeError` inside the
reader thread: the return code still arrives, but `stdout` comes back as **`None`**. The
error message vanishes precisely when someone needs to read it.

### `SB003` ast-parse-without-bom-tolerance
Source read with `encoding="utf-8"` and handed to `ast.parse` raises `SyntaxError` when the
file starts with a UTF-8 byte-order mark. Scanners wrap that parse in
`except SyntaxError: continue`, so the file is skipped silently and the tool reports a clean
pass over code it never looked at. Use `encoding="utf-8-sig"`.

### `SB004` unbounded-polling-loop
A `while` loop that sleeps, has no `break`/`return`/`raise`, no counter, no deadline, and a
condition its own body can never change. If the state never arrives it runs forever without
ever failing. One of these span for **21 hours** before anybody noticed.

`while True:` is deliberately **not** flagged — a daemon that runs forever is doing its job.
That exclusion came from measurement: before it, every single hit of this rule on a real
773-file repository was a daemon main loop. Seven out of seven false positives, which is how
a rule gets switched off on its first day.

## Adopting it on an existing codebase

A mature repository will have findings. Record them and hold the line from here:

```console
silentbugs . --write-baseline     # accept what exists today
silentbugs . --baseline           # exit 1 only on something new
```

The baseline is keyed on **content**, not line numbers, so adding a comment above a finding
does not resurrect it. Delete an entry once you fix it; if the bug comes back, so does the
report.

## In CI

```yaml
- run: pip install silentbugs
- run: silentbugs . --baseline
```

| exit | meaning |
|---|---|
| `0` | scanned, nothing new |
| `1` | findings |
| `2` | could not look — a path does not exist, or an unknown rule id |

`2` is separate on purpose: *"I could not look"* must never be readable as *"I looked and it
was clean"*. That is the same class of bug the tool reports, and it would be embarrassing to
ship it.

## Silencing one line

```python
result = subprocess.run(cmd, text=True)  # silentbugs: ignore SB002
```

A bare `# silentbugs: ignore` silences every rule on that line. The comment works anywhere in
a multi-line call.

## Options

| flag | effect |
|---|---|
| `--baseline [PATH]` | ignore findings recorded in the baseline (default `silentbugs-baseline.json`) |
| `--write-baseline [PATH]` | record current findings as accepted, exit 0 |
| `--select SB001,SB002` | run only these rules |
| `--json` | machine-readable output with fingerprints |
| `--list-rules` | print the rules and exit |

## What this is not

Not a linter replacement, not a formatter, not a type checker, and not a security scanner.
Four rules, each one earned. If a rule ever fires on code that is fine, that is a bug in the
rule — open an issue and it gets tightened or dropped.

## Requirements

Python 3.9+. No dependencies. Linux, macOS and Windows.

## License

MIT
