vibe-code-checker 2.0.0__tar.gz → 2.0.1__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.
- {vibe_code_checker-2.0.0/vibe_code_checker.egg-info → vibe_code_checker-2.0.1}/PKG-INFO +23 -8
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/README.md +22 -7
- vibe_code_checker-2.0.1/analyzers/cosmetic.py +452 -0
- vibe_code_checker-2.0.1/analyzers/multilang.py +422 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/native_fast.py +8 -5
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/python_ast.py +283 -162
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/rules.json +1 -1
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/engine.py +2 -3
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/zen_client.py +21 -15
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/check.py +96 -61
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/pyproject.toml +1 -1
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/llm_report.py +81 -53
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/rules.json +1 -1
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/tests/test_vcc.py +3 -4
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1/vibe_code_checker.egg-info}/PKG-INFO +23 -8
- vibe_code_checker-2.0.0/analyzers/cosmetic.py +0 -373
- vibe_code_checker-2.0.0/analyzers/multilang.py +0 -373
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/LICENSE +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/MANIFEST.in +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/__init__.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/walker.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/__init__.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/gate.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/check +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/__init__.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/terminal.py +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/setup.cfg +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/SOURCES.txt +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/dependency_links.txt +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/entry_points.txt +0 -0
- {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vibe-code-checker
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: Production-grade, zero-dependency instant bug engine for any codebase.
|
|
5
5
|
Author-email: Krishiv Joshi <krishivjoshi219@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -25,10 +25,11 @@ Dynamic: license-file
|
|
|
25
25
|
|
|
26
26
|
<div align="center">
|
|
27
27
|
|
|
28
|
+
[](https://pypi.org/project/vibe-code-checker/)
|
|
28
29
|
[](https://www.python.org/)
|
|
29
30
|
[](https://opensource.org/licenses/MIT)
|
|
30
31
|
[-brightgreen.svg)]()
|
|
31
|
-
[-success.svg)]()
|
|
32
33
|
[]()
|
|
33
34
|
[]()
|
|
34
35
|
|
|
@@ -188,22 +189,36 @@ VCC categorizes all issues into three strict severity tiers:
|
|
|
188
189
|
|
|
189
190
|
## 💻 Installation & Quickstart
|
|
190
191
|
|
|
191
|
-
### Option 1:
|
|
192
|
+
### Option 1: Install from PyPI (Recommended)
|
|
193
|
+
Install globally or in your virtual environment:
|
|
194
|
+
```bash
|
|
195
|
+
pip install vibe-code-checker
|
|
196
|
+
```
|
|
197
|
+
Once installed, both `check` and `vcc` commands are immediately available in your terminal:
|
|
198
|
+
```bash
|
|
199
|
+
# Run with 'check'
|
|
200
|
+
check --target .
|
|
201
|
+
|
|
202
|
+
# Or run with 'vcc'
|
|
203
|
+
vcc --target .
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Option 2: Standalone CLI (Zero Install)
|
|
192
207
|
Clone and run immediately without installing anything:
|
|
193
208
|
```bash
|
|
194
209
|
git clone git@github.com:krishivjoshi219-collab/Vibe-Code-Checker.git
|
|
195
210
|
cd Vibe-Code-Checker
|
|
196
211
|
|
|
197
|
-
#
|
|
212
|
+
# Direct launcher script
|
|
213
|
+
./check --target /path/to/project
|
|
214
|
+
|
|
215
|
+
# Or python module
|
|
198
216
|
python check.py --target /path/to/project
|
|
199
217
|
```
|
|
200
218
|
|
|
201
|
-
### Option
|
|
219
|
+
### Option 3: Local Editable Install
|
|
202
220
|
```bash
|
|
203
221
|
pip install -e .
|
|
204
|
-
|
|
205
|
-
# Run globally from any directory
|
|
206
|
-
vcc --target .
|
|
207
222
|
```
|
|
208
223
|
|
|
209
224
|
---
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
+
[](https://pypi.org/project/vibe-code-checker/)
|
|
5
6
|
[](https://www.python.org/)
|
|
6
7
|
[](https://opensource.org/licenses/MIT)
|
|
7
8
|
[-brightgreen.svg)]()
|
|
8
|
-
[-success.svg)]()
|
|
9
10
|
[]()
|
|
10
11
|
[]()
|
|
11
12
|
|
|
@@ -165,22 +166,36 @@ VCC categorizes all issues into three strict severity tiers:
|
|
|
165
166
|
|
|
166
167
|
## 💻 Installation & Quickstart
|
|
167
168
|
|
|
168
|
-
### Option 1:
|
|
169
|
+
### Option 1: Install from PyPI (Recommended)
|
|
170
|
+
Install globally or in your virtual environment:
|
|
171
|
+
```bash
|
|
172
|
+
pip install vibe-code-checker
|
|
173
|
+
```
|
|
174
|
+
Once installed, both `check` and `vcc` commands are immediately available in your terminal:
|
|
175
|
+
```bash
|
|
176
|
+
# Run with 'check'
|
|
177
|
+
check --target .
|
|
178
|
+
|
|
179
|
+
# Or run with 'vcc'
|
|
180
|
+
vcc --target .
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Option 2: Standalone CLI (Zero Install)
|
|
169
184
|
Clone and run immediately without installing anything:
|
|
170
185
|
```bash
|
|
171
186
|
git clone git@github.com:krishivjoshi219-collab/Vibe-Code-Checker.git
|
|
172
187
|
cd Vibe-Code-Checker
|
|
173
188
|
|
|
174
|
-
#
|
|
189
|
+
# Direct launcher script
|
|
190
|
+
./check --target /path/to/project
|
|
191
|
+
|
|
192
|
+
# Or python module
|
|
175
193
|
python check.py --target /path/to/project
|
|
176
194
|
```
|
|
177
195
|
|
|
178
|
-
### Option
|
|
196
|
+
### Option 3: Local Editable Install
|
|
179
197
|
```bash
|
|
180
198
|
pip install -e .
|
|
181
|
-
|
|
182
|
-
# Run globally from any directory
|
|
183
|
-
vcc --target .
|
|
184
199
|
```
|
|
185
200
|
|
|
186
201
|
---
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""VCC Cosmetic Analyzer — Style, formatting, hygiene, typos, and complexity.
|
|
2
|
+
|
|
3
|
+
Catches cosmetic and maintainability bugs with zero false-positives.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import ast
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
COMMON_TYPOS = {
|
|
11
|
+
"recieve": "receive",
|
|
12
|
+
"recieved": "received",
|
|
13
|
+
"reciever": "receiver",
|
|
14
|
+
"seperate": "separate",
|
|
15
|
+
"seperated": "separated",
|
|
16
|
+
"occured": "occurred",
|
|
17
|
+
"occuring": "occurring",
|
|
18
|
+
"succesful": "successful",
|
|
19
|
+
"succesfully": "successfully",
|
|
20
|
+
"lenght": "length",
|
|
21
|
+
"unknwon": "unknown",
|
|
22
|
+
"defualt": "default",
|
|
23
|
+
"paramter": "parameter",
|
|
24
|
+
"paramters": "parameters",
|
|
25
|
+
"enviroment": "environment",
|
|
26
|
+
"reponse": "response",
|
|
27
|
+
"reponses": "responses",
|
|
28
|
+
"flase": "false",
|
|
29
|
+
"tru": "true",
|
|
30
|
+
"adn": "and",
|
|
31
|
+
"calback": "callback",
|
|
32
|
+
"calbacks": "callbacks",
|
|
33
|
+
"overide": "override",
|
|
34
|
+
"overriden": "overridden",
|
|
35
|
+
"refernce": "reference",
|
|
36
|
+
"refernces": "references",
|
|
37
|
+
"adress": "address",
|
|
38
|
+
"adresses": "addresses",
|
|
39
|
+
"maintainance": "maintenance",
|
|
40
|
+
"definately": "definitely",
|
|
41
|
+
"priviledge": "privilege",
|
|
42
|
+
"threshhold": "threshold",
|
|
43
|
+
"compatability": "compatibility",
|
|
44
|
+
"dependancy": "dependency",
|
|
45
|
+
"dependancies": "dependencies",
|
|
46
|
+
"untill": "until",
|
|
47
|
+
"writting": "writing",
|
|
48
|
+
"existance": "existence",
|
|
49
|
+
"guarentee": "guarantee",
|
|
50
|
+
"happend": "happened",
|
|
51
|
+
"noticable": "noticeable",
|
|
52
|
+
"persistance": "persistence",
|
|
53
|
+
"performence": "performance",
|
|
54
|
+
"possession": "possession",
|
|
55
|
+
"prefered": "preferred",
|
|
56
|
+
"publically": "publicly",
|
|
57
|
+
"refered": "referred",
|
|
58
|
+
"relavent": "relevant",
|
|
59
|
+
"resistence": "resistance",
|
|
60
|
+
"seperator": "separator",
|
|
61
|
+
"similiar": "similar",
|
|
62
|
+
"sucess": "success",
|
|
63
|
+
"transfered": "transferred",
|
|
64
|
+
"unforseen": "unforeseen",
|
|
65
|
+
"visiblity": "visibility",
|
|
66
|
+
"whish": "which",
|
|
67
|
+
"wierd": "weird",
|
|
68
|
+
"yeild": "yield",
|
|
69
|
+
"asnyc": "async",
|
|
70
|
+
"synch": "sync",
|
|
71
|
+
"canceld": "canceled",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
TODO_RE = re.compile(r"(?:#|//|/\*|<!--|\*)\s*(TODO|FIXME|XXX|HACK)\b", re.I)
|
|
75
|
+
URL_RE = re.compile(r"https?://\S+")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_snippet(lines: list[str], lineno: int, col_offset: int = 0, ctx: int = 2) -> str:
|
|
79
|
+
start = max(0, lineno - ctx - 1)
|
|
80
|
+
end = min(len(lines), lineno + ctx)
|
|
81
|
+
out = []
|
|
82
|
+
for i in range(start, end):
|
|
83
|
+
ln = i + 1
|
|
84
|
+
line_text = lines[i]
|
|
85
|
+
prefix = ">" if ln == lineno else " "
|
|
86
|
+
out.append(f"{prefix} {ln:4d} | {line_text}")
|
|
87
|
+
if ln == lineno and col_offset > 0:
|
|
88
|
+
indent = " " * (col_offset + 9)
|
|
89
|
+
out.append(f"{indent}^")
|
|
90
|
+
return "\n".join(out)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def split_identifier(ident: str) -> list[str]:
|
|
94
|
+
# Split snake_case and camelCase into individual words
|
|
95
|
+
words = re.sub(r"([A-Z][a-z]+)", r" \1", ident).split()
|
|
96
|
+
sub_words = []
|
|
97
|
+
for w in words:
|
|
98
|
+
sub_words.extend(w.split("_"))
|
|
99
|
+
return [w.lower() for w in sub_words if len(w) >= 3]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _check_comments(
|
|
103
|
+
idx: int,
|
|
104
|
+
line: str,
|
|
105
|
+
lines: list[str],
|
|
106
|
+
path: str,
|
|
107
|
+
sev: dict,
|
|
108
|
+
categories: dict,
|
|
109
|
+
) -> list[dict]:
|
|
110
|
+
findings: list[dict] = []
|
|
111
|
+
if "#" not in line:
|
|
112
|
+
return findings
|
|
113
|
+
|
|
114
|
+
comment_part = line.split("#", 1)[1]
|
|
115
|
+
words = re.findall(r"[A-Za-z]+", comment_part)
|
|
116
|
+
for w in words:
|
|
117
|
+
low = w.lower()
|
|
118
|
+
if low in COMMON_TYPOS:
|
|
119
|
+
rule = "comment-typo"
|
|
120
|
+
findings.append({
|
|
121
|
+
"rule": rule,
|
|
122
|
+
"severity": sev.get(rule, "P2"),
|
|
123
|
+
"category": categories.get(rule, "cosmetic"),
|
|
124
|
+
"file": path,
|
|
125
|
+
"line": idx,
|
|
126
|
+
"col": line.find(w),
|
|
127
|
+
"title": f"Typo `{w}` in comment (did you mean `{COMMON_TYPOS[low]}`?)",
|
|
128
|
+
"description": f"Misspelled word `{w}` found in comment.",
|
|
129
|
+
"evidence": get_snippet(lines, idx),
|
|
130
|
+
"confidence": "high",
|
|
131
|
+
"fix_suggestion": f"Replace `{w}` with `{COMMON_TYPOS[low]}`.",
|
|
132
|
+
})
|
|
133
|
+
return findings
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _check_whitespace_and_todo(line: str, idx: int, ctx: dict) -> list[dict]:
|
|
137
|
+
findings: list[dict] = []
|
|
138
|
+
sev = ctx["sev"]
|
|
139
|
+
categories = ctx["categories"]
|
|
140
|
+
path = ctx["path"]
|
|
141
|
+
lines = ctx["lines"]
|
|
142
|
+
max_line_len = ctx["max_line_len"]
|
|
143
|
+
|
|
144
|
+
if line.endswith((" ", "\t")):
|
|
145
|
+
rule = "trailing-whitespace"
|
|
146
|
+
findings.append({
|
|
147
|
+
"rule": rule,
|
|
148
|
+
"severity": sev.get(rule, "P2"),
|
|
149
|
+
"category": categories.get(rule, "cosmetic"),
|
|
150
|
+
"file": path,
|
|
151
|
+
"line": idx,
|
|
152
|
+
"col": len(line.rstrip(" \t")),
|
|
153
|
+
"title": "Trailing whitespace at end of line",
|
|
154
|
+
"description": "Trailing whitespace clutters git diffs and is against coding style standards.",
|
|
155
|
+
"evidence": get_snippet(lines, idx),
|
|
156
|
+
"confidence": "high",
|
|
157
|
+
"fix_suggestion": "Strip trailing whitespace.",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
if line.startswith(("\t ", " \t")):
|
|
161
|
+
rule = "mixed-indentation"
|
|
162
|
+
findings.append({
|
|
163
|
+
"rule": rule,
|
|
164
|
+
"severity": sev.get(rule, "P2"),
|
|
165
|
+
"category": categories.get(rule, "cosmetic"),
|
|
166
|
+
"file": path,
|
|
167
|
+
"line": idx,
|
|
168
|
+
"col": 0,
|
|
169
|
+
"title": "Mixed tabs and spaces in line indentation",
|
|
170
|
+
"description": "Mixing tabs and spaces causes indentation errors and layout inconsistencies.",
|
|
171
|
+
"evidence": get_snippet(lines, idx),
|
|
172
|
+
"confidence": "high",
|
|
173
|
+
"fix_suggestion": "Standardize on 4 spaces for indentation.",
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
if len(line) > max_line_len and not URL_RE.search(line):
|
|
177
|
+
rule = "line-too-long"
|
|
178
|
+
findings.append({
|
|
179
|
+
"rule": rule,
|
|
180
|
+
"severity": sev.get(rule, "P2"),
|
|
181
|
+
"category": categories.get(rule, "cosmetic"),
|
|
182
|
+
"file": path,
|
|
183
|
+
"line": idx,
|
|
184
|
+
"col": max_line_len,
|
|
185
|
+
"title": f"Line exceeds {max_line_len} characters ({len(line)} chars)",
|
|
186
|
+
"description": "Excessively long lines reduce readability on standard displays and code reviews.",
|
|
187
|
+
"evidence": get_snippet(lines, idx, max_line_len),
|
|
188
|
+
"confidence": "med",
|
|
189
|
+
"fix_suggestion": "Wrap line or break long statements into multiple lines.",
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
m_todo = TODO_RE.search(line)
|
|
193
|
+
if m_todo:
|
|
194
|
+
rule = "todo-marker"
|
|
195
|
+
findings.append({
|
|
196
|
+
"rule": rule,
|
|
197
|
+
"severity": sev.get(rule, "P2"),
|
|
198
|
+
"category": categories.get(rule, "cosmetic"),
|
|
199
|
+
"file": path,
|
|
200
|
+
"line": idx,
|
|
201
|
+
"col": m_todo.start(),
|
|
202
|
+
"title": f"Unfinished work marker `{m_todo.group(1)}` in code",
|
|
203
|
+
"description": f"Marker indicates pending implementation: {line.strip()[:80]}",
|
|
204
|
+
"evidence": get_snippet(lines, idx, m_todo.start()),
|
|
205
|
+
"confidence": "low",
|
|
206
|
+
"fix_suggestion": "Complete the task or track in issue tracker.",
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
return findings
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _analyze_lines(path: str, lines: list[str], sev: dict, categories: dict, max_line_len: int) -> list[dict]:
|
|
213
|
+
findings: list[dict] = []
|
|
214
|
+
consecutive_commented_code = 0
|
|
215
|
+
comment_block_start = 0
|
|
216
|
+
ctx = {"path": path, "lines": lines, "sev": sev, "categories": categories, "max_line_len": max_line_len}
|
|
217
|
+
|
|
218
|
+
for idx, line in enumerate(lines, 1):
|
|
219
|
+
if "# noqa" in line or "# vcc:ignore" in line:
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
findings.extend(_check_whitespace_and_todo(line, idx, ctx))
|
|
223
|
+
|
|
224
|
+
stripped = line.strip()
|
|
225
|
+
is_comment_code = stripped.startswith("#") and bool(
|
|
226
|
+
re.search(r"^#\s*(def |return |import |from |for |if |class |elif |else:)", stripped)
|
|
227
|
+
)
|
|
228
|
+
if is_comment_code:
|
|
229
|
+
if consecutive_commented_code == 0:
|
|
230
|
+
comment_block_start = idx
|
|
231
|
+
consecutive_commented_code += 1
|
|
232
|
+
else:
|
|
233
|
+
if consecutive_commented_code >= 3:
|
|
234
|
+
rule = "commented-out-code"
|
|
235
|
+
findings.append({
|
|
236
|
+
"rule": rule,
|
|
237
|
+
"severity": sev.get(rule, "P2"),
|
|
238
|
+
"category": categories.get(rule, "cosmetic"),
|
|
239
|
+
"file": path,
|
|
240
|
+
"line": comment_block_start,
|
|
241
|
+
"col": 0,
|
|
242
|
+
"title": f"Block of commented-out code ({consecutive_commented_code} lines)",
|
|
243
|
+
"description": "Commented-out code increases noise and rots over time. Use Git history instead.",
|
|
244
|
+
"evidence": get_snippet(lines, comment_block_start),
|
|
245
|
+
"confidence": "high",
|
|
246
|
+
"fix_suggestion": "Delete commented-out code.",
|
|
247
|
+
})
|
|
248
|
+
consecutive_commented_code = 0
|
|
249
|
+
|
|
250
|
+
findings.extend(_check_comments(idx, line, lines, path, sev, categories))
|
|
251
|
+
|
|
252
|
+
return findings
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _check_fn_node(node: ast.FunctionDef | ast.AsyncFunctionDef, ctx: dict) -> list[dict]:
|
|
256
|
+
findings: list[dict] = []
|
|
257
|
+
lines = ctx["lines"]
|
|
258
|
+
path = ctx["path"]
|
|
259
|
+
sev = ctx["sev"]
|
|
260
|
+
categories = ctx["categories"]
|
|
261
|
+
max_fn_len = ctx["max_fn_len"]
|
|
262
|
+
max_args = ctx["max_args"]
|
|
263
|
+
|
|
264
|
+
end = getattr(node, "end_lineno", node.lineno)
|
|
265
|
+
fn_len = end - node.lineno
|
|
266
|
+
line_text = lines[node.lineno - 1] if node.lineno <= len(lines) else ""
|
|
267
|
+
ignored = "# noqa" in line_text or "# vcc:ignore" in line_text
|
|
268
|
+
|
|
269
|
+
if fn_len > max_fn_len and not ignored:
|
|
270
|
+
rule = "long-function"
|
|
271
|
+
findings.append({
|
|
272
|
+
"rule": rule,
|
|
273
|
+
"severity": sev.get(rule, "P2"),
|
|
274
|
+
"category": categories.get(rule, "cosmetic"),
|
|
275
|
+
"file": path,
|
|
276
|
+
"line": node.lineno,
|
|
277
|
+
"col": node.col_offset,
|
|
278
|
+
"title": f"Function `{node.name}()` is {fn_len} lines long (exceeds {max_fn_len})",
|
|
279
|
+
"description": "Monolithic functions are harder to test, maintain, and reason about.",
|
|
280
|
+
"evidence": get_snippet(lines, node.lineno),
|
|
281
|
+
"confidence": "med",
|
|
282
|
+
"fix_suggestion": "Decompose function into smaller, single-purpose helper functions.",
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
n_args = len(node.args.posonlyargs + node.args.args + node.args.kwonlyargs)
|
|
286
|
+
if n_args > max_args and not ignored:
|
|
287
|
+
rule = "many-args"
|
|
288
|
+
findings.append({
|
|
289
|
+
"rule": rule,
|
|
290
|
+
"severity": sev.get(rule, "P2"),
|
|
291
|
+
"category": categories.get(rule, "cosmetic"),
|
|
292
|
+
"file": path,
|
|
293
|
+
"line": node.lineno,
|
|
294
|
+
"col": node.col_offset,
|
|
295
|
+
"title": f"Function `{node.name}()` has {n_args} arguments (exceeds {max_args})",
|
|
296
|
+
"description": "Functions with many parameters increase call-site error risk and tight coupling.",
|
|
297
|
+
"evidence": get_snippet(lines, node.lineno),
|
|
298
|
+
"confidence": "med",
|
|
299
|
+
"fix_suggestion": "Bundle arguments into a dataclass, pydantic model, or configuration object.",
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
for word in split_identifier(node.name):
|
|
303
|
+
if word in COMMON_TYPOS:
|
|
304
|
+
rule = "identifier-typo"
|
|
305
|
+
findings.append({
|
|
306
|
+
"rule": rule,
|
|
307
|
+
"severity": sev.get(rule, "P2"),
|
|
308
|
+
"category": categories.get(rule, "cosmetic"),
|
|
309
|
+
"file": path,
|
|
310
|
+
"line": node.lineno,
|
|
311
|
+
"col": node.col_offset,
|
|
312
|
+
"title": f"Typo in function name `{node.name}`: `{word}` -> `{COMMON_TYPOS[word]}`",
|
|
313
|
+
"description": f"Identifier contains misspelled word `{word}`.",
|
|
314
|
+
"evidence": get_snippet(lines, node.lineno),
|
|
315
|
+
"confidence": "high",
|
|
316
|
+
"fix_suggestion": f"Rename to include `{COMMON_TYPOS[word]}`.",
|
|
317
|
+
})
|
|
318
|
+
return findings
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _check_nesting(
|
|
322
|
+
tree: ast.AST,
|
|
323
|
+
lines: list[str],
|
|
324
|
+
path: str,
|
|
325
|
+
sev: dict,
|
|
326
|
+
categories: dict,
|
|
327
|
+
max_nesting: int,
|
|
328
|
+
) -> list[dict]:
|
|
329
|
+
findings: list[dict] = []
|
|
330
|
+
block_types = (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.With, ast.AsyncWith)
|
|
331
|
+
|
|
332
|
+
def walk(curr: ast.AST, depth: int = 0) -> bool:
|
|
333
|
+
is_block = isinstance(curr, block_types)
|
|
334
|
+
new_depth = depth + 1 if is_block else depth
|
|
335
|
+
if is_block and new_depth > max_nesting:
|
|
336
|
+
lineno = getattr(curr, "lineno", 1)
|
|
337
|
+
rule = "deep-nesting"
|
|
338
|
+
findings.append({
|
|
339
|
+
"rule": rule,
|
|
340
|
+
"severity": sev.get(rule, "P2"),
|
|
341
|
+
"category": categories.get(rule, "cosmetic"),
|
|
342
|
+
"file": path,
|
|
343
|
+
"line": lineno,
|
|
344
|
+
"col": getattr(curr, "col_offset", 0),
|
|
345
|
+
"title": f"Deeply nested control flow ({new_depth} levels deep, exceeds {max_nesting})",
|
|
346
|
+
"description": "Deeply nested control flow increases cognitive load and branch complexity.",
|
|
347
|
+
"evidence": get_snippet(lines, lineno),
|
|
348
|
+
"confidence": "high",
|
|
349
|
+
"fix_suggestion": "Invert conditions to return early (guard clauses) or extract helper functions.",
|
|
350
|
+
})
|
|
351
|
+
return True
|
|
352
|
+
for child in ast.iter_child_nodes(curr):
|
|
353
|
+
if walk(child, new_depth):
|
|
354
|
+
return True
|
|
355
|
+
return False
|
|
356
|
+
|
|
357
|
+
walk(tree)
|
|
358
|
+
return findings
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _analyze_python_ast(
|
|
362
|
+
path: str,
|
|
363
|
+
text: str,
|
|
364
|
+
lines: list[str],
|
|
365
|
+
sev: dict,
|
|
366
|
+
categories: dict,
|
|
367
|
+
thresholds: dict,
|
|
368
|
+
) -> list[dict]:
|
|
369
|
+
findings: list[dict] = []
|
|
370
|
+
max_fn_len = int(thresholds.get("long_function_lines", 80))
|
|
371
|
+
max_args = int(thresholds.get("many_args_count", 6))
|
|
372
|
+
max_nesting = int(thresholds.get("deep_nesting_levels", 4))
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
tree = ast.parse(text, filename=path)
|
|
376
|
+
except (SyntaxError, ValueError):
|
|
377
|
+
return []
|
|
378
|
+
|
|
379
|
+
fn_ctx = {
|
|
380
|
+
"lines": lines,
|
|
381
|
+
"path": path,
|
|
382
|
+
"sev": sev,
|
|
383
|
+
"categories": categories,
|
|
384
|
+
"max_fn_len": max_fn_len,
|
|
385
|
+
"max_args": max_args,
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
for node in ast.walk(tree):
|
|
389
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
390
|
+
findings.extend(_check_fn_node(node, fn_ctx))
|
|
391
|
+
|
|
392
|
+
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
393
|
+
for word in split_identifier(node.id):
|
|
394
|
+
if word in COMMON_TYPOS:
|
|
395
|
+
rule = "identifier-typo"
|
|
396
|
+
findings.append({
|
|
397
|
+
"rule": rule,
|
|
398
|
+
"severity": sev.get(rule, "P2"),
|
|
399
|
+
"category": categories.get(rule, "cosmetic"),
|
|
400
|
+
"file": path,
|
|
401
|
+
"line": node.lineno,
|
|
402
|
+
"col": node.col_offset,
|
|
403
|
+
"title": f"Typo in variable `{node.id}`: `{word}` -> `{COMMON_TYPOS[word]}`",
|
|
404
|
+
"description": f"Variable identifier contains misspelled word `{word}`.",
|
|
405
|
+
"evidence": get_snippet(lines, node.lineno),
|
|
406
|
+
"confidence": "high",
|
|
407
|
+
"fix_suggestion": f"Rename to `{COMMON_TYPOS[word]}`.",
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
findings.extend(_check_nesting(tree, lines, path, sev, categories, max_nesting))
|
|
411
|
+
return findings
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def analyze_cosmetic(path: str, rules: dict) -> list[dict]:
|
|
415
|
+
findings: list[dict] = []
|
|
416
|
+
sev = rules.get("severity", {})
|
|
417
|
+
categories = rules.get("categories", {})
|
|
418
|
+
thresholds = rules.get("thresholds", {})
|
|
419
|
+
max_line_len = int(thresholds.get("line_length_limit", 120))
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
with open(path, "rb") as f:
|
|
423
|
+
raw_bytes = f.read()
|
|
424
|
+
except OSError:
|
|
425
|
+
return []
|
|
426
|
+
|
|
427
|
+
# Missing final newline check
|
|
428
|
+
if raw_bytes and not raw_bytes.endswith(b"\n"):
|
|
429
|
+
rule = "missing-final-newline"
|
|
430
|
+
findings.append({
|
|
431
|
+
"rule": rule,
|
|
432
|
+
"severity": sev.get(rule, "P2"),
|
|
433
|
+
"category": categories.get(rule, "cosmetic"),
|
|
434
|
+
"file": path,
|
|
435
|
+
"line": len(raw_bytes.splitlines()) or 1,
|
|
436
|
+
"col": 0,
|
|
437
|
+
"title": "Missing final newline at end of file",
|
|
438
|
+
"description": "POSIX standards specify that text files should end with a newline (\\n) character.",
|
|
439
|
+
"evidence": "[End of File without newline]",
|
|
440
|
+
"confidence": "high",
|
|
441
|
+
"fix_suggestion": "Add a newline at the end of the file.",
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
text = raw_bytes.decode("utf-8", errors="replace")
|
|
445
|
+
lines = text.splitlines()
|
|
446
|
+
|
|
447
|
+
findings.extend(_analyze_lines(path, lines, sev, categories, max_line_len))
|
|
448
|
+
|
|
449
|
+
if path.endswith((".py", ".pyi")):
|
|
450
|
+
findings.extend(_analyze_python_ast(path, text, lines, sev, categories, thresholds))
|
|
451
|
+
|
|
452
|
+
return findings
|