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/__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
|