python-vibe-guard 0.7.1__tar.gz → 0.9.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.
Files changed (44) hide show
  1. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/PKG-INFO +90 -6
  2. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/README.md +89 -5
  3. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyproject.toml +1 -1
  4. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/PKG-INFO +90 -6
  5. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/SOURCES.txt +7 -1
  6. python_vibe_guard-0.9.0/pyvibe/__init__.py +1 -0
  7. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/analyzer.py +13 -0
  8. python_vibe_guard-0.9.0/pyvibe/autofix.py +31 -0
  9. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/cli.py +50 -0
  10. python_vibe_guard-0.9.0/pyvibe/explain.py +157 -0
  11. python_vibe_guard-0.9.0/pyvibe/rule_docs.py +64 -0
  12. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/async_requests.py +14 -2
  13. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/async_sleep.py +13 -1
  14. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/asyncio_run.py +15 -1
  15. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/base.py +1 -0
  16. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/sqlite_async.py +11 -2
  17. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/subprocess_async.py +43 -1
  18. python_vibe_guard-0.9.0/pyvibe/sarif.py +95 -0
  19. python_vibe_guard-0.9.0/tests/test_explain.py +112 -0
  20. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/tests/test_rules.py +69 -0
  21. python_vibe_guard-0.9.0/tests/test_sarif.py +116 -0
  22. python_vibe_guard-0.7.1/pyvibe/__init__.py +0 -1
  23. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/dependency_links.txt +0 -0
  24. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/entry_points.txt +0 -0
  25. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/python_vibe_guard.egg-info/top_level.txt +0 -0
  26. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/__main__.py +0 -0
  27. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/__init__.py +0 -0
  28. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/celery_time_limit.py +0 -0
  29. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/contextvar_cleanup.py +0 -0
  30. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/create_task_orphan.py +0 -0
  31. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/ensure_future_orphan.py +0 -0
  32. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/gather_no_return_exceptions.py +0 -0
  33. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/httpx_client_sync.py +0 -0
  34. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/httpx_sync.py +0 -0
  35. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/loop_run_until_complete.py +0 -0
  36. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/open_async.py +0 -0
  37. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/os_blocking.py +0 -0
  38. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/queue_put_nowait.py +0 -0
  39. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/retry_no_backoff.py +0 -0
  40. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/silent_except.py +0 -0
  41. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/threading_lock.py +0 -0
  42. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/pyvibe/rules/while_true_no_await.py +0 -0
  43. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/setup.cfg +0 -0
  44. {python_vibe_guard-0.7.1 → python_vibe_guard-0.9.0}/tests/test_exclude.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-vibe-guard
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
5
  License: MIT
6
6
  Keywords: async,linter,fastapi,asyncio,static-analysis
@@ -163,9 +163,16 @@ python -m pyvibe src/
163
163
  # JSON output for CI/CD pipelines
164
164
  python -m pyvibe src/ --json
165
165
 
166
+ # SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
167
+ python -m pyvibe src/ --sarif
168
+ python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
169
+
166
170
  # Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
167
171
  python -m pyvibe src/ --exclude tests
168
172
 
