vibe-code-checker 2.0.0__py3-none-any.whl
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.
- analyzers/__init__.py +39 -0
- analyzers/cosmetic.py +373 -0
- analyzers/multilang.py +373 -0
- analyzers/native_fast.py +87 -0
- analyzers/python_ast.py +890 -0
- analyzers/rules.json +211 -0
- analyzers/walker.py +86 -0
- autofix/__init__.py +6 -0
- autofix/engine.py +111 -0
- autofix/gate.py +44 -0
- autofix/zen_client.py +57 -0
- check.py +279 -0
- reporters/__init__.py +6 -0
- reporters/llm_report.py +150 -0
- reporters/terminal.py +137 -0
- vibe_code_checker-2.0.0.dist-info/METADATA +384 -0
- vibe_code_checker-2.0.0.dist-info/RECORD +21 -0
- vibe_code_checker-2.0.0.dist-info/WHEEL +5 -0
- vibe_code_checker-2.0.0.dist-info/entry_points.txt +4 -0
- vibe_code_checker-2.0.0.dist-info/licenses/LICENSE +21 -0
- vibe_code_checker-2.0.0.dist-info/top_level.txt +4 -0
analyzers/multilang.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""VCC Multi-Language & Config Analyzer.
|
|
2
|
+
|
|
3
|
+
Detects bugs and bad practices in JSON, YAML, TOML, JS/TS, HTML, CSS, Dockerfile, and Shell scripts.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import tomllib
|
|
13
|
+
except ImportError:
|
|
14
|
+
tomllib = None
|
|
15
|
+
|
|
16
|
+
# Regex patterns for various file types
|
|
17
|
+
JS_LOOSE_EQ = re.compile(r"(?<![!=<>&|])==(?![=])|(?<![!=])!=(?![=])")
|
|
18
|
+
JS_DEBUGGER = re.compile(r"\bdebugger\s*;?")
|
|
19
|
+
JS_EVAL = re.compile(r"\beval\s*\(")
|
|
20
|
+
JS_CONSOLE = re.compile(r"\bconsole\.(log|debug|info)\s*\(")
|
|
21
|
+
JS_VAR = re.compile(r"\bvar\s+([a-zA-Z_$][0-9a-zA-Z_$]*)\s*=")
|
|
22
|
+
|
|
23
|
+
HTML_IMG_NO_ALT = re.compile(r"<img\b(?![^>]*\balt=)[^>]*>", re.I)
|
|
24
|
+
HTML_EMPTY_HREF = re.compile(r'<a\b[^>]*\bhref=["\'](?:#|javascript:void\(0\)|)["\'][^>]*>', re.I)
|
|
25
|
+
HTML_INLINE_EVENT = re.compile(r'\b(on(?:click|load|error|change|submit|mouseover|keydown))\s*=\s*["\']', re.I)
|
|
26
|
+
|
|
27
|
+
CSS_EMPTY_RULE = re.compile(r"([^{}]+)\s*\{\s*\}")
|
|
28
|
+
CSS_INVALID_HEX = re.compile(r"#(?:[0-9a-fA-F]{1,2}|[0-9a-fA-F]{5}|[0-9a-fA-F]{7}|[0-9a-fA-F]{9,})\b")
|
|
29
|
+
|
|
30
|
+
DOCKER_LATEST = re.compile(r"^\s*FROM\s+([^\s:]+)(?::latest)?\s*$", re.I)
|
|
31
|
+
DOCKER_APT_DIRTY = re.compile(r"apt-get\s+update\b(?!.*rm\s+-rf\s+/var/lib/apt/lists)", re.I | re.DOTALL)
|
|
32
|
+
DOCKER_SENSITIVE = re.compile(r"^\s*(?:COPY|ADD)\s+.*\b(\.env|\.pem|\.key|id_rsa|credentials)\b", re.I)
|
|
33
|
+
|
|
34
|
+
SHELL_DANGEROUS_RM = re.compile(r"\brm\s+-[a-zA-Z]*r[a-zA-Z]*f?[a-zA-Z]*\s+\$([a-zA-Z_][a-zA-Z0-9_]*)")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_snippet(lines: list[str], lineno: int, col_offset: int = 0, ctx: int = 2) -> str:
|
|
38
|
+
start = max(0, lineno - ctx - 1)
|
|
39
|
+
end = min(len(lines), lineno + ctx)
|
|
40
|
+
out = []
|
|
41
|
+
for i in range(start, end):
|
|
42
|
+
ln = i + 1
|
|
43
|
+
line_text = lines[i]
|
|
44
|
+
prefix = ">" if ln == lineno else " "
|
|
45
|
+
out.append(f"{prefix} {ln:4d} | {line_text}")
|
|
46
|
+
if ln == lineno and col_offset > 0:
|
|
47
|
+
indent = " " * (col_offset + 9)
|
|
48
|
+
out.append(f"{indent}^")
|
|
49
|
+
return "\n".join(out)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def analyze_multilang(path: str, rules: dict) -> list[dict]:
|
|
53
|
+
findings: list[dict] = []
|
|
54
|
+
sev = rules.get("severity", {})
|
|
55
|
+
categories = rules.get("categories", {})
|
|
56
|
+
fname = os.path.basename(path).lower()
|
|
57
|
+
ext = os.path.splitext(path)[1].lower()
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
61
|
+
content = f.read()
|
|
62
|
+
except Exception:
|
|
63
|
+
return []
|
|
64
|
+
|
|
65
|
+
lines = content.splitlines()
|
|
66
|
+
|
|
67
|
+
# --- JSON Files ---
|
|
68
|
+
if ext == ".json":
|
|
69
|
+
try:
|
|
70
|
+
json.loads(content)
|
|
71
|
+
except json.JSONDecodeError as e:
|
|
72
|
+
rule = "broken-json-syntax"
|
|
73
|
+
findings.append({
|
|
74
|
+
"rule": rule,
|
|
75
|
+
"severity": sev.get(rule, "P0"),
|
|
76
|
+
"category": "syntax",
|
|
77
|
+
"file": path,
|
|
78
|
+
"line": e.lineno,
|
|
79
|
+
"col": e.colno,
|
|
80
|
+
"title": f"Invalid JSON syntax: {e.msg}",
|
|
81
|
+
"description": f"Failed to parse JSON: {e.msg} at line {e.lineno}, column {e.colno}.",
|
|
82
|
+
"evidence": get_snippet(lines, e.lineno, e.colno),
|
|
83
|
+
"confidence": "high",
|
|
84
|
+
"fix_suggestion": "Fix JSON syntax error (check for trailing commas, unquoted keys, single quotes).",
|
|
85
|
+
})
|
|
86
|
+
return findings
|
|
87
|
+
|
|
88
|
+
# --- YAML Files ---
|
|
89
|
+
if ext in (".yaml", ".yml"):
|
|
90
|
+
for idx, line in enumerate(lines, 1):
|
|
91
|
+
if "\t" in line and (line.startswith("\t") or re.match(r"^\s*\t", line)):
|
|
92
|
+
rule = "broken-yaml-syntax"
|
|
93
|
+
findings.append({
|
|
94
|
+
"rule": rule,
|
|
95
|
+
"severity": sev.get(rule, "P0"),
|
|
96
|
+
"category": "syntax",
|
|
97
|
+
"file": path,
|
|
98
|
+
"line": idx,
|
|
99
|
+
"col": line.find("\t"),
|
|
100
|
+
"title": "Tab indentation in YAML file (strictly forbidden by YAML spec)",
|
|
101
|
+
"description": "YAML specification does not allow tab characters for indentation. Tabs cause YAML parsers to fail.",
|
|
102
|
+
"evidence": get_snippet(lines, idx, line.find("\t")),
|
|
103
|
+
"confidence": "high",
|
|
104
|
+
"fix_suggestion": "Replace all tabs with spaces.",
|
|
105
|
+
})
|
|
106
|
+
return findings
|
|
107
|
+
|
|
108
|
+
# --- TOML Files ---
|
|
109
|
+
if ext == ".toml" and tomllib is not None:
|
|
110
|
+
try:
|
|
111
|
+
tomllib.loads(content)
|
|
112
|
+
except tomllib.TOMLDecodeError as e:
|
|
113
|
+
rule = "broken-toml-syntax"
|
|
114
|
+
findings.append({
|
|
115
|
+
"rule": rule,
|
|
116
|
+
"severity": sev.get(rule, "P0"),
|
|
117
|
+
"category": "syntax",
|
|
118
|
+
"file": path,
|
|
119
|
+
"line": 1,
|
|
120
|
+
"col": 0,
|
|
121
|
+
"title": f"Invalid TOML syntax: {e}",
|
|
122
|
+
"description": f"Failed to parse TOML file: {e}",
|
|
123
|
+
"evidence": content[:200],
|
|
124
|
+
"confidence": "high",
|
|
125
|
+
"fix_suggestion": "Correct TOML syntax.",
|
|
126
|
+
})
|
|
127
|
+
return findings
|
|
128
|
+
|
|
129
|
+
# --- JavaScript / TypeScript ---
|
|
130
|
+
if ext in (".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"):
|
|
131
|
+
for idx, line in enumerate(lines, 1):
|
|
132
|
+
stripped = line.strip()
|
|
133
|
+
if stripped.startswith("//") or stripped.startswith("/*"):
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
# Loose equality (== or !=)
|
|
137
|
+
m_eq = JS_LOOSE_EQ.search(line)
|
|
138
|
+
if m_eq:
|
|
139
|
+
rule = "js-loose-equality"
|
|
140
|
+
findings.append({
|
|
141
|
+
"rule": rule,
|
|
142
|
+
"severity": sev.get(rule, "P1"),
|
|
143
|
+
"category": "multilang",
|
|
144
|
+
"file": path,
|
|
145
|
+
"line": idx,
|
|
146
|
+
"col": m_eq.start(),
|
|
147
|
+
"title": f"Loose equality operator `{m_eq.group(0)}` in JS/TS",
|
|
148
|
+
"description": "Loose equality (`==` and `!=`) performs implicit type coercion, leading to unexpected truthiness bugs.",
|
|
149
|
+
"evidence": get_snippet(lines, idx, m_eq.start()),
|
|
150
|
+
"confidence": "high",
|
|
151
|
+
"fix_suggestion": f"Replace `{m_eq.group(0)}` with `{'===' if '==' in m_eq.group(0) else '!=='}`.",
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
# Debugger statement
|
|
155
|
+
m_dbg = JS_DEBUGGER.search(line)
|
|
156
|
+
if m_dbg:
|
|
157
|
+
rule = "js-debugger-statement"
|
|
158
|
+
findings.append({
|
|
159
|
+
"rule": rule,
|
|
160
|
+
"severity": sev.get(rule, "P1"),
|
|
161
|
+
"category": "multilang",
|
|
162
|
+
"file": path,
|
|
163
|
+
"line": idx,
|
|
164
|
+
"col": m_dbg.start(),
|
|
165
|
+
"title": "`debugger;` statement left in code",
|
|
166
|
+
"description": "Debugger statements pause code execution in browser/node debuggers and should not ship to production.",
|
|
167
|
+
"evidence": get_snippet(lines, idx, m_dbg.start()),
|
|
168
|
+
"confidence": "high",
|
|
169
|
+
"fix_suggestion": "Remove `debugger;` statement.",
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
# eval() usage
|
|
173
|
+
m_eval = JS_EVAL.search(line)
|
|
174
|
+
if m_eval:
|
|
175
|
+
rule = "js-eval-usage"
|
|
176
|
+
findings.append({
|
|
177
|
+
"rule": rule,
|
|
178
|
+
"severity": sev.get(rule, "P1"),
|
|
179
|
+
"category": "multilang",
|
|
180
|
+
"file": path,
|
|
181
|
+
"line": idx,
|
|
182
|
+
"col": m_eval.start(),
|
|
183
|
+
"title": "`eval()` used in JavaScript/TypeScript",
|
|
184
|
+
"description": "eval() executes strings as code, posing serious security and performance risks.",
|
|
185
|
+
"evidence": get_snippet(lines, idx, m_eval.start()),
|
|
186
|
+
"confidence": "high",
|
|
187
|
+
"fix_suggestion": "Replace `eval()` with structured JSON parsing or functions.",
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
return findings
|
|
191
|
+
|
|
192
|
+
# --- HTML Files ---
|
|
193
|
+
if ext in (".html", ".htm"):
|
|
194
|
+
for idx, line in enumerate(lines, 1):
|
|
195
|
+
if HTML_IMG_NO_ALT.search(line):
|
|
196
|
+
rule = "html-img-missing-alt"
|
|
197
|
+
findings.append({
|
|
198
|
+
"rule": rule,
|
|
199
|
+
"severity": sev.get(rule, "P2"),
|
|
200
|
+
"category": "multilang",
|
|
201
|
+
"file": path,
|
|
202
|
+
"line": idx,
|
|
203
|
+
"col": 0,
|
|
204
|
+
"title": "`<img>` tag missing `alt` attribute",
|
|
205
|
+
"description": "Images without `alt` attributes degrade web accessibility (WCAG) and SEO.",
|
|
206
|
+
"evidence": get_snippet(lines, idx),
|
|
207
|
+
"confidence": "high",
|
|
208
|
+
"fix_suggestion": "Add descriptive `alt=\"...\"` or `alt=\"\"` for decorative images.",
|
|
209
|
+
})
|
|
210
|
+
if HTML_EMPTY_HREF.search(line):
|
|
211
|
+
rule = "html-empty-href"
|
|
212
|
+
findings.append({
|
|
213
|
+
"rule": rule,
|
|
214
|
+
"severity": sev.get(rule, "P2"),
|
|
215
|
+
"category": "multilang",
|
|
216
|
+
"file": path,
|
|
217
|
+
"line": idx,
|
|
218
|
+
"col": 0,
|
|
219
|
+
"title": "Anchor `<a>` with empty or `#` href",
|
|
220
|
+
"description": "Anchors with `#` href cause page scrolling to top. Use `<button>` for interactive actions.",
|
|
221
|
+
"evidence": get_snippet(lines, idx),
|
|
222
|
+
"confidence": "med",
|
|
223
|
+
"fix_suggestion": "Use `<button type=\"button\">` instead of `<a href=\"#\">`.",
|
|
224
|
+
})
|
|
225
|
+
m_ev = HTML_INLINE_EVENT.search(line)
|
|
226
|
+
if m_ev:
|
|
227
|
+
rule = "html-inline-event-handler"
|
|
228
|
+
findings.append({
|
|
229
|
+
"rule": rule,
|
|
230
|
+
"severity": sev.get(rule, "P2"),
|
|
231
|
+
"category": "multilang",
|
|
232
|
+
"file": path,
|
|
233
|
+
"line": idx,
|
|
234
|
+
"col": m_ev.start(),
|
|
235
|
+
"title": f"Inline event handler `{m_ev.group(1)}` in HTML",
|
|
236
|
+
"description": "Inline event handlers violate Content Security Policy (CSP) and increase XSS risk.",
|
|
237
|
+
"evidence": get_snippet(lines, idx, m_ev.start()),
|
|
238
|
+
"confidence": "high",
|
|
239
|
+
"fix_suggestion": "Attach event listeners using `.addEventListener()` in external script.",
|
|
240
|
+
})
|
|
241
|
+
return findings
|
|
242
|
+
|
|
243
|
+
# --- CSS Files ---
|
|
244
|
+
if ext in (".css", ".scss"):
|
|
245
|
+
for idx, line in enumerate(lines, 1):
|
|
246
|
+
m_empty = CSS_EMPTY_RULE.search(line)
|
|
247
|
+
if m_empty:
|
|
248
|
+
rule = "css-empty-ruleset"
|
|
249
|
+
findings.append({
|
|
250
|
+
"rule": rule,
|
|
251
|
+
"severity": sev.get(rule, "P2"),
|
|
252
|
+
"category": "multilang",
|
|
253
|
+
"file": path,
|
|
254
|
+
"line": idx,
|
|
255
|
+
"col": m_empty.start(),
|
|
256
|
+
"title": f"Empty CSS ruleset `{m_empty.group(1).strip()}`",
|
|
257
|
+
"description": "Empty style rulesets add dead bytes with zero visual effect.",
|
|
258
|
+
"evidence": get_snippet(lines, idx),
|
|
259
|
+
"confidence": "high",
|
|
260
|
+
"fix_suggestion": "Remove empty ruleset.",
|
|
261
|
+
})
|
|
262
|
+
m_hex = CSS_INVALID_HEX.search(line)
|
|
263
|
+
if m_hex:
|
|
264
|
+
rule = "css-invalid-hex-color"
|
|
265
|
+
findings.append({
|
|
266
|
+
"rule": rule,
|
|
267
|
+
"severity": sev.get(rule, "P2"),
|
|
268
|
+
"category": "multilang",
|
|
269
|
+
"file": path,
|
|
270
|
+
"line": idx,
|
|
271
|
+
"col": m_hex.start(),
|
|
272
|
+
"title": f"Invalid hex color `{m_hex.group(0)}` in CSS",
|
|
273
|
+
"description": "Hex colors must be 3, 4, 6, or 8 hexadecimal digits.",
|
|
274
|
+
"evidence": get_snippet(lines, idx, m_hex.start()),
|
|
275
|
+
"confidence": "high",
|
|
276
|
+
"fix_suggestion": "Correct hex color length.",
|
|
277
|
+
})
|
|
278
|
+
return findings
|
|
279
|
+
|
|
280
|
+
# --- Dockerfile ---
|
|
281
|
+
if fname in ("dockerfile", "dockerfile.dev", "dockerfile.prod") or ext == ".dockerfile":
|
|
282
|
+
has_user = False
|
|
283
|
+
for idx, line in enumerate(lines, 1):
|
|
284
|
+
if re.match(r"^\s*USER\s+", line, re.I) and not re.match(r"^\s*USER\s+root\b", line, re.I):
|
|
285
|
+
has_user = True
|
|
286
|
+
m_lat = DOCKER_LATEST.search(line)
|
|
287
|
+
if m_lat:
|
|
288
|
+
rule = "docker-latest-tag"
|
|
289
|
+
findings.append({
|
|
290
|
+
"rule": rule,
|
|
291
|
+
"severity": sev.get(rule, "P1"),
|
|
292
|
+
"category": "config",
|
|
293
|
+
"file": path,
|
|
294
|
+
"line": idx,
|
|
295
|
+
"col": 0,
|
|
296
|
+
"title": f"Unpinned `:latest` or unversioned Docker image `{m_lat.group(1)}`",
|
|
297
|
+
"description": "Using `:latest` or omitting image tags causes non-deterministic builds and breaking upgrades.",
|
|
298
|
+
"evidence": get_snippet(lines, idx),
|
|
299
|
+
"confidence": "high",
|
|
300
|
+
"fix_suggestion": f"Pin image to a specific version or digest (e.g. `FROM {m_lat.group(1)}:1.2-slim`).",
|
|
301
|
+
})
|
|
302
|
+
m_sens = DOCKER_SENSITIVE.search(line)
|
|
303
|
+
if m_sens:
|
|
304
|
+
rule = "docker-sensitive-copy"
|
|
305
|
+
findings.append({
|
|
306
|
+
"rule": rule,
|
|
307
|
+
"severity": sev.get(rule, "P1"),
|
|
308
|
+
"category": "config",
|
|
309
|
+
"file": path,
|
|
310
|
+
"line": idx,
|
|
311
|
+
"col": 0,
|
|
312
|
+
"title": f"Sensitive file `{m_sens.group(1)}` copied into Docker image",
|
|
313
|
+
"description": "Secrets, keys, or .env files copied into Docker images leak through layers and public registries.",
|
|
314
|
+
"evidence": get_snippet(lines, idx),
|
|
315
|
+
"confidence": "high",
|
|
316
|
+
"fix_suggestion": "Inject secrets at runtime via environment variables or secret mounts.",
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
if not has_user:
|
|
320
|
+
rule = "docker-root-user"
|
|
321
|
+
findings.append({
|
|
322
|
+
"rule": rule,
|
|
323
|
+
"severity": sev.get(rule, "P1"),
|
|
324
|
+
"category": "config",
|
|
325
|
+
"file": path,
|
|
326
|
+
"line": len(lines) or 1,
|
|
327
|
+
"col": 0,
|
|
328
|
+
"title": "Missing non-root `USER` declaration in Dockerfile",
|
|
329
|
+
"description": "Containers running as root allow container breakout risks. Define a non-root user.",
|
|
330
|
+
"evidence": "[End of Dockerfile without USER declaration]",
|
|
331
|
+
"confidence": "med",
|
|
332
|
+
"fix_suggestion": "Add `USER nonroot` or `USER 1001` before ENTRYPOINT/CMD.",
|
|
333
|
+
})
|
|
334
|
+
return findings
|
|
335
|
+
|
|
336
|
+
# --- Shell Scripts ---
|
|
337
|
+
if ext in (".sh", ".bash"):
|
|
338
|
+
has_set_e = any(re.match(r"^\s*set\s+-[a-zA-Z]*e", ln) for ln in lines)
|
|
339
|
+
if not has_set_e:
|
|
340
|
+
rule = "shell-missing-strict"
|
|
341
|
+
findings.append({
|
|
342
|
+
"rule": rule,
|
|
343
|
+
"severity": sev.get(rule, "P1"),
|
|
344
|
+
"category": "config",
|
|
345
|
+
"file": path,
|
|
346
|
+
"line": 1,
|
|
347
|
+
"col": 0,
|
|
348
|
+
"title": "Missing `set -e` or strict mode in shell script",
|
|
349
|
+
"description": "Shell scripts without `set -e` continue running after failed commands, risking cascading failures.",
|
|
350
|
+
"evidence": get_snippet(lines, 1),
|
|
351
|
+
"confidence": "med",
|
|
352
|
+
"fix_suggestion": "Add `set -euo pipefail` near the top of the script.",
|
|
353
|
+
})
|
|
354
|
+
for idx, line in enumerate(lines, 1):
|
|
355
|
+
m_rm = SHELL_DANGEROUS_RM.search(line)
|
|
356
|
+
if m_rm:
|
|
357
|
+
rule = "shell-dangerous-rm"
|
|
358
|
+
findings.append({
|
|
359
|
+
"rule": rule,
|
|
360
|
+
"severity": sev.get(rule, "P1"),
|
|
361
|
+
"category": "config",
|
|
362
|
+
"file": path,
|
|
363
|
+
"line": idx,
|
|
364
|
+
"col": m_rm.start(),
|
|
365
|
+
"title": f"Unquoted variable in `rm -rf ${m_rm.group(1)}`",
|
|
366
|
+
"description": f"If `${m_rm.group(1)}` is empty or unset, this executes `rm -rf /`, destroying the filesystem.",
|
|
367
|
+
"evidence": get_snippet(lines, idx, m_rm.start()),
|
|
368
|
+
"confidence": "high",
|
|
369
|
+
"fix_suggestion": f"Quote the variable: `rm -rf \"${m_rm.group(1)}\"` and check variable is set first.",
|
|
370
|
+
})
|
|
371
|
+
return findings
|
|
372
|
+
|
|
373
|
+
return findings
|
analyzers/native_fast.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""VCC Native Fast Analyzer — Integrates with high-performance native linters (Ruff) if available.
|
|
2
|
+
|
|
3
|
+
Transparently accelerates Python analysis and augments VCC's custom AST engine.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
|
|
12
|
+
RUFF_BIN = shutil.which("ruff") or (
|
|
13
|
+
os.path.expanduser("~/.local/bin/ruff") if os.path.exists(os.path.expanduser("~/.local/bin/ruff")) else None
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def has_ruff() -> bool:
|
|
18
|
+
return RUFF_BIN is not None and os.access(RUFF_BIN, os.X_OK)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def run_ruff(target: str, rules: dict) -> list[dict]:
|
|
22
|
+
if not has_ruff():
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
sev = rules.get("severity", {})
|
|
26
|
+
categories = rules.get("categories", {})
|
|
27
|
+
findings: list[dict] = []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
skip_dirs = [
|
|
31
|
+
".git", "__pycache__", ".pytest_cache", ".ruff_cache",
|
|
32
|
+
".venv", "venv", "env", ".env", "node_modules", "dist",
|
|
33
|
+
"build", ".mypy_cache", "repros", "fixes", "vibe code checker"
|
|
34
|
+
]
|
|
35
|
+
cmd = [
|
|
36
|
+
RUFF_BIN,
|
|
37
|
+
"check",
|
|
38
|
+
"--output-format", "json",
|
|
39
|
+
"--no-cache",
|
|
40
|
+
"--select", "E,F,W,B,S,ASYNC",
|
|
41
|
+
"--exclude", ",".join(skip_dirs),
|
|
42
|
+
target,
|
|
43
|
+
]
|
|
44
|
+
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=15)
|
|
45
|
+
raw_output = proc.stdout.strip()
|
|
46
|
+
if not raw_output:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
data = json.loads(raw_output)
|
|
50
|
+
for item in data:
|
|
51
|
+
code = item.get("code", "")
|
|
52
|
+
msg = item.get("message", "")
|
|
53
|
+
filename = item.get("filename", "")
|
|
54
|
+
location = item.get("location", {})
|
|
55
|
+
row = location.get("row", 1)
|
|
56
|
+
col = location.get("column", 0)
|
|
57
|
+
|
|
58
|
+
# Map Ruff severity
|
|
59
|
+
if code.startswith(("F821", "F822", "F823", "E999", "S")):
|
|
60
|
+
severity = "P0"
|
|
61
|
+
category = "security" if code.startswith("S") else "logic"
|
|
62
|
+
elif code.startswith(("B", "ASYNC", "E711", "E712", "F")):
|
|
63
|
+
severity = "P1"
|
|
64
|
+
category = "async" if code.startswith("ASYNC") else "logic"
|
|
65
|
+
else:
|
|
66
|
+
severity = "P2"
|
|
67
|
+
category = "cosmetic"
|
|
68
|
+
|
|
69
|
+
rule_name = f"ruff-{code.lower()}" if code else "ruff-lint"
|
|
70
|
+
findings.append({
|
|
71
|
+
"rule": rule_name,
|
|
72
|
+
"severity": sev.get(rule_name, severity),
|
|
73
|
+
"category": categories.get(rule_name, category),
|
|
74
|
+
"file": filename,
|
|
75
|
+
"line": row,
|
|
76
|
+
"col": col,
|
|
77
|
+
"title": f"[{code}] {msg}",
|
|
78
|
+
"description": f"Ruff reported: {msg}",
|
|
79
|
+
"evidence": f"Line {row}, Col {col}: {msg}",
|
|
80
|
+
"confidence": "high",
|
|
81
|
+
"fix_suggestion": item.get("fix", {}).get("message", "Review and fix lint warning."),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
return findings
|