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.
Files changed (31) hide show
  1. {vibe_code_checker-2.0.0/vibe_code_checker.egg-info → vibe_code_checker-2.0.1}/PKG-INFO +23 -8
  2. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/README.md +22 -7
  3. vibe_code_checker-2.0.1/analyzers/cosmetic.py +452 -0
  4. vibe_code_checker-2.0.1/analyzers/multilang.py +422 -0
  5. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/native_fast.py +8 -5
  6. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/python_ast.py +283 -162
  7. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/rules.json +1 -1
  8. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/engine.py +2 -3
  9. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/zen_client.py +21 -15
  10. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/check.py +96 -61
  11. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/pyproject.toml +1 -1
  12. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/llm_report.py +81 -53
  13. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/rules.json +1 -1
  14. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/tests/test_vcc.py +3 -4
  15. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1/vibe_code_checker.egg-info}/PKG-INFO +23 -8
  16. vibe_code_checker-2.0.0/analyzers/cosmetic.py +0 -373
  17. vibe_code_checker-2.0.0/analyzers/multilang.py +0 -373
  18. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/LICENSE +0 -0
  19. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/MANIFEST.in +0 -0
  20. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/__init__.py +0 -0
  21. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/analyzers/walker.py +0 -0
  22. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/__init__.py +0 -0
  23. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/autofix/gate.py +0 -0
  24. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/check +0 -0
  25. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/__init__.py +0 -0
  26. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/reporters/terminal.py +0 -0
  27. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/setup.cfg +0 -0
  28. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/SOURCES.txt +0 -0
  29. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/dependency_links.txt +0 -0
  30. {vibe_code_checker-2.0.0 → vibe_code_checker-2.0.1}/vibe_code_checker.egg-info/entry_points.txt +0 -0
  31. {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.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
+ [![PyPI](https://img.shields.io/pypi/v/vibe-code-checker.svg)](https://pypi.org/project/vibe-code-checker/)
28
29
  [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
29
30
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
31
  [![Dependencies](https://img.shields.io/badge/dependencies-zero%20(stdlib%20only)-brightgreen.svg)]()
31
- [![Tests](https://img.shields.io/badge/tests-20%20passed%20(10ms)-success.svg)]()
32
+ [![Tests](https://img.shields.io/badge/tests-21%20passed%20(10ms)-success.svg)]()
32
33
  [![CI](https://img.shields.io/badge/CI-GitHub%20Actions-blueviolet.svg)]()
33
34
  [![Code Quality](https://img.shields.io/badge/quality-production--grade-orange.svg)]()
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: Standalone CLI (Recommended)
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
- # Scan any target project
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 2: Editable Install (`vcc` Command)
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
+ [![PyPI](https://img.shields.io/pypi/v/vibe-code-checker.svg)](https://pypi.org/project/vibe-code-checker/)
5
6
  [![Python](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue.svg)](https://www.python.org/)
6
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
8
  [![Dependencies](https://img.shields.io/badge/dependencies-zero%20(stdlib%20only)-brightgreen.svg)]()
8
- [![Tests](https://img.shields.io/badge/tests-20%20passed%20(10ms)-success.svg)]()
9
+ [![Tests](https://img.shields.io/badge/tests-21%20passed%20(10ms)-success.svg)]()
9
10
  [![CI](https://img.shields.io/badge/CI-GitHub%20Actions-blueviolet.svg)]()
10
11
  [![Code Quality](https://img.shields.io/badge/quality-production--grade-orange.svg)]()
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: Standalone CLI (Recommended)
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
- # Scan any target project
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 2: Editable Install (`vcc` Command)
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