failroute 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- failroute-0.3.0/LICENSE +21 -0
- failroute-0.3.0/PKG-INFO +205 -0
- failroute-0.3.0/README.md +182 -0
- failroute-0.3.0/pyproject.toml +50 -0
- failroute-0.3.0/setup.cfg +4 -0
- failroute-0.3.0/src/failroute/__init__.py +27 -0
- failroute-0.3.0/src/failroute/__main__.py +6 -0
- failroute-0.3.0/src/failroute/analyzer.py +595 -0
- failroute-0.3.0/src/failroute/cli.py +142 -0
- failroute-0.3.0/src/failroute/sarif.py +138 -0
- failroute-0.3.0/src/failroute.egg-info/PKG-INFO +205 -0
- failroute-0.3.0/src/failroute.egg-info/SOURCES.txt +19 -0
- failroute-0.3.0/src/failroute.egg-info/dependency_links.txt +1 -0
- failroute-0.3.0/src/failroute.egg-info/entry_points.txt +2 -0
- failroute-0.3.0/src/failroute.egg-info/requires.txt +3 -0
- failroute-0.3.0/src/failroute.egg-info/top_level.txt +1 -0
- failroute-0.3.0/tests/test_analyzer.py +372 -0
- failroute-0.3.0/tests/test_benchmark.py +37 -0
- failroute-0.3.0/tests/test_cli.py +103 -0
- failroute-0.3.0/tests/test_deep_optimization.py +301 -0
- failroute-0.3.0/tests/test_sarif.py +79 -0
failroute-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 feiiiiii5
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
failroute-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: failroute
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Static detection of failure-routing anti-patterns in Python
|
|
5
|
+
Author: fc
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: static-analysis,exception-handling,code-quality,llm,reliability
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Provides-Extra: test
|
|
21
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# failroute
|
|
25
|
+
|
|
26
|
+
Static detection of **failure-routing** anti-patterns in Python: the practice of
|
|
27
|
+
converting an underlying failure into a success-like outcome at the wrong layer.
|
|
28
|
+
|
|
29
|
+
Failure-routing is the root cause behind some of the most insidious correctness
|
|
30
|
+
bugs in real LLM/eval/agent codebases:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# Before — a judge API outage becomes a perfect "0.0 score" with no way to tell
|
|
34
|
+
try:
|
|
35
|
+
score = await llm_judge(prompt)
|
|
36
|
+
except Exception:
|
|
37
|
+
return 0.0 # ← silent fallback: failure looks like a legitimate low score
|
|
38
|
+
|
|
39
|
+
# After — the failure propagates; callers can route it to the right outcome
|
|
40
|
+
return await llm_judge(prompt)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## What it detects
|
|
44
|
+
|
|
45
|
+
| Mode | Pattern |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `no-action` | `except ...: pass` — the exception is discarded, callers never learn |
|
|
48
|
+
| `silent-fallback` | handler returns/assigns a constant (`None`, `0`, `0.0`, `False`, `[]`, …) without re-raising |
|
|
49
|
+
| `masked-exception` | **catch-all** handler re-raises conditionally yet also falls through to a success-looking return |
|
|
50
|
+
| `name-shadowing` | `except E as e:` whose body rebinds `e` — Python deletes the binding at handler exit, so later uses raise `NameError` |
|
|
51
|
+
|
|
52
|
+
Findings are emitted as `file:line: mode: message`, or as JSON for CI.
|
|
53
|
+
|
|
54
|
+
### Logging exemption (two tiers)
|
|
55
|
+
|
|
56
|
+
A handler that *records* the failure is informational, not silent — but what
|
|
57
|
+
counts as a record depends on how wide the handler is:
|
|
58
|
+
|
|
59
|
+
- **Catch-all handlers** (`except:` / `except Exception:`) must log at a
|
|
60
|
+
severity worth reading (`warning`+). A `debug` line or a bare `print(...)`
|
|
61
|
+
does not survive production triage, so it does not exempt.
|
|
62
|
+
- **Typed handlers** name an anticipated failure mode; recording it at *any*
|
|
63
|
+
level (even `logger.info`) is enough.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
```console
|
|
68
|
+
$ failroute path/to/file.py
|
|
69
|
+
$ failroute path/to/dir # recursive
|
|
70
|
+
$ failroute --repo . # skip .git/.venv/build/...
|
|
71
|
+
$ failroute --repo . --exclude tests/corpus # repeatable path exclusions
|
|
72
|
+
$ failroute --json --repo . | jq 'select(.mode=="silent-fallback")'
|
|
73
|
+
$ failroute --format sarif --output results.sarif --repo . # code scanning
|
|
74
|
+
$ failroute --threshold 5 # exit 1 when more than 5 findings
|
|
75
|
+
$ python -m failroute . # module form (no console script needed)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Exit codes: `0` clean, `1` findings above threshold, `2` usage error.
|
|
79
|
+
|
|
80
|
+
### Output formats
|
|
81
|
+
|
|
82
|
+
```console
|
|
83
|
+
$ failroute --format text path/ # default: file:line: mode: message
|
|
84
|
+
$ failroute --format json path/ # one JSON object per finding
|
|
85
|
+
$ failroute --format sarif --output scan.sarif path/ # SARIF 2.1.0
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
SARIF output plugs straight into [GitHub code scanning](
|
|
89
|
+
https://docs.github.com/en/code-security/code-scanning) via the
|
|
90
|
+
`upload-sarif` action, so findings appear inline on pull requests:
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
- run: failroute --format sarif --output results.sarif --repo .
|
|
94
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
95
|
+
with:
|
|
96
|
+
sarif_file: results.sarif
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Or use the bundled composite action, which installs failroute, scans, and
|
|
100
|
+
uploads SARIF in one step:
|
|
101
|
+
|
|
102
|
+
```yaml
|
|
103
|
+
- uses: feiiiiii5/failroute/action@main
|
|
104
|
+
with:
|
|
105
|
+
path: src
|
|
106
|
+
exclude: tests/corpus fixtures
|
|
107
|
+
threshold: "0"
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Severity mapping: `silent-fallback` → `error`, `no-action` and
|
|
111
|
+
`masked-exception` → `warning`.
|
|
112
|
+
|
|
113
|
+
### Suppressing findings
|
|
114
|
+
|
|
115
|
+
Reviewed-and-accepted handlers can be opted out with a line marker (the
|
|
116
|
+
scanner honors both):
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
try:
|
|
120
|
+
return best_effort()
|
|
121
|
+
except Exception: # failroute: ignore - documented fallback semantics
|
|
122
|
+
return None
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`# pragma: no cover` markers are honored as well (explicitly defensive code).
|
|
126
|
+
|
|
127
|
+
## Examples that trip it
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
def classify(text): # no-action
|
|
131
|
+
try:
|
|
132
|
+
return model.predict(text)
|
|
133
|
+
except Exception:
|
|
134
|
+
pass # 💥 swallowed
|
|
135
|
+
|
|
136
|
+
def score(prompt): # silent-fallback
|
|
137
|
+
try:
|
|
138
|
+
return judge(prompt)
|
|
139
|
+
except Exception:
|
|
140
|
+
return 0.0 # 💥 outage == "0.0 score"
|
|
141
|
+
|
|
142
|
+
def fetch(url): # silent-fallback (assign)
|
|
143
|
+
data = None
|
|
144
|
+
try:
|
|
145
|
+
data = download(url)
|
|
146
|
+
except Exception:
|
|
147
|
+
data = {"items": []} # 💥 error looks like an empty result
|
|
148
|
+
return data
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## What it does *not* flag (by design)
|
|
152
|
+
|
|
153
|
+
* `except KeyboardInterrupt` / `except SystemExit` — normally intentional.
|
|
154
|
+
* Handlers that re-raise unconditionally without a fallback.
|
|
155
|
+
* `except` bodies that log at `warning`/`error` **and** re-raise — the failure
|
|
156
|
+
still propagates; we only flag the success-looking path.
|
|
157
|
+
|
|
158
|
+
Run `failroute` on its own checkout as a smoke test:
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
$ pip install -e .
|
|
162
|
+
$ failroute --repo . # expected: zero findings (self-hosting)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Benchmarks & validation
|
|
166
|
+
|
|
167
|
+
All numbers below are reproducible from this checkout; nothing here is
|
|
168
|
+
copy-pasted from a run that cannot be re-executed.
|
|
169
|
+
|
|
170
|
+
### Labelled corpus (precision / recall)
|
|
171
|
+
|
|
172
|
+
`tests/corpus/` holds 19 hand-labelled exception handlers (10 positives across
|
|
173
|
+
all three modes, 9 negatives covering re-raise, log-and-raise, derived values,
|
|
174
|
+
dead code, opt-out markers, and non-fallback constants). Ground truth lives in
|
|
175
|
+
`tests/corpus/manifest.json` and was written from the *semantics* of each
|
|
176
|
+
fixture, independently of tool output.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
corpus v1 TP=10 FP=0 FN=0 TN=9
|
|
180
|
+
precision=1.0 recall=1.0
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Re-run: `python tools/benchmark.py` (also enforced by `pytest`).
|
|
184
|
+
|
|
185
|
+
### What syntactic linters miss
|
|
186
|
+
|
|
187
|
+
Against the source packages of 8 real AI/eval repositories (garak,
|
|
188
|
+
inspect_ai, pydantic-ai, uqlm, trl, smolagents, deepteam, fickling),
|
|
189
|
+
failroute reported **647 findings**; ruff's exception-handling rules
|
|
190
|
+
(`S110` try-except-pass, `S112` try-except-continue) reported **80**, of which
|
|
191
|
+
70 overlap failroute's `no-action` mode. The remaining **390 findings are
|
|
192
|
+
silent-fallback / masked-exception handlers** -- failures converted into
|
|
193
|
+
success-looking values -- a class syntactic rules cannot express by
|
|
194
|
+
construction.
|
|
195
|
+
|
|
196
|
+
Re-run: `python tools/compare_ruff.py <repo> [<repo> ...]`.
|
|
197
|
+
Results are checked into `bench/`.
|
|
198
|
+
|
|
199
|
+
## Development
|
|
200
|
+
|
|
201
|
+
```console
|
|
202
|
+
$ pip install -e ".[test]"
|
|
203
|
+
$ pytest
|
|
204
|
+
$ ruff check .
|
|
205
|
+
```
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# failroute
|
|
2
|
+
|
|
3
|
+
Static detection of **failure-routing** anti-patterns in Python: the practice of
|
|
4
|
+
converting an underlying failure into a success-like outcome at the wrong layer.
|
|
5
|
+
|
|
6
|
+
Failure-routing is the root cause behind some of the most insidious correctness
|
|
7
|
+
bugs in real LLM/eval/agent codebases:
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
# Before — a judge API outage becomes a perfect "0.0 score" with no way to tell
|
|
11
|
+
try:
|
|
12
|
+
score = await llm_judge(prompt)
|
|
13
|
+
except Exception:
|
|
14
|
+
return 0.0 # ← silent fallback: failure looks like a legitimate low score
|
|
15
|
+
|
|
16
|
+
# After — the failure propagates; callers can route it to the right outcome
|
|
17
|
+
return await llm_judge(prompt)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What it detects
|
|
21
|
+
|
|
22
|
+
| Mode | Pattern |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `no-action` | `except ...: pass` — the exception is discarded, callers never learn |
|
|
25
|
+
| `silent-fallback` | handler returns/assigns a constant (`None`, `0`, `0.0`, `False`, `[]`, …) without re-raising |
|
|
26
|
+
| `masked-exception` | **catch-all** handler re-raises conditionally yet also falls through to a success-looking return |
|
|
27
|
+
| `name-shadowing` | `except E as e:` whose body rebinds `e` — Python deletes the binding at handler exit, so later uses raise `NameError` |
|
|
28
|
+
|
|
29
|
+
Findings are emitted as `file:line: mode: message`, or as JSON for CI.
|
|
30
|
+
|
|
31
|
+
### Logging exemption (two tiers)
|
|
32
|
+
|
|
33
|
+
A handler that *records* the failure is informational, not silent — but what
|
|
34
|
+
counts as a record depends on how wide the handler is:
|
|
35
|
+
|
|
36
|
+
- **Catch-all handlers** (`except:` / `except Exception:`) must log at a
|
|
37
|
+
severity worth reading (`warning`+). A `debug` line or a bare `print(...)`
|
|
38
|
+
does not survive production triage, so it does not exempt.
|
|
39
|
+
- **Typed handlers** name an anticipated failure mode; recording it at *any*
|
|
40
|
+
level (even `logger.info`) is enough.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ failroute path/to/file.py
|
|
46
|
+
$ failroute path/to/dir # recursive
|
|
47
|
+
$ failroute --repo . # skip .git/.venv/build/...
|
|
48
|
+
$ failroute --repo . --exclude tests/corpus # repeatable path exclusions
|
|
49
|
+
$ failroute --json --repo . | jq 'select(.mode=="silent-fallback")'
|
|
50
|
+
$ failroute --format sarif --output results.sarif --repo . # code scanning
|
|
51
|
+
$ failroute --threshold 5 # exit 1 when more than 5 findings
|
|
52
|
+
$ python -m failroute . # module form (no console script needed)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Exit codes: `0` clean, `1` findings above threshold, `2` usage error.
|
|
56
|
+
|
|
57
|
+
### Output formats
|
|
58
|
+
|
|
59
|
+
```console
|
|
60
|
+
$ failroute --format text path/ # default: file:line: mode: message
|
|
61
|
+
$ failroute --format json path/ # one JSON object per finding
|
|
62
|
+
$ failroute --format sarif --output scan.sarif path/ # SARIF 2.1.0
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
SARIF output plugs straight into [GitHub code scanning](
|
|
66
|
+
https://docs.github.com/en/code-security/code-scanning) via the
|
|
67
|
+
`upload-sarif` action, so findings appear inline on pull requests:
|
|
68
|
+
|
|
69
|
+
```yaml
|
|
70
|
+
- run: failroute --format sarif --output results.sarif --repo .
|
|
71
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
72
|
+
with:
|
|
73
|
+
sarif_file: results.sarif
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or use the bundled composite action, which installs failroute, scans, and
|
|
77
|
+
uploads SARIF in one step:
|
|
78
|
+
|
|
79
|
+
```yaml
|
|
80
|
+
- uses: feiiiiii5/failroute/action@main
|
|
81
|
+
with:
|
|
82
|
+
path: src
|
|
83
|
+
exclude: tests/corpus fixtures
|
|
84
|
+
threshold: "0"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Severity mapping: `silent-fallback` → `error`, `no-action` and
|
|
88
|
+
`masked-exception` → `warning`.
|
|
89
|
+
|
|
90
|
+
### Suppressing findings
|
|
91
|
+
|
|
92
|
+
Reviewed-and-accepted handlers can be opted out with a line marker (the
|
|
93
|
+
scanner honors both):
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
try:
|
|
97
|
+
return best_effort()
|
|
98
|
+
except Exception: # failroute: ignore - documented fallback semantics
|
|
99
|
+
return None
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`# pragma: no cover` markers are honored as well (explicitly defensive code).
|
|
103
|
+
|
|
104
|
+
## Examples that trip it
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
def classify(text): # no-action
|
|
108
|
+
try:
|
|
109
|
+
return model.predict(text)
|
|
110
|
+
except Exception:
|
|
111
|
+
pass # 💥 swallowed
|
|
112
|
+
|
|
113
|
+
def score(prompt): # silent-fallback
|
|
114
|
+
try:
|
|
115
|
+
return judge(prompt)
|
|
116
|
+
except Exception:
|
|
117
|
+
return 0.0 # 💥 outage == "0.0 score"
|
|
118
|
+
|
|
119
|
+
def fetch(url): # silent-fallback (assign)
|
|
120
|
+
data = None
|
|
121
|
+
try:
|
|
122
|
+
data = download(url)
|
|
123
|
+
except Exception:
|
|
124
|
+
data = {"items": []} # 💥 error looks like an empty result
|
|
125
|
+
return data
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## What it does *not* flag (by design)
|
|
129
|
+
|
|
130
|
+
* `except KeyboardInterrupt` / `except SystemExit` — normally intentional.
|
|
131
|
+
* Handlers that re-raise unconditionally without a fallback.
|
|
132
|
+
* `except` bodies that log at `warning`/`error` **and** re-raise — the failure
|
|
133
|
+
still propagates; we only flag the success-looking path.
|
|
134
|
+
|
|
135
|
+
Run `failroute` on its own checkout as a smoke test:
|
|
136
|
+
|
|
137
|
+
```console
|
|
138
|
+
$ pip install -e .
|
|
139
|
+
$ failroute --repo . # expected: zero findings (self-hosting)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Benchmarks & validation
|
|
143
|
+
|
|
144
|
+
All numbers below are reproducible from this checkout; nothing here is
|
|
145
|
+
copy-pasted from a run that cannot be re-executed.
|
|
146
|
+
|
|
147
|
+
### Labelled corpus (precision / recall)
|
|
148
|
+
|
|
149
|
+
`tests/corpus/` holds 19 hand-labelled exception handlers (10 positives across
|
|
150
|
+
all three modes, 9 negatives covering re-raise, log-and-raise, derived values,
|
|
151
|
+
dead code, opt-out markers, and non-fallback constants). Ground truth lives in
|
|
152
|
+
`tests/corpus/manifest.json` and was written from the *semantics* of each
|
|
153
|
+
fixture, independently of tool output.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
corpus v1 TP=10 FP=0 FN=0 TN=9
|
|
157
|
+
precision=1.0 recall=1.0
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Re-run: `python tools/benchmark.py` (also enforced by `pytest`).
|
|
161
|
+
|
|
162
|
+
### What syntactic linters miss
|
|
163
|
+
|
|
164
|
+
Against the source packages of 8 real AI/eval repositories (garak,
|
|
165
|
+
inspect_ai, pydantic-ai, uqlm, trl, smolagents, deepteam, fickling),
|
|
166
|
+
failroute reported **647 findings**; ruff's exception-handling rules
|
|
167
|
+
(`S110` try-except-pass, `S112` try-except-continue) reported **80**, of which
|
|
168
|
+
70 overlap failroute's `no-action` mode. The remaining **390 findings are
|
|
169
|
+
silent-fallback / masked-exception handlers** -- failures converted into
|
|
170
|
+
success-looking values -- a class syntactic rules cannot express by
|
|
171
|
+
construction.
|
|
172
|
+
|
|
173
|
+
Re-run: `python tools/compare_ruff.py <repo> [<repo> ...]`.
|
|
174
|
+
Results are checked into `bench/`.
|
|
175
|
+
|
|
176
|
+
## Development
|
|
177
|
+
|
|
178
|
+
```console
|
|
179
|
+
$ pip install -e ".[test]"
|
|
180
|
+
$ pytest
|
|
181
|
+
$ ruff check .
|
|
182
|
+
```
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "failroute"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Static detection of failure-routing anti-patterns in Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "fc" }]
|
|
13
|
+
keywords = ["static-analysis", "exception-handling", "code-quality", "llm", "reliability"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
24
|
+
]
|
|
25
|
+
dependencies = []
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
test = ["pytest>=7"]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
failroute = "failroute.cli:main"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
pythonpath = ["src"]
|
|
39
|
+
addopts = "-q"
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
target-version = "py39"
|
|
43
|
+
line-length = 110
|
|
44
|
+
|
|
45
|
+
[tool.ruff.lint]
|
|
46
|
+
select = ["E", "F", "I", "B", "UP"]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
# Corpus fixtures are intentionally synthetic snippets with undefined names.
|
|
50
|
+
"tests/corpus/*" = ["F821", "B017"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""failroute — static detection of failure-routing anti-patterns in Python.
|
|
2
|
+
|
|
3
|
+
Failure-routing is the practice of converting an underlying failure into a
|
|
4
|
+
*success-like* outcome at the wrong layer: swallowing an exception and
|
|
5
|
+
returning a default truthy/"no error" value, transforming a metric failure
|
|
6
|
+
into a 0.0/False score, or logging-and-continuing where the caller is
|
|
7
|
+
contractually entitled to know the operation failed.
|
|
8
|
+
|
|
9
|
+
This module implements an AST-based scanner that flags those patterns, so
|
|
10
|
+
tests / CI can treat them like the correctness bugs they are.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from failroute.analyzer import FailureMode, Finding, scan_path, scan_repo, scan_source
|
|
14
|
+
from failroute.cli import main
|
|
15
|
+
from failroute.sarif import to_sarif, to_sarif_json
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"FailureMode",
|
|
19
|
+
"Finding",
|
|
20
|
+
"scan_source",
|
|
21
|
+
"scan_path",
|
|
22
|
+
"scan_repo",
|
|
23
|
+
"main",
|
|
24
|
+
"to_sarif",
|
|
25
|
+
"to_sarif_json",
|
|
26
|
+
]
|
|
27
|
+
__version__ = "0.3.0"
|