173
+ # Show the research evidence behind a rule (accuracy, false positives, sources)
174
+ python -m pyvibe explain PYVIBE-002
175
+
169
176
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
170
177
  ```
171
178
 
@@ -177,22 +184,29 @@ python -m pyvibe src/ --exclude tests
177
184
 
178
185
  demo/bad_async.py
179
186
 
180
- [CRITICAL] [PYVIBE-001] — line 14
187
+ [CRITICAL] [PYVIBE-001] — line 27
181
188
  Function : process_order()
182
189
  Problem : time.sleep() blocks the entire event loop
183
190
  Fix : Use `await asyncio.sleep(n)` instead
191
+ Suggested fix:
192
+ await asyncio.sleep(2)
184
193
 
185
- [CRITICAL] [PYVIBE-002] — line 20
194
+ [CRITICAL] [PYVIBE-002] — line 33
186
195
  Function : fetch_user()
187
196
  Problem : requests.get() is synchronous — blocks the event loop
188
197
  Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
198
+ Suggested fix:
199
+ async with httpx.AsyncClient() as client:
200
+ response = await client.get(f"https://api.example.com/users/{user_id}")
189
201
 
190
- [CRITICAL] [PYVIBE-003] — line 26
202
+ [CRITICAL] [PYVIBE-003] — line 39
191
203
  Function : orchestrate()
192
204
  Problem : asyncio.run() inside async def raises RuntimeError at runtime
193
205
  Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
206
+ Suggested fix:
207
+ await process_order(42)
194
208
 
195
- [CRITICAL] [PYVIBE-004] — line 32
209
+ [CRITICAL] [PYVIBE-004] — line 45
196
210
  Function : update_counter()
197
211
  Problem : threading.Lock() blocks the event loop under contention
198
212
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
@@ -201,6 +215,46 @@ python -m pyvibe src/ --exclude tests
201
215
  4 violation(s) in 1 file(s)
202
216
  ```
203
217
 
218
+ `Suggested fix:` blocks are generated from the real code on the flagged line — they are
219
+ printed to the terminal / JSON output only and are never written back to your files.
220
+ Currently available for PYVIBE-001, 002, 003, 007, and 008.
221
+
222
+ ### `pyvibe explain`
223
+
224
+ Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
225
+ evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
226
+ audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
227
+
228
+ ```bash
229
+ python -m pyvibe explain PYVIBE-002
230
+ ```
231
+
232
+ ```
233
+ PYVIBE-002 — requests.* inside async def
234
+ ───────────────────────────────────────────────
235
+
236
+ Problema:
237
+ requests.* inside async def
238
+
239
+ Por qué ocurre:
240
+ `requests` is a synchronous HTTP library. Calling it inside an async function
241
+ blocks the OS thread running the event loop. Under concurrent load this
242
+ serialises all I/O and eliminates any benefit of async.
243
+
244
+ Visto en: 4.0% (10/250 repos, sweep-250 dataset)
245
+ Nivel de evidencia: B
246
+ Precisión auditada: ~83%
247
+ Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
248
+
249
+ Fix sugerido:
250
+ use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
251
+
252
+ Full report: research/accepted/PYVIBE-002.md
253
+ ```
254
+
255
+ If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
256
+ and exits non-zero — it never invents data.
257
+
204
258
  ---
205
259
 
206
260
  ## CI/CD integration
@@ -216,6 +270,36 @@ Add to your GitHub Actions workflow:
216
270
 
217
271
  The scanner exits with code `1` when violations are found, failing the CI job.
218
272
 
273
+ ### GitHub Code Scanning (SARIF)
274
+
275
+ ```yaml
276
+ - name: python-vibe-guard scan
277
+ run: |
278
+ pip install python-vibe-guard
279
+ python -m pyvibe src/ --sarif
280
+ continue-on-error: true # let the upload step surface results in the PR instead
281
+
282
+ - name: Upload SARIF
283
+ uses: github/codeql-action/upload-sarif@v3
284
+ with:
285
+ sarif_file: results.sarif
286
+ ```
287
+
288
+ Findings then show up as annotations on the PR diff and in the repo's Security tab,
289
+ each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
290
+
291
+ ---
292
+
293
+ ## GitHub Action
294
+
295
+ - name: python-vibe-guard
296
+ uses: joaquinriosheredia/python-vibe-guard-action@v1
297
+ with:
298
+ path: src/
299
+
300
+ Violations appear directly in GitHub Security tab (SARIF).
301
+ Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
302
+
219
303
  ---
220
304
 
221
305
  ## Pre-commit integration
@@ -268,7 +352,7 @@ python -m pytest tests/ -v
268
352
  python tests/test_rules.py
