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 ADDED
@@ -0,0 +1,39 @@
1
+ """VCC Analyzers package — Unified entry point for all analyzers."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+ from . import cosmetic, multilang, native_fast, python_ast, walker
7
+
8
+
9
+ def scan_file(path: str, rules: dict) -> list[dict]:
10
+ """Scans a single file with relevant analyzers based on file extension."""
11
+ findings: list[dict] = []
12
+ ext = os.path.splitext(path)[1].lower()
13
+ fname = os.path.basename(path).lower()
14
+
15
+ # 1. Python AST Analyzer
16
+ if ext in (".py", ".pyi"):
17
+ findings.extend(python_ast.analyze_python_ast(path, rules))
18
+
19
+ # 2. Multi-Language / Config Analyzer
20
+ if ext in (
21
+ ".json", ".yaml", ".yml", ".toml", ".js", ".jsx", ".ts", ".tsx",
22
+ ".mjs", ".cjs", ".html", ".htm", ".css", ".scss", ".sh", ".bash"
23
+ ) or fname in ("dockerfile", "dockerfile.dev", "dockerfile.prod") or ext == ".dockerfile":
24
+ findings.extend(multilang.analyze_multilang(path, rules))
25
+
26
+ # 3. Cosmetic / Hygiene Analyzer (runs across all text files)
27
+ findings.extend(cosmetic.analyze_cosmetic(path, rules))
28
+
29
+ return findings
30
+
31
+
32
+ __all__ = [
33
+ "scan_file",
34
+ "cosmetic",
35
+ "multilang",
36
+ "native_fast",
37
+ "python_ast",
38
+ "walker",
39
+ ]
analyzers/cosmetic.py ADDED
@@ -0,0 +1,373 @@
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 analyze_cosmetic(path: str, rules: dict) -> list[dict]:
103
+ findings: list[dict] = []
104
+ sev = rules.get("severity", {})
105
+ categories = rules.get("categories", {})
106
+ thresholds = rules.get("thresholds", {})
107
+ max_line_len = int(thresholds.get("line_length_limit", 120))
108
+ max_fn_len = int(thresholds.get("long_function_lines", 80))
109
+ max_args = int(thresholds.get("many_args_count", 6))
110
+ max_nesting = int(thresholds.get("deep_nesting_levels", 4))
111
+
112
+ try:
113
+ with open(path, "rb") as f:
114
+ raw_bytes = f.read()
115
+ except Exception:
116
+ return []
117
+
118
+ # Missing final newline check
119
+ if raw_bytes and not raw_bytes.endswith(b"\n"):
120
+ rule = "missing-final-newline"
121
+ findings.append({
122
+ "rule": rule,
123
+ "severity": sev.get(rule, "P2"),
124
+ "category": categories.get(rule, "cosmetic"),
125
+ "file": path,
126
+ "line": len(raw_bytes.splitlines()) or 1,
127
+ "col": 0,
128
+ "title": "Missing final newline at end of file",
129
+ "description": "POSIX standards specify that text files should end with a newline (\\n) character.",
130
+ "evidence": "[End of File without newline]",
131
+ "confidence": "high",
132
+ "fix_suggestion": "Add a newline at the end of the file.",
133
+ })
134
+
135
+ text = raw_bytes.decode("utf-8", errors="replace")
136
+ lines = text.splitlines()
137
+
138
+ # Line-by-line checks
139
+ consecutive_commented_code = 0
140
+ comment_block_start = 0
141
+
142
+ for idx, line in enumerate(lines, 1):
143
+ if "# noqa" in line or "# vcc:ignore" in line:
144
+ continue
145
+
146
+ # Trailing whitespace
147
+ if line.endswith(" ") or line.endswith("\t"):
148
+ rule = "trailing-whitespace"
149
+ findings.append({
150
+ "rule": rule,
151
+ "severity": sev.get(rule, "P2"),
152
+ "category": categories.get(rule, "cosmetic"),
153
+ "file": path,
154
+ "line": idx,
155
+ "col": len(line.rstrip(" \t")),
156
+ "title": "Trailing whitespace at end of line",
157
+ "description": "Trailing whitespace clutters git diffs and is against coding style standards.",
158
+ "evidence": get_snippet(lines, idx),
159
+ "confidence": "high",
160
+ "fix_suggestion": "Strip trailing whitespace.",
161
+ })
162
+
163
+ # Mixed indentation
164
+ if line.startswith("\t ") or line.startswith(" \t"):
165
+ rule = "mixed-indentation"
166
+ findings.append({
167
+ "rule": rule,
168
+ "severity": sev.get(rule, "P2"),
169
+ "category": categories.get(rule, "cosmetic"),
170
+ "file": path,
171
+ "line": idx,
172
+ "col": 0,
173
+ "title": "Mixed tabs and spaces in line indentation",
174
+ "description": "Mixing tabs and spaces causes indentation errors and layout inconsistencies.",
175
+ "evidence": get_snippet(lines, idx),
176
+ "confidence": "high",
177
+ "fix_suggestion": "Standardize on 4 spaces for indentation.",
178
+ })
179
+
180
+ # Line too long
181
+ if len(line) > max_line_len and not URL_RE.search(line):
182
+ rule = "line-too-long"
183
+ findings.append({
184
+ "rule": rule,
185
+ "severity": sev.get(rule, "P2"),
186
+ "category": categories.get(rule, "cosmetic"),
187
+ "file": path,
188
+ "line": idx,
189
+ "col": max_line_len,
190
+ "title": f"Line exceeds {max_line_len} characters ({len(line)} chars)",
191
+ "description": "Excessively long lines reduce readability on standard displays and code reviews.",
192
+ "evidence": get_snippet(lines, idx, max_line_len),
193
+ "confidence": "med",
194
+ "fix_suggestion": "Wrap line or break long statements into multiple lines.",
195
+ })
196
+
197
+ # TODO marker
198
+ m_todo = TODO_RE.search(line)
199
+ if m_todo:
200
+ rule = "todo-marker"
201
+ findings.append({
202
+ "rule": rule,
203
+ "severity": sev.get(rule, "P2"),
204
+ "category": categories.get(rule, "cosmetic"),
205
+ "file": path,
206
+ "line": idx,
207
+ "col": m_todo.start(),
208
+ "title": f"Unfinished work marker `{m_todo.group(1)}` in code",
209
+ "description": f"Marker indicates pending implementation: {line.strip()[:80]}",
210
+ "evidence": get_snippet(lines, idx, m_todo.start()),
211
+ "confidence": "low",
212
+ "fix_suggestion": "Complete the task or track in issue tracker.",
213
+ })
214
+
215
+ # Commented-out code blocks
216
+ stripped = line.strip()
217
+ if stripped.startswith("#") and re.search(r"^#\s*(def |return |import |from |for |if |class |elif |else:)", stripped):
218
+ if consecutive_commented_code == 0:
219
+ comment_block_start = idx
220
+ consecutive_commented_code += 1
221
+ else:
222
+ if consecutive_commented_code >= 3:
223
+ rule = "commented-out-code"
224
+ findings.append({
225
+ "rule": rule,
226
+ "severity": sev.get(rule, "P2"),
227
+ "category": categories.get(rule, "cosmetic"),
228
+ "file": path,
229
+ "line": comment_block_start,
230
+ "col": 0,
231
+ "title": f"Block of commented-out code ({consecutive_commented_code} lines)",
232
+ "description": "Commented-out code increases noise and rots over time. Rely on Git version history instead.",
233
+ "evidence": get_snippet(lines, comment_block_start),
234
+ "confidence": "high",
235
+ "fix_suggestion": "Delete commented-out code.",
236
+ })
237
+ consecutive_commented_code = 0
238
+
239
+ # Typos in comments
240
+ if "#" in line:
241
+ comment_part = line.split("#", 1)[1]
242
+ words = re.findall(r"[A-Za-z]+", comment_part)
243
+ for w in words:
244
+ low = w.lower()
245
+ if low in COMMON_TYPOS:
246
+ rule = "comment-typo"
247
+ findings.append({
248
+ "rule": rule,
249
+ "severity": sev.get(rule, "P2"),
250
+ "category": categories.get(rule, "cosmetic"),
251
+ "file": path,
252
+ "line": idx,
253
+ "col": line.find(w),
254
+ "title": f"Typo `{w}` in comment (did you mean `{COMMON_TYPOS[low]}`?)",
255
+ "description": f"Misspelled word `{w}` found in comment.",
256
+ "evidence": get_snippet(lines, idx),
257
+ "confidence": "high",
258
+ "fix_suggestion": f"Replace `{w}` with `{COMMON_TYPOS[low]}`.",
259
+ })
260
+
261
+ # AST-based complexity and naming checks for Python
262
+ if path.endswith((".py", ".pyi")):
263
+ try:
264
+ tree = ast.parse(text, filename=path)
265
+ for node in ast.walk(tree):
266
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
267
+ # Long function
268
+ end = getattr(node, "end_lineno", node.lineno)
269
+ fn_len = end - node.lineno
270
+ line_text = lines[node.lineno - 1] if node.lineno <= len(lines) else ""
271
+ if fn_len > max_fn_len and "# noqa" not in line_text and "# vcc:ignore" not in line_text:
272
+ rule = "long-function"
273
+ findings.append({
274
+ "rule": rule,
275
+ "severity": sev.get(rule, "P2"),
276
+ "category": categories.get(rule, "cosmetic"),
277
+ "file": path,
278
+ "line": node.lineno,
279
+ "col": node.col_offset,
280
+ "title": f"Function `{node.name}()` is {fn_len} lines long (exceeds {max_fn_len})",
281
+ "description": "Monolithic functions are harder to test, maintain, and reason about.",
282
+ "evidence": get_snippet(lines, node.lineno),
283
+ "confidence": "med",
284
+ "fix_suggestion": "Decompose function into smaller, single-purpose helper functions.",
285
+ })
286
+
287
+ # Many arguments
288
+ n_args = len(node.args.posonlyargs + node.args.args + node.args.kwonlyargs)
289
+ if n_args > max_args and "# noqa" not in line_text and "# vcc:ignore" not in line_text:
290
+ rule = "many-args"
291
+ findings.append({
292
+ "rule": rule,
293
+ "severity": sev.get(rule, "P2"),
294
+ "category": categories.get(rule, "cosmetic"),
295
+ "file": path,
296
+ "line": node.lineno,
297
+ "col": node.col_offset,
298
+ "title": f"Function `{node.name}()` has {n_args} arguments (exceeds {max_args})",
299
+ "description": "Functions with many parameters increase call-site error risk and tight coupling.",
300
+ "evidence": get_snippet(lines, node.lineno),
301
+ "confidence": "med",
302
+ "fix_suggestion": "Bundle arguments into a dataclass, pydantic model, or configuration object.",
303
+ })
304
+
305
+ # Typo in function name
306
+ for word in split_identifier(node.name):
307
+ if word in COMMON_TYPOS:
308
+ rule = "identifier-typo"
309
+ findings.append({
310
+ "rule": rule,
311
+ "severity": sev.get(rule, "P2"),
312
+ "category": categories.get(rule, "cosmetic"),
313
+ "file": path,
314
+ "line": node.lineno,
315
+ "col": node.col_offset,
316
+ "title": f"Typo in function name `{node.name}`: `{word}` -> `{COMMON_TYPOS[word]}`",
317
+ "description": f"Identifier contains misspelled word `{word}`.",
318
+ "evidence": get_snippet(lines, node.lineno),
319
+ "confidence": "high",
320
+ "fix_suggestion": f"Rename to include `{COMMON_TYPOS[word]}`.",
321
+ })
322
+
323
+ # Check variable name typos
324
+ if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
325
+ for word in split_identifier(node.id):
326
+ if word in COMMON_TYPOS:
327
+ rule = "identifier-typo"
328
+ findings.append({
329
+ "rule": rule,
330
+ "severity": sev.get(rule, "P2"),
331
+ "category": categories.get(rule, "cosmetic"),
332
+ "file": path,
333
+ "line": node.lineno,
334
+ "col": node.col_offset,
335
+ "title": f"Typo in variable `{node.id}`: `{word}` -> `{COMMON_TYPOS[word]}`",
336
+ "description": f"Variable identifier contains misspelled word `{word}`.",
337
+ "evidence": get_snippet(lines, node.lineno),
338
+ "confidence": "high",
339
+ "fix_suggestion": f"Rename to `{COMMON_TYPOS[word]}`.",
340
+ })
341
+
342
+ # Check deep control-flow nesting via AST
343
+ def check_nesting(node: ast.AST, depth: int = 0) -> bool:
344
+ is_block = isinstance(node, (ast.If, ast.For, ast.AsyncFor, ast.While, ast.Try, ast.With, ast.AsyncWith))
345
+ new_depth = depth + 1 if is_block else depth
346
+ if is_block and new_depth > max_nesting:
347
+ lineno = getattr(node, "lineno", 1)
348
+ rule = "deep-nesting"
349
+ findings.append({
350
+ "rule": rule,
351
+ "severity": sev.get(rule, "P2"),
352
+ "category": categories.get(rule, "cosmetic"),
353
+ "file": path,
354
+ "line": lineno,
355
+ "col": getattr(node, "col_offset", 0),
356
+ "title": f"Deeply nested control flow ({new_depth} levels deep, exceeds {max_nesting})",
357
+ "description": "Deeply nested control flow significantly increases cognitive load and branch complexity.",
358
+ "evidence": get_snippet(lines, lineno),
359
+ "confidence": "high",
360
+ "fix_suggestion": "Invert conditions to return early (guard clauses) or extract helper functions.",
361
+ })
362
+ return True
363
+ for child in ast.iter_child_nodes(node):
364
+ if check_nesting(child, new_depth):
365
+ return True
366
+ return False
367
+
368
+ check_nesting(tree)
369
+
370
+ except Exception:
371
+ pass
372
+
373
+ return findings