safelint 1.3.2__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.
- safelint-1.3.2/LICENSE +21 -0
- safelint-1.3.2/PKG-INFO +287 -0
- safelint-1.3.2/README.md +255 -0
- safelint-1.3.2/pyproject.toml +297 -0
- safelint-1.3.2/setup.cfg +4 -0
- safelint-1.3.2/src/safelint/__init__.py +29 -0
- safelint-1.3.2/src/safelint/analysis/__init__.py +8 -0
- safelint-1.3.2/src/safelint/analysis/dataflow.py +174 -0
- safelint-1.3.2/src/safelint/cli.py +463 -0
- safelint-1.3.2/src/safelint/core/__init__.py +8 -0
- safelint-1.3.2/src/safelint/core/config.py +281 -0
- safelint-1.3.2/src/safelint/core/engine.py +292 -0
- safelint-1.3.2/src/safelint/core/runner.py +62 -0
- safelint-1.3.2/src/safelint/py.typed +0 -0
- safelint-1.3.2/src/safelint/rules/__init__.py +72 -0
- safelint-1.3.2/src/safelint/rules/base.py +61 -0
- safelint-1.3.2/src/safelint/rules/complexity.py +55 -0
- safelint-1.3.2/src/safelint/rules/dataflow.py +243 -0
- safelint-1.3.2/src/safelint/rules/documentation.py +35 -0
- safelint-1.3.2/src/safelint/rules/error_handling.py +90 -0
- safelint-1.3.2/src/safelint/rules/function_length.py +39 -0
- safelint-1.3.2/src/safelint/rules/loop_safety.py +43 -0
- safelint-1.3.2/src/safelint/rules/max_arguments.py +40 -0
- safelint-1.3.2/src/safelint/rules/nesting_depth.py +58 -0
- safelint-1.3.2/src/safelint/rules/resource_lifecycle.py +58 -0
- safelint-1.3.2/src/safelint/rules/side_effects.py +96 -0
- safelint-1.3.2/src/safelint/rules/state_purity.py +83 -0
- safelint-1.3.2/src/safelint/rules/test_coverage.py +78 -0
- safelint-1.3.2/src/safelint.egg-info/PKG-INFO +287 -0
- safelint-1.3.2/src/safelint.egg-info/SOURCES.txt +38 -0
- safelint-1.3.2/src/safelint.egg-info/dependency_links.txt +1 -0
- safelint-1.3.2/src/safelint.egg-info/entry_points.txt +2 -0
- safelint-1.3.2/src/safelint.egg-info/requires.txt +12 -0
- safelint-1.3.2/src/safelint.egg-info/top_level.txt +1 -0
- safelint-1.3.2/tests/test_cli.py +176 -0
- safelint-1.3.2/tests/test_config.py +155 -0
- safelint-1.3.2/tests/test_coverage.py +1021 -0
- safelint-1.3.2/tests/test_dataflow.py +494 -0
- safelint-1.3.2/tests/test_engine.py +147 -0
- safelint-1.3.2/tests/test_suppression.py +286 -0
safelint-1.3.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Shelke
|
|
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.
|
safelint-1.3.2/PKG-INFO
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: safelint
|
|
3
|
+
Version: 1.3.2
|
|
4
|
+
Summary: Engineering safety lint rules and pre-commit integration for modern Python codebases
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/shelkesays/safelint
|
|
7
|
+
Project-URL: Repository, https://github.com/shelkesays/safelint
|
|
8
|
+
Project-URL: Issues, https://github.com/shelkesays/safelint/issues
|
|
9
|
+
Keywords: lint,pre-commit,static-analysis,safety,python
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
17
|
+
Classifier: Topic :: Software Development :: Testing
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Provides-Extra: yaml
|
|
22
|
+
Requires-Dist: PyYAML>=6.0; extra == "yaml"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pre-commit>=4.5.1; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest>=9.0.2; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock>=3.15.1; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.15.8; extra == "dev"
|
|
29
|
+
Requires-Dist: ty>=0.0.26; extra == "dev"
|
|
30
|
+
Requires-Dist: PyYAML>=6.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# SafeLint
|
|
34
|
+
|
|
35
|
+
[](https://github.com/shelkesays/safelint/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/safelint/)
|
|
37
|
+
[](https://pypi.org/project/safelint/)
|
|
38
|
+
|
|
39
|
+
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.
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
SafeLint integrates with pre-commit and CI pipelines to prevent unsafe code from entering your codebase.
|
|
44
|
+
|
|
45
|
+
## Why SafeLint?
|
|
46
|
+
|
|
47
|
+
Fast-moving codebases - whether written by humans under pressure or generated by AI tools - tend to drift toward the same failure patterns:
|
|
48
|
+
|
|
49
|
+
- Unbounded loops
|
|
50
|
+
- Silent error handling
|
|
51
|
+
- Hidden side effects
|
|
52
|
+
- Poor resource management
|
|
53
|
+
|
|
54
|
+
SafeLint catches these early, automatically, regardless of who wrote the code.
|
|
55
|
+
|
|
56
|
+
## Philosophy
|
|
57
|
+
|
|
58
|
+
> "When it really counts, it may be worth going the extra mile and living within stricter limits than may be desirable."
|
|
59
|
+
> - Gerard J. Holzmann, NASA/JPL
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Power of Ten - adapted for Python
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
|
|
67
|
+
| # | Holzmann's Rule | SafeLint Rule | Code |
|
|
68
|
+
|---|---|---|---|
|
|
69
|
+
| 1 | No complex control flow - no `goto`, no deep recursion | `nesting_depth`, `complexity` | [SAFE102](CONFIGURATION.md#safe102----nesting_depth), [SAFE104](CONFIGURATION.md#safe104----complexity) |
|
|
70
|
+
| 2 | All loops must have a fixed upper bound | `unbounded_loops` | [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) |
|
|
71
|
+
| 3 | No dynamic memory allocation after startup | - | *(not applicable to Python)* |
|
|
72
|
+
| 4 | Functions must fit on one printed page | `function_length` | [SAFE101](CONFIGURATION.md#safe101----function_length) |
|
|
73
|
+
| 5 | Use at least two assertions per function | `missing_assertions` | [SAFE601](CONFIGURATION.md#safe601----missing_assertions) |
|
|
74
|
+
| 6 | Declare variables at the smallest scope | - | *(Python handles this)* |
|
|
75
|
+
| 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) |
|
|
76
|
+
| 8 | Limit preprocessor use | - | *(not applicable to Python)* |
|
|
77
|
+
| 9 | Restrict pointer use - no chained indirection | `null_dereference` | [SAFE803](CONFIGURATION.md#safe803----null_dereference) |
|
|
78
|
+
| 10 | Compile with all warnings; use static analysis | SafeLint itself | - |
|
|
79
|
+
|
|
80
|
+
Original paper: [spinroot.com/gerard/pdf/P10.pdf](https://spinroot.com/gerard/pdf/P10.pdf)
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Installation
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pip install safelint
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To also support YAML config files (`.safelint.yaml`):
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pip install "safelint[yaml]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
**Check modified files** (default — only files changed since last commit):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
safelint check src/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Check all files** (full scan, e.g. in CI):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
safelint check src/ --all-files
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Check specific files** (pre-commit style):
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
safelint src/mymodule.py src/utils.py
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Fail on warnings too** (useful in CI):
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
safelint check src/ --all-files --fail-on=warning
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Run in CI mode** (warnings become blocking):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
safelint check src/ --all-files --mode=ci
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Ignore specific rules for one run:**
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
safelint check src/ --ignore SAFE203 --ignore side_effects
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Pre-commit integration
|
|
139
|
+
|
|
140
|
+
Add this to your `.pre-commit-config.yaml`:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
repos:
|
|
144
|
+
- repo: https://github.com/shelkesays/safelint
|
|
145
|
+
rev: v1.0.0 # replace with the latest release tag
|
|
146
|
+
hooks:
|
|
147
|
+
- id: safelint
|
|
148
|
+
args: [--fail-on=error] # use --fail-on=warning for stricter CI
|
|
149
|
+
files: ^src/
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Then install the hooks:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pre-commit install
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
SafeLint will now run on every `git commit` and block the commit if it finds errors.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## What it checks
|
|
163
|
+
|
|
164
|
+
| Code | Rule | What it flags |
|
|
165
|
+
|---|---|---|
|
|
166
|
+
| [SAFE101](CONFIGURATION.md#safe101----function_length) | `function_length` | Functions longer than 60 lines |
|
|
167
|
+
| [SAFE102](CONFIGURATION.md#safe102----nesting_depth) | `nesting_depth` | Control flow nested more than 2 levels deep |
|
|
168
|
+
| [SAFE103](CONFIGURATION.md#safe103----max_arguments) | `max_arguments` | Functions with more than 7 parameters |
|
|
169
|
+
| [SAFE104](CONFIGURATION.md#safe104----complexity) | `complexity` | Functions with high cyclomatic complexity |
|
|
170
|
+
| [SAFE201](CONFIGURATION.md#safe201----bare_except) | `bare_except` | `except:` with no exception type |
|
|
171
|
+
| [SAFE202](CONFIGURATION.md#safe202----empty_except) | `empty_except` | `except` blocks that do nothing (`pass`) |
|
|
172
|
+
| [SAFE203](CONFIGURATION.md#safe203----logging_on_error) | `logging_on_error` | Except blocks that swallow errors silently |
|
|
173
|
+
| [SAFE301](CONFIGURATION.md#safe301----global_state) | `global_state` | Use of the `global` keyword inside functions |
|
|
174
|
+
| [SAFE302](CONFIGURATION.md#safe302----global_mutation) | `global_mutation` | Writing to global variables inside functions |
|
|
175
|
+
| [SAFE303](CONFIGURATION.md#safe303----side_effects_hidden) | `side_effects_hidden` | Pure-looking functions that secretly do I/O |
|
|
176
|
+
| [SAFE304](CONFIGURATION.md#safe304----side_effects) | `side_effects` | Functions that call `print`, `open`, etc. without signalling intent |
|
|
177
|
+
| [SAFE401](CONFIGURATION.md#safe401----resource_lifecycle) | `resource_lifecycle` | Files or connections opened outside a `with` block |
|
|
178
|
+
| [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) | `unbounded_loops` | `while True` loops with no `break` |
|
|
179
|
+
|
|
180
|
+
**Dataflow rules** (opt-in, disabled by default):
|
|
181
|
+
|
|
182
|
+
| Code | Rule | What it flags |
|
|
183
|
+
|---|---|---|
|
|
184
|
+
| [SAFE801](CONFIGURATION.md#safe801----tainted_sink) | `tainted_sink` | User input flowing into `eval`, `exec`, `subprocess`, etc. without sanitization |
|
|
185
|
+
| [SAFE802](CONFIGURATION.md#safe802----return_value_ignored) | `return_value_ignored` | Discarding the return value of calls like `subprocess.run` or `file.write` |
|
|
186
|
+
| [SAFE803](CONFIGURATION.md#safe803----null_dereference) | `null_dereference` | Chaining methods directly on calls that can return `None`, e.g. `d.get("key").strip()` |
|
|
187
|
+
|
|
188
|
+
For opt-in rules (`SAFE601`, `SAFE701`, `SAFE702`) and full configuration options for every rule, see [CONFIGURATION.md](CONFIGURATION.md).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Suppressing violations inline
|
|
193
|
+
|
|
194
|
+
Add a `# nosafe` comment to suppress a violation on a specific line without changing global config.
|
|
195
|
+
|
|
196
|
+
**Suppress all violations on a line:**
|
|
197
|
+
```python
|
|
198
|
+
result = eval(user_input) # nosafe
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Suppress a specific rule by code:**
|
|
202
|
+
```python
|
|
203
|
+
while True: # nosafe: SAFE501
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
**Suppress by rule name:**
|
|
208
|
+
```python
|
|
209
|
+
while True: # nosafe: unbounded_loops
|
|
210
|
+
...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Suppress multiple rules at once:**
|
|
214
|
+
```python
|
|
215
|
+
def get_data(conn, q, p1, p2, p3, p4, p5, p6): # nosafe: SAFE101, SAFE103
|
|
216
|
+
...
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
When at least one violation is suppressed, the CLI summary reports the count so suppressions remain visible and auditable. Use `# nosafe` sparingly — it's for line-level exceptions only. For broader suppression use the config-level options:
|
|
220
|
+
|
|
221
|
+
```toml
|
|
222
|
+
# pyproject.toml
|
|
223
|
+
[tool.safelint]
|
|
224
|
+
ignore = ["SAFE203", "side_effects"] # suppress project-wide
|
|
225
|
+
|
|
226
|
+
[tool.safelint.per_file_ignores]
|
|
227
|
+
"tests/**" = ["SAFE101", "SAFE103"] # suppress only for matching files
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
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.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Configuration
|
|
235
|
+
|
|
236
|
+
SafeLint is configured via `[tool.safelint]` in your `pyproject.toml`, or a `.safelint.yaml` file. See [CONFIGURATION.md](CONFIGURATION.md) for all options, defaults, and examples.
|
|
237
|
+
|
|
238
|
+
Ready-to-copy samples:
|
|
239
|
+
|
|
240
|
+
- [examples/sample.pyproject.toml](examples/sample.pyproject.toml) — TOML format (recommended)
|
|
241
|
+
- [examples/sample.safelint.yaml](examples/sample.safelint.yaml) — YAML format (legacy)
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## Development
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# Install with dev dependencies
|
|
249
|
+
pip install -e ".[dev]"
|
|
250
|
+
|
|
251
|
+
# Run tests
|
|
252
|
+
pytest
|
|
253
|
+
|
|
254
|
+
# Run the linter on itself
|
|
255
|
+
safelint check src/
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Releasing to PyPI (Trusted Publishing)
|
|
259
|
+
|
|
260
|
+
This project publishes to PyPI via GitHub Actions using PyPI Trusted Publishing (OIDC). Do not use local `uv publish` username/password auth.
|
|
261
|
+
|
|
262
|
+
One-time setup:
|
|
263
|
+
|
|
264
|
+
1. In PyPI, open your project → **Manage** → **Publishing** → **Add a trusted publisher**.
|
|
265
|
+
2. Use:
|
|
266
|
+
- Owner: `shelkesays`
|
|
267
|
+
- Repository: `safelint`
|
|
268
|
+
- Workflow: `publish.yml`
|
|
269
|
+
- Environment: `pypi`
|
|
270
|
+
3. In GitHub, create an environment named `pypi` in **Settings → Environments**.
|
|
271
|
+
|
|
272
|
+
Release flow:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
# 1) bump version in pyproject.toml
|
|
276
|
+
# 2) commit and push
|
|
277
|
+
git tag vX.Y.Z
|
|
278
|
+
git push origin vX.Y.Z
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Pushing the version tag triggers `.github/workflows/publish.yml`, which builds and publishes to PyPI.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on bug reports, adding new rules, and opening pull requests.
|
safelint-1.3.2/README.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# SafeLint
|
|
2
|
+
|
|
3
|
+
[](https://github.com/shelkesays/safelint/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/safelint/)
|
|
5
|
+
[](https://pypi.org/project/safelint/)
|
|
6
|
+
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
SafeLint integrates with pre-commit and CI pipelines to prevent unsafe code from entering your codebase.
|
|
12
|
+
|
|
13
|
+
## Why SafeLint?
|
|
14
|
+
|
|
15
|
+
Fast-moving codebases - whether written by humans under pressure or generated by AI tools - tend to drift toward the same failure patterns:
|
|
16
|
+
|
|
17
|
+
- Unbounded loops
|
|
18
|
+
- Silent error handling
|
|
19
|
+
- Hidden side effects
|
|
20
|
+
- Poor resource management
|
|
21
|
+
|
|
22
|
+
SafeLint catches these early, automatically, regardless of who wrote the code.
|
|
23
|
+
|
|
24
|
+
## Philosophy
|
|
25
|
+
|
|
26
|
+
> "When it really counts, it may be worth going the extra mile and living within stricter limits than may be desirable."
|
|
27
|
+
> - Gerard J. Holzmann, NASA/JPL
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Power of Ten - adapted for Python
|
|
32
|
+
|
|
33
|
+
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.
|
|
34
|
+
|
|
35
|
+
| # | Holzmann's Rule | SafeLint Rule | Code |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| 1 | No complex control flow - no `goto`, no deep recursion | `nesting_depth`, `complexity` | [SAFE102](CONFIGURATION.md#safe102----nesting_depth), [SAFE104](CONFIGURATION.md#safe104----complexity) |
|
|
38
|
+
| 2 | All loops must have a fixed upper bound | `unbounded_loops` | [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) |
|
|
39
|
+
| 3 | No dynamic memory allocation after startup | - | *(not applicable to Python)* |
|
|
40
|
+
| 4 | Functions must fit on one printed page | `function_length` | [SAFE101](CONFIGURATION.md#safe101----function_length) |
|
|
41
|
+
| 5 | Use at least two assertions per function | `missing_assertions` | [SAFE601](CONFIGURATION.md#safe601----missing_assertions) |
|
|
42
|
+
| 6 | Declare variables at the smallest scope | - | *(Python handles this)* |
|
|
43
|
+
| 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) |
|
|
44
|
+
| 8 | Limit preprocessor use | - | *(not applicable to Python)* |
|
|
45
|
+
| 9 | Restrict pointer use - no chained indirection | `null_dereference` | [SAFE803](CONFIGURATION.md#safe803----null_dereference) |
|
|
46
|
+
| 10 | Compile with all warnings; use static analysis | SafeLint itself | - |
|
|
47
|
+
|
|
48
|
+
Original paper: [spinroot.com/gerard/pdf/P10.pdf](https://spinroot.com/gerard/pdf/P10.pdf)
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install safelint
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To also support YAML config files (`.safelint.yaml`):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install "safelint[yaml]"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
**Check modified files** (default — only files changed since last commit):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
safelint check src/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Check all files** (full scan, e.g. in CI):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
safelint check src/ --all-files
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Check specific files** (pre-commit style):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
safelint src/mymodule.py src/utils.py
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Fail on warnings too** (useful in CI):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
safelint check src/ --all-files --fail-on=warning
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Run in CI mode** (warnings become blocking):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
safelint check src/ --all-files --mode=ci
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Ignore specific rules for one run:**
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
safelint check src/ --ignore SAFE203 --ignore side_effects
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Pre-commit integration
|
|
107
|
+
|
|
108
|
+
Add this to your `.pre-commit-config.yaml`:
|
|
109
|
+
|
|
110
|
+
```yaml
|
|
111
|
+
repos:
|
|
112
|
+
- repo: https://github.com/shelkesays/safelint
|
|
113
|
+
rev: v1.0.0 # replace with the latest release tag
|
|
114
|
+
hooks:
|
|
115
|
+
- id: safelint
|
|
116
|
+
args: [--fail-on=error] # use --fail-on=warning for stricter CI
|
|
117
|
+
files: ^src/
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Then install the hooks:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
pre-commit install
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
SafeLint will now run on every `git commit` and block the commit if it finds errors.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## What it checks
|
|
131
|
+
|
|
132
|
+
| Code | Rule | What it flags |
|
|
133
|
+
|---|---|---|
|
|
134
|
+
| [SAFE101](CONFIGURATION.md#safe101----function_length) | `function_length` | Functions longer than 60 lines |
|
|
135
|
+
| [SAFE102](CONFIGURATION.md#safe102----nesting_depth) | `nesting_depth` | Control flow nested more than 2 levels deep |
|
|
136
|
+
| [SAFE103](CONFIGURATION.md#safe103----max_arguments) | `max_arguments` | Functions with more than 7 parameters |
|
|
137
|
+
| [SAFE104](CONFIGURATION.md#safe104----complexity) | `complexity` | Functions with high cyclomatic complexity |
|
|
138
|
+
| [SAFE201](CONFIGURATION.md#safe201----bare_except) | `bare_except` | `except:` with no exception type |
|
|
139
|
+
| [SAFE202](CONFIGURATION.md#safe202----empty_except) | `empty_except` | `except` blocks that do nothing (`pass`) |
|
|
140
|
+
| [SAFE203](CONFIGURATION.md#safe203----logging_on_error) | `logging_on_error` | Except blocks that swallow errors silently |
|
|
141
|
+
| [SAFE301](CONFIGURATION.md#safe301----global_state) | `global_state` | Use of the `global` keyword inside functions |
|
|
142
|
+
| [SAFE302](CONFIGURATION.md#safe302----global_mutation) | `global_mutation` | Writing to global variables inside functions |
|
|
143
|
+
| [SAFE303](CONFIGURATION.md#safe303----side_effects_hidden) | `side_effects_hidden` | Pure-looking functions that secretly do I/O |
|
|
144
|
+
| [SAFE304](CONFIGURATION.md#safe304----side_effects) | `side_effects` | Functions that call `print`, `open`, etc. without signalling intent |
|
|
145
|
+
| [SAFE401](CONFIGURATION.md#safe401----resource_lifecycle) | `resource_lifecycle` | Files or connections opened outside a `with` block |
|
|
146
|
+
| [SAFE501](CONFIGURATION.md#safe501----unbounded_loops) | `unbounded_loops` | `while True` loops with no `break` |
|
|
147
|
+
|
|
148
|
+
**Dataflow rules** (opt-in, disabled by default):
|
|
149
|
+
|
|
150
|
+
| Code | Rule | What it flags |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| [SAFE801](CONFIGURATION.md#safe801----tainted_sink) | `tainted_sink` | User input flowing into `eval`, `exec`, `subprocess`, etc. without sanitization |
|
|
153
|
+
| [SAFE802](CONFIGURATION.md#safe802----return_value_ignored) | `return_value_ignored` | Discarding the return value of calls like `subprocess.run` or `file.write` |
|
|
154
|
+
| [SAFE803](CONFIGURATION.md#safe803----null_dereference) | `null_dereference` | Chaining methods directly on calls that can return `None`, e.g. `d.get("key").strip()` |
|
|
155
|
+
|
|
156
|
+
For opt-in rules (`SAFE601`, `SAFE701`, `SAFE702`) and full configuration options for every rule, see [CONFIGURATION.md](CONFIGURATION.md).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Suppressing violations inline
|
|
161
|
+
|
|
162
|
+
Add a `# nosafe` comment to suppress a violation on a specific line without changing global config.
|
|
163
|
+
|
|
164
|
+
**Suppress all violations on a line:**
|
|
165
|
+
```python
|
|
166
|
+
result = eval(user_input) # nosafe
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Suppress a specific rule by code:**
|
|
170
|
+
```python
|
|
171
|
+
while True: # nosafe: SAFE501
|
|
172
|
+
...
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Suppress by rule name:**
|
|
176
|
+
```python
|
|
177
|
+
while True: # nosafe: unbounded_loops
|
|
178
|
+
...
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Suppress multiple rules at once:**
|
|
182
|
+
```python
|
|
183
|
+
def get_data(conn, q, p1, p2, p3, p4, p5, p6): # nosafe: SAFE101, SAFE103
|
|
184
|
+
...
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
When at least one violation is suppressed, the CLI summary reports the count so suppressions remain visible and auditable. Use `# nosafe` sparingly — it's for line-level exceptions only. For broader suppression use the config-level options:
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
# pyproject.toml
|
|
191
|
+
[tool.safelint]
|
|
192
|
+
ignore = ["SAFE203", "side_effects"] # suppress project-wide
|
|
193
|
+
|
|
194
|
+
[tool.safelint.per_file_ignores]
|
|
195
|
+
"tests/**" = ["SAFE101", "SAFE103"] # suppress only for matching files
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
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.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
SafeLint is configured via `[tool.safelint]` in your `pyproject.toml`, or a `.safelint.yaml` file. See [CONFIGURATION.md](CONFIGURATION.md) for all options, defaults, and examples.
|
|
205
|
+
|
|
206
|
+
Ready-to-copy samples:
|
|
207
|
+
|
|
208
|
+
- [examples/sample.pyproject.toml](examples/sample.pyproject.toml) — TOML format (recommended)
|
|
209
|
+
- [examples/sample.safelint.yaml](examples/sample.safelint.yaml) — YAML format (legacy)
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Development
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
# Install with dev dependencies
|
|
217
|
+
pip install -e ".[dev]"
|
|
218
|
+
|
|
219
|
+
# Run tests
|
|
220
|
+
pytest
|
|
221
|
+
|
|
222
|
+
# Run the linter on itself
|
|
223
|
+
safelint check src/
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Releasing to PyPI (Trusted Publishing)
|
|
227
|
+
|
|
228
|
+
This project publishes to PyPI via GitHub Actions using PyPI Trusted Publishing (OIDC). Do not use local `uv publish` username/password auth.
|
|
229
|
+
|
|
230
|
+
One-time setup:
|
|
231
|
+
|
|
232
|
+
1. In PyPI, open your project → **Manage** → **Publishing** → **Add a trusted publisher**.
|
|
233
|
+
2. Use:
|
|
234
|
+
- Owner: `shelkesays`
|
|
235
|
+
- Repository: `safelint`
|
|
236
|
+
- Workflow: `publish.yml`
|
|
237
|
+
- Environment: `pypi`
|
|
238
|
+
3. In GitHub, create an environment named `pypi` in **Settings → Environments**.
|
|
239
|
+
|
|
240
|
+
Release flow:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
# 1) bump version in pyproject.toml
|
|
244
|
+
# 2) commit and push
|
|
245
|
+
git tag vX.Y.Z
|
|
246
|
+
git push origin vX.Y.Z
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Pushing the version tag triggers `.github/workflows/publish.yml`, which builds and publishes to PyPI.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Contributing
|
|
254
|
+
|
|
255
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on bug reports, adding new rules, and opening pull requests.
|