269
353
  ```
270
354
 
271
- 123 tests: true positives + false-positive guards for every rule.
355
+ 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
272
356
 
273
357
  ---
274
358
 
@@ -148,9 +148,16 @@ python -m pyvibe src/
148
148
  # JSON output for CI/CD pipelines
149
149
  python -m pyvibe src/ --json
150
150
 
151
+ # SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
152
+ python -m pyvibe src/ --sarif
153
+ python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
154
+
151
155
  # Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
152
156
  python -m pyvibe src/ --exclude tests
153
157
 
158
+ # Show the research evidence behind a rule (accuracy, false positives, sources)
159
+ python -m pyvibe explain PYVIBE-002
160
+
154
161
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
155
162
  ```
156
163
 
@@ -162,22 +169,29 @@ python -m pyvibe src/ --exclude tests
162
169
 
163
170
  demo/bad_async.py
164
171
 
165
- [CRITICAL] [PYVIBE-001] — line 14
172
+ [CRITICAL] [PYVIBE-001] — line 27
166
173
  Function : process_order()
167
174
  Problem : time.sleep() blocks the entire event loop
168
175
  Fix : Use `await asyncio.sleep(n)` instead
176
+ Suggested fix:
177
+ await asyncio.sleep(2)
169
178
 
170
- [CRITICAL] [PYVIBE-002] — line 20
179
+ [CRITICAL] [PYVIBE-002] — line 33
171
180
  Function : fetch_user()
172
181
  Problem : requests.get() is synchronous — blocks the event loop
173
182
  Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
183
+ Suggested fix:
184
+ async with httpx.AsyncClient() as client:
185
+ response = await client.get(f"https://api.example.com/users/{user_id}")
174
186
 
175
- [CRITICAL] [PYVIBE-003] — line 26
187
+ [CRITICAL] [PYVIBE-003] — line 39
176
188
  Function : orchestrate()
177
189
  Problem : asyncio.run() inside async def raises RuntimeError at runtime
178
190
  Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
191
+ Suggested fix:
192
+ await process_order(42)
179
193
 
180
- [CRITICAL] [PYVIBE-004] — line 32
194
+ [CRITICAL] [PYVIBE-004] — line 45
181
195
  Function : update_counter()
182
196
  Problem : threading.Lock() blocks the event loop under contention
183
197
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
@@ -186,6 +200,46 @@ python -m pyvibe src/ --exclude tests
186
200
  4 violation(s) in 1 file(s)
187
201
  ```
188
202
 
203
+ `Suggested fix:` blocks are generated from the real code on the flagged line — they are
204
+ printed to the terminal / JSON output only and are never written back to your files.
205
+ Currently available for PYVIBE-001, 002, 003, 007, and 008.
206
+
207
+ ### `pyvibe explain`
208
+
209
+ Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
210
+ evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
211
+ audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
212
+
213
+ ```bash
214
+ python -m pyvibe explain PYVIBE-002
215
+ ```
216
+
217
+ ```
218
+ PYVIBE-002 — requests.* inside async def
219
+ ───────────────────────────────────────────────
220
+
221
+ Problema:
222
+ requests.* inside async def
223
+
224
+ Por qué ocurre:
225
+ `requests` is a synchronous HTTP library. Calling it inside an async function
226
+ blocks the OS thread running the event loop. Under concurrent load this
227
+ serialises all I/O and eliminates any benefit of async.
228
+
229
+ Visto en: 4.0% (10/250 repos, sweep-250 dataset)
230
+ Nivel de evidencia: B
231
+ Precisión auditada: ~83%
232
+ Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
233
+
234
+ Fix sugerido:
235
+ use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
236
+
237
+ Full report: research/accepted/PYVIBE-002.md
238
+ ```
239
+
240
+ If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
241
+ and exits non-zero — it never invents data.
242
+
189
243
  ---
190
244
 
191
245
  ## CI/CD integration
@@ -201,6 +255,36 @@ Add to your GitHub Actions workflow:
201
255
 
202
256
  The scanner exits with code `1` when violations are found, failing the CI job.
203
257
 
258
+ ### GitHub Code Scanning (SARIF)
259
+
260
+ ```yaml
261
+ - name: python-vibe-guard scan
262
+ run: |
263
+ pip install python-vibe-guard
264
+ python -m pyvibe src/ --sarif
265
+ continue-on-error: true # let the upload step surface results in the PR instead
266
+
267
+ - name: Upload SARIF
268
+ uses: github/codeql-action/upload-sarif@v3
269
+ with:
270
+ sarif_file: results.sarif
271
+ ```
272
+
273
+ Findings then show up as annotations on the PR diff and in the repo's Security tab,
274
+ each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
275
+
276
+ ---
277
+
278
+ ## GitHub Action
279
+
280
+ - name: python-vibe-guard
281
+ uses: joaquinriosheredia/python-vibe-guard-action@v1
282
+ with:
283
+ path: src/
284
+
285
+ Violations appear directly in GitHub Security tab (SARIF).
286
+ Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
287
+
204
288
  ---
205
289
 
206
290
  ## Pre-commit integration
@@ -253,7 +337,7 @@ python -m pytest tests/ -v
253
337
  python tests/test_rules.py
254
338
  ```
255
339
 
256
- 123 tests: true positives + false-positive guards for every rule.
340
+ 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
257
341
 
258
342
  ---
259
343
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-vibe-guard"
7
- version = "0.7.1"
7
+ version = "0.9.0"
8
8
  description = "Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-vibe-guard
3
- Version: 0.7.1
3
+ Version: 0.9.0
4
4
  Summary: Runtime anti-pattern scanner for async Python — detects what AI-generated code gets wrong
5
5
  License: MIT
6
6
  Keywords: async,linter,fastapi,asyncio,static-analysis
@@ -163,9 +163,16 @@ python -m pyvibe src/
163
163
  # JSON output for CI/CD pipelines
164
164
  python -m pyvibe src/ --json
165
165
 
166
+ # SARIF 2.1.0 output for GitHub Code Scanning (writes results.sarif)
167
+ python -m pyvibe src/ --sarif
168
+ python -m pyvibe src/ --sarif --sarif-output custom-path.sarif
169
+
166
170
  # Exclude directories (adds to built-in defaults: venv, .venv, __pycache__, …)
167
171
  python -m pyvibe src/ --exclude tests
168
172
 
173
+ # Show the research evidence behind a rule (accuracy, false positives, sources)
174
+ python -m pyvibe explain PYVIBE-002
175
+
169
176
  # Exit code: 0 = clean, 1 = violations found, 2 = path error
170
177
  ```
171
178
 
@@ -177,22 +184,29 @@ python -m pyvibe src/ --exclude tests
177
184
 
178
185
  demo/bad_async.py
179
186
 
180
- [CRITICAL] [PYVIBE-001] — line 14
187
+ [CRITICAL] [PYVIBE-001] — line 27
181
188
  Function : process_order()
182
189
  Problem : time.sleep() blocks the entire event loop
183
190
  Fix : Use `await asyncio.sleep(n)` instead
191
+ Suggested fix:
192
+ await asyncio.sleep(2)
184
193
 
185
- [CRITICAL] [PYVIBE-002] — line 20
194
+ [CRITICAL] [PYVIBE-002] — line 33
186
195
  Function : fetch_user()
187
196
  Problem : requests.get() is synchronous — blocks the event loop
188
197
  Fix : Use `async with httpx.AsyncClient() as c: await c.get(url)`
198
+ Suggested fix:
199
+ async with httpx.AsyncClient() as client:
200
+ response = await client.get(f"https://api.example.com/users/{user_id}")
189
201
 
190
- [CRITICAL] [PYVIBE-003] — line 26
202
+ [CRITICAL] [PYVIBE-003] — line 39
191
203
  Function : orchestrate()
192
204
  Problem : asyncio.run() inside async def raises RuntimeError at runtime
193
205
  Fix : Use `await coroutine()` directly — asyncio.run() is for sync entrypoints only
206
+ Suggested fix:
207
+ await process_order(42)
194
208
 
195
- [CRITICAL] [PYVIBE-004] — line 32
209
+ [CRITICAL] [PYVIBE-004] — line 45
196
210
  Function : update_counter()
197
211
  Problem : threading.Lock() blocks the event loop under contention
198
212
  Fix : Use `asyncio.Lock()` with `async with lock:` instead
@@ -201,6 +215,46 @@ python -m pyvibe src/ --exclude tests
201
215
  4 violation(s) in 1 file(s)
202
216
  ```
203
217
 
218
+ `Suggested fix:` blocks are generated from the real code on the flagged line — they are
219
+ printed to the terminal / JSON output only and are never written back to your files.
220
+ Currently available for PYVIBE-001, 002, 003, 007, and 008.
221
+
222
+ ### `pyvibe explain`
223
+
224
+ Every rule's accuracy claims come from `research/accepted/PYVIBE-XXX.md` — a per-rule
225
+ evidence file with repo-sweep data, an evidence-level grade, and a hit-by-hit precision
226
+ audit. `pyvibe explain` surfaces that in the terminal instead of making you go dig for it:
227
+
228
+ ```bash
229
+ python -m pyvibe explain PYVIBE-002
230
+ ```
231
+
232
+ ```
233
+ PYVIBE-002 — requests.* inside async def
234
+ ───────────────────────────────────────────────
235
+
236
+ Problema:
237
+ requests.* inside async def
238
+
239
+ Por qué ocurre:
240
+ `requests` is a synchronous HTTP library. Calling it inside an async function
241
+ blocks the OS thread running the event loop. Under concurrent load this
242
+ serialises all I/O and eliminates any benefit of async.
243
+
244
+ Visto en: 4.0% (10/250 repos, sweep-250 dataset)
245
+ Nivel de evidencia: B
246
+ Precisión auditada: ~83%
247
+ Falsos positivos conocidos: EXECUTOR_WRAPPER; INNER_SYNC_FUNCTION_EXECUTOR; ...
248
+
249
+ Fix sugerido:
250
+ use `httpx.AsyncClient` or `aiohttp.ClientSession` with await.
251
+
252
+ Full report: research/accepted/PYVIBE-002.md
253
+ ```
254
+
255
+ If a rule has no evidence file, it prints a clear `No evidence file found for PYVIBE-XXX`
256
+ and exits non-zero — it never invents data.
257
+
204
258
  ---
205
259
 
206
260
  ## CI/CD integration
@@ -216,6 +270,36 @@ Add to your GitHub Actions workflow:
216
270
 
217
271
  The scanner exits with code `1` when violations are found, failing the CI job.
218
272
 
273
+ ### GitHub Code Scanning (SARIF)
274
+
275
+ ```yaml
276
+ - name: python-vibe-guard scan
277
+ run: |
278
+ pip install python-vibe-guard
279
+ python -m pyvibe src/ --sarif
280
+ continue-on-error: true # let the upload step surface results in the PR instead
281
+
282
+ - name: Upload SARIF
283
+ uses: github/codeql-action/upload-sarif@v3
284
+ with:
285
+ sarif_file: results.sarif
286
+ ```
287
+
288
+ Findings then show up as annotations on the PR diff and in the repo's Security tab,
289
+ each linking back to its `research/accepted/PYVIBE-XXX.md` evidence file via `helpUri`.
290
+
291
+ ---
292
+
293
+ ## GitHub Action
294
+
295
+ - name: python-vibe-guard
296
+ uses: joaquinriosheredia/python-vibe-guard-action@v1
297
+ with:
298
+ path: src/
299
+
300
+ Violations appear directly in GitHub Security tab (SARIF).
301
+ Link: https://github.com/Joaquinriosheredia/python-vibe-guard-action
302
+
219
303
  ---
220
304
 
221
305
  ## Pre-commit integration
@@ -268,7 +352,7 @@ python -m pytest tests/ -v
268
352
  python tests/test_rules.py
269
353
  ```
270
354
 
271
- 123 tests: true positives + false-positive guards for every rule.
355
+ 226 tests: true positives + false-positive guards for every rule, plus SARIF output and `pyvibe explain` coverage.
272
356
 
273
357
  ---
274
358
 
@@ -8,7 +8,11 @@ python_vibe_guard.egg-info/top_level.txt
8
8
  pyvibe/__init__.py
9
9
  pyvibe/__main__.py
10
10
  pyvibe/analyzer.py
11
+ pyvibe/autofix.py
11
12
  pyvibe/cli.py
13
+ pyvibe/explain.py
14
+ pyvibe/rule_docs.py
15
+ pyvibe/sarif.py
12
16
  pyvibe/rules/__init__.py
13
17
  pyvibe/rules/async_requests.py
14
18
  pyvibe/rules/async_sleep.py
@@ -32,4 +36,6 @@ pyvibe/rules/subprocess_async.py
32
36
  pyvibe/rules/threading_lock.py
33
37
  pyvibe/rules/while_true_no_await.py
34
38
  tests/test_exclude.py
35
- tests/test_rules.py
39
+ tests/test_explain.py
40
+ tests/test_rules.py
41
+ tests/test_sarif.py
@@ -0,0 +1 @@
1
+ __version__ = "0.9.0"
@@ -49,6 +49,17 @@ ALL_RULES = [
49
49
 
50
50
  ALL_RULE_IDS: FrozenSet[str] = frozenset(r.RULE_ID for r in ALL_RULES)
51
51
 
52
+ # Rules that build a concrete "Suggested fix" snippet from the real source
53
+ # text (see pyvibe/autofix.py) and therefore need the full source passed
54
+ # into their constructor.
55
+ _NEEDS_SOURCE: FrozenSet[type] = frozenset({
56
+ AsyncSleepRule,
57
+ AsyncRequestsRule,
58
+ AsyncioRunRule,
59
+ SubprocessAsyncRule,
60
+ SqliteAsyncRule,
61
+ })
62
+
52
63
  # Rules downgraded CRITICAL → WARNING when the violation is inside a test file.
53
64
  # time.sleep in fixtures, subprocess in service-startup helpers, open() in
54
65
  # async test helpers (e.g. asyncssh, aiofiles own test suites), and Celery task
@@ -101,6 +112,8 @@ def analyze_source(
101
112
  for RuleClass in ALL_RULES:
102
113
  if RuleClass is SilentExceptRule:
103
114
  visitor = RuleClass(source_lines)
115
+ elif RuleClass in _NEEDS_SOURCE:
116
+ visitor = RuleClass(source)
104
117
  else:
105
118
  visitor = RuleClass()
106
119
  visitor.visit(tree)
@@ -0,0 +1,31 @@
1
+ """Shared helpers for building concrete "Suggested fix" snippets from real source context.
2
+
3
+ Rules that support autofix suggestions receive the original source text and use
4
+ `ast.get_source_segment` to pull the exact expressions the developer wrote
5
+ (variable names, literals, kwargs) into the suggested replacement, instead of
6
+ a synthetic placeholder like `...`.
7
+ """
8
+ import ast
9
+ from typing import Optional
10
+
11
+
12
+ def render_node(source: str, node: ast.AST) -> Optional[str]:
13
+ """Return the literal source text of an AST node, or None if unavailable."""
14
+ return ast.get_source_segment(source, node)
15
+
16
+
17
+ def render_call_args(source: str, node: ast.Call) -> str:
18
+ """Reconstruct the literal argument-list text of a Call node from source."""
19
+ parts = []
20
+ for arg in node.args:
21
+ if isinstance(arg, ast.Starred):
22
+ seg = render_node(source, arg.value)
23
+ parts.append(f"*{seg}" if seg else "*args")
24
+ else:
25
+ seg = render_node(source, arg)
26
+ parts.append(seg if seg is not None else "...")
27
+ for kw in node.keywords:
28
+ seg = render_node(source, kw.value)
29
+ seg = seg if seg is not None else "..."
30
+ parts.append(f"**{seg}" if kw.arg is None else f"{kw.arg}={seg}")
31
+ return ", ".join(parts)
@@ -5,8 +5,10 @@ python-vibe-guard — runtime anti-pattern scanner for async Python
5
5
  Usage:
6
6
  python -m pyvibe <path> # file or directory
7
7
  python -m pyvibe <path> --json # machine-readable output
8
+ python -m pyvibe <path> --sarif # SARIF 2.1.0 -> results.sarif
8
9
  python -m pyvibe <path> --no-test-files # skip test files entirely
9
10
  python -m pyvibe <path> --downgrade-in-tests # WARNING instead of CRITICAL in all test files
11
+ python -m pyvibe explain PYVIBE-002 # show research evidence for a rule
10
12
  """
11
13
  import argparse
12
14
  import json
@@ -25,6 +27,10 @@ from pyvibe.analyzer import (
25
27
 
26
28
 
27
29
  def main():
30
+ if len(sys.argv) > 1 and sys.argv[1] == "explain":
31
+ _main_explain(sys.argv[2:])
32
+ return
33
+
28
34
  parser = argparse.ArgumentParser(
29
35
  prog="pyvibe",
30
36
  description="Detect runtime anti-patterns in async Python code",
@@ -32,6 +38,17 @@ def main():
32
38
  parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
33
39
  parser.add_argument("path", help="File or directory to scan")
34
40
  parser.add_argument("--json", action="store_true", help="Output as JSON")
41
+ parser.add_argument(
42
+ "--sarif",
43
+ action="store_true",
44
+ help="Also write SARIF 2.1.0 output (for GitHub Code Scanning)",
45
+ )
46
+ parser.add_argument(
47
+ "--sarif-output",
48
+ metavar="PATH",
49
+ default="results.sarif",
50
+ help="Path to write SARIF output to (default: results.sarif)",
51
+ )
35
52
  parser.add_argument(
36
53
  "--exclude",
37
54
  metavar="DIR",
@@ -97,6 +114,12 @@ def main():
97
114
  total_violations = sum(len(v) for v in file_results.values())
98
115
  total_files = sum(1 for v in file_results.values() if v)
99
116
 
117
+ if args.sarif:
118
+ from pyvibe.sarif import write_sarif
119
+
120
+ write_sarif(file_results, args.sarif_output)
121
+ print(f"SARIF results written to {args.sarif_output}")
122
+
100
123
  if args.json:
101
124
  output = []
102
125
  for path, violations in file_results.items():
@@ -109,6 +132,7 @@ def main():
109
132
  "function": v.function_name,
110
133
  "message": v.message,
111
134
  "evidence": v.evidence,
135
+ "suggested_fix": v.suggested_fix,
112
136
  })
113
137
  print(json.dumps(output, indent=2))
114
138
  else:
@@ -137,6 +161,10 @@ def _print_human(file_results: dict, total_violations: int, total_files: int):
137
161
  print(f" Function : {v.function_name}()")
138
162
  print(f" Problem : {v.message}")
139
163
  print(f" Fix : {v.evidence}")
164
+ if v.suggested_fix:
165
+ print(" Suggested fix:")
166
+ for line in v.suggested_fix.splitlines():
167
+ print(f" {line}")
140
168
  print()
141
169
 
142
170
  print(" ─────────────────────────────────────────────")
@@ -144,5 +172,27 @@ def _print_human(file_results: dict, total_violations: int, total_files: int):
144
172
  print()
145
173
 
146
174
 
175
+ def _main_explain(argv):
176
+ parser = argparse.ArgumentParser(
177
+ prog="pyvibe explain",
178
+ description="Show the research evidence behind a python-vibe-guard rule",
179
+ )
180
+ parser.add_argument("rule_id", help="Rule ID, e.g. PYVIBE-002")
181
+ args = parser.parse_args(argv)
182
+
183
+ from pyvibe.explain import EvidenceNotFoundError, explain_rule
184
+
185
+ try:
186
+ text = explain_rule(args.rule_id)
187
+ except EvidenceNotFoundError as e:
188
+ print(str(e))
189
+ sys.exit(1)
190
+
191
+ print()
192
+ print(text)
193
+ print()
194
+ sys.exit(0)
195
+
196
+
147
197
  if __name__ == "__main__":
148
198
  main()