code-oracle 0.1.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.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,494 @@
1
+ """
2
+ PERF003: Resource Leak / Unclosed Descriptors Rule.
3
+ Detects unclosed file/socket/db descriptors without scoped context manager:
4
+ missing `with` in Python, missing `defer resp.Body.Close()` in Go,
5
+ unhandled stream/fd in TS/JS, Box::leak/mem::forget in Rust.
6
+ """
7
+
8
+ import re
9
+ from typing import List, Optional
10
+ from tree_sitter import Node
11
+
12
+ from code_oracle.perf_lint.models import PerfDiagnostic, PerfRule, Severity
13
+
14
+
15
+ class UnclosedResourceRule:
16
+ """Evaluates PERF003: Resource Leak / Unclosed Descriptors."""
17
+
18
+ RULE_ID = PerfRule.PERF003.value
19
+
20
+ @staticmethod
21
+ def is_open_call(callee_text: str, language: str) -> bool:
22
+ """Check if callee is a resource open call requiring scoped cleanup."""
23
+ clean = callee_text.strip()
24
+ if "(" in clean:
25
+ return False
26
+ clean_lower = clean.lower()
27
+
28
+ if language == "python":
29
+ if clean in ("open", "socket.socket", "socket.create_connection", "os.open"):
30
+ return True
31
+ if clean.endswith((".open",)):
32
+ return True
33
+ if "." in clean and clean.endswith(".connect"):
34
+ receiver = clean.rsplit(".", 1)[0].lower()
35
+ if any(
36
+ db in receiver
37
+ for db in (
38
+ "sqlite",
39
+ "psycopg",
40
+ "mysql",
41
+ "asyncpg",
42
+ "db",
43
+ "database",
44
+ "postgres",
45
+ "engine",
46
+ "sql",
47
+ )
48
+ ) or receiver in ("conn", "connection"):
49
+ return True
50
+ return False
51
+
52
+ elif language in ("typescript", "javascript"):
53
+ return clean.startswith((
54
+ "fs.open",
55
+ "fs.createReadStream",
56
+ "fs.createWriteStream",
57
+ "net.connect",
58
+ "net.createConnection",
59
+ "tls.connect",
60
+ ))
61
+
62
+ elif language == "go":
63
+ return clean.startswith((
64
+ "os.Open",
65
+ "os.OpenFile",
66
+ "os.Create",
67
+ "net.Dial",
68
+ "net.DialTimeout",
69
+ "net.Listen",
70
+ "sql.Open",
71
+ "http.Get",
72
+ "http.Post",
73
+ "http.Head",
74
+ ))
75
+
76
+ elif language == "rust":
77
+ return clean in (
78
+ "Box::leak",
79
+ "std::boxed::Box::leak",
80
+ "Box::into_raw",
81
+ "std::boxed::Box::into_raw",
82
+ "CString::into_raw",
83
+ "std::ffi::CString::into_raw",
84
+ "std::mem::forget",
85
+ "mem::forget",
86
+ "std::mem::ManuallyDrop::new",
87
+ "ManuallyDrop::new",
88
+ )
89
+
90
+ return False
91
+
92
+ @staticmethod
93
+ def _extract_assigned_var(call_node: Node, source_bytes: bytes, language: str) -> Optional[str]:
94
+ """Extract variable identifier to which the resource is directly assigned."""
95
+ unwrapped = call_node
96
+ while unwrapped.parent and unwrapped.parent.type == "parenthesized_expression":
97
+ unwrapped = unwrapped.parent
98
+
99
+ p = unwrapped.parent
100
+ if p is None:
101
+ return None
102
+
103
+ if language in ("typescript", "javascript"):
104
+ if p.type == "variable_declarator":
105
+ val = p.child_by_field_name("value")
106
+ if val == unwrapped:
107
+ name_node = p.child_by_field_name("name")
108
+ if name_node and name_node.type == "identifier":
109
+ return source_bytes[name_node.start_byte : name_node.end_byte].decode("utf-8", errors="ignore").strip()
110
+ elif p.type == "assignment_expression":
111
+ right = p.child_by_field_name("right")
112
+ if right == unwrapped:
113
+ left = p.child_by_field_name("left")
114
+ if left and left.type == "identifier":
115
+ return source_bytes[left.start_byte : left.end_byte].decode("utf-8", errors="ignore").strip()
116
+
117
+ elif language == "go":
118
+ stmt = p if p.type in ("short_var_declaration", "assignment_statement", "var_spec") else p.parent
119
+ if stmt and stmt.type in ("short_var_declaration", "assignment_statement", "var_spec"):
120
+ right = stmt.child_by_field_name("right")
121
+ if right is None:
122
+ for ch in stmt.children:
123
+ if ch.type == "expression_list" and ch != stmt.child_by_field_name("left"):
124
+ right = ch
125
+ break
126
+ is_in_right = (right == unwrapped) or (right and unwrapped in right.named_children)
127
+ if is_in_right:
128
+ left = stmt.child_by_field_name("left")
129
+ if left is None:
130
+ for ch in stmt.children:
131
+ if ch.type == "expression_list":
132
+ left = ch
133
+ break
134
+ if left:
135
+ if right and len(right.named_children) == len(left.named_children) and unwrapped in right.named_children:
136
+ idx = right.named_children.index(unwrapped)
137
+ if idx < len(left.named_children) and left.named_children[idx].type == "identifier":
138
+ return source_bytes[left.named_children[idx].start_byte : left.named_children[idx].end_byte].decode("utf-8", errors="ignore").strip()
139
+ for ch in left.children:
140
+ if ch.type == "identifier":
141
+ return source_bytes[ch.start_byte : ch.end_byte].decode("utf-8", errors="ignore").strip()
142
+
143
+ elif language == "python":
144
+ if p.type == "assignment":
145
+ right = p.child_by_field_name("right")
146
+ if right == unwrapped:
147
+ left = p.child_by_field_name("left")
148
+ if left and left.type == "identifier":
149
+ return source_bytes[left.start_byte : left.end_byte].decode("utf-8", errors="ignore").strip()
150
+ elif p.type in ("expression_list", "tuple"):
151
+ assign = p.parent
152
+ if assign and assign.type == "assignment" and assign.child_by_field_name("right") == p:
153
+ named_rhs = p.named_children
154
+ if unwrapped in named_rhs:
155
+ idx = named_rhs.index(unwrapped)
156
+ left = assign.child_by_field_name("left")
157
+ if left and left.type in ("pattern_list", "tuple_pattern"):
158
+ named_lhs = left.named_children
159
+ if idx < len(named_lhs) and named_lhs[idx].type == "identifier":
160
+ return source_bytes[named_lhs[idx].start_byte : named_lhs[idx].end_byte].decode("utf-8", errors="ignore").strip()
161
+
162
+ return None
163
+
164
+ @classmethod
165
+ def _is_returned(cls, call_node: Node, source_bytes: bytes, language: str) -> bool:
166
+ """Check if resource or its assigned variable is returned (ownership transferred)."""
167
+ # 1. Direct return: call_node is inside a return statement
168
+ curr = call_node.parent
169
+ while curr is not None:
170
+ if curr.type in (
171
+ "function_definition",
172
+ "function_declaration",
173
+ "method_definition",
174
+ "method_declaration",
175
+ "func_literal",
176
+ "arrow_function",
177
+ "function_expression",
178
+ ):
179
+ break
180
+ if curr.type == "return_statement":
181
+ # Ensure call_node is not simply calling a method (e.g. open().read())
182
+ # or accessing a property (e.g. open().name)
183
+ p = call_node.parent
184
+ while p and p.type == "parenthesized_expression":
185
+ p = p.parent
186
+ if p and p.type in ("attribute", "member_expression", "selector_expression"):
187
+ if language in ("typescript", "javascript"):
188
+ prop = p.child_by_field_name("property")
189
+ if prop and source_bytes[prop.start_byte : prop.end_byte].strip() == b"pipe":
190
+ return True
191
+ return False
192
+ return True
193
+ curr = curr.parent
194
+
195
+ # 2. Variable return: resource assigned to var and var is returned
196
+ enclosing_func = None
197
+ curr = call_node.parent
198
+ while curr is not None:
199
+ if curr.type in (
200
+ "function_definition",
201
+ "function_declaration",
202
+ "method_definition",
203
+ "method_declaration",
204
+ "func_literal",
205
+ "arrow_function",
206
+ "function_expression",
207
+ ):
208
+ enclosing_func = curr
209
+ break
210
+ curr = curr.parent
211
+
212
+ if enclosing_func is None:
213
+ return False
214
+
215
+ var_name = cls._extract_assigned_var(call_node, source_bytes, language)
216
+ if not var_name:
217
+ return False
218
+
219
+ def is_var_returned_in_node(ret_node: Node) -> bool:
220
+ def check_node(n: Node) -> bool:
221
+ if n.type in ("identifier", "shorthand_property_identifier"):
222
+ ident = source_bytes[n.start_byte : n.end_byte].decode("utf-8", errors="ignore").strip()
223
+ if ident == var_name:
224
+ p = n.parent
225
+ while p and p.type == "parenthesized_expression":
226
+ p = p.parent
227
+ if p and p.type in ("attribute", "member_expression", "selector_expression"):
228
+ if language in ("typescript", "javascript"):
229
+ prop = p.child_by_field_name("property")
230
+ if prop and source_bytes[prop.start_byte : prop.end_byte].strip() == b"pipe":
231
+ return True
232
+ return False
233
+ return True
234
+ for ch in n.children:
235
+ if check_node(ch):
236
+ return True
237
+ return False
238
+
239
+ return check_node(ret_node)
240
+
241
+ def search_returns(node: Node) -> bool:
242
+ if node.type == "return_statement":
243
+ if is_var_returned_in_node(node):
244
+ return True
245
+ for ch in node.children:
246
+ if ch.type in (
247
+ "function_definition",
248
+ "function_declaration",
249
+ "method_definition",
250
+ "method_declaration",
251
+ "func_literal",
252
+ "arrow_function",
253
+ "function_expression",
254
+ ):
255
+ continue
256
+ if search_returns(ch):
257
+ return True
258
+ return False
259
+
260
+ return search_returns(enclosing_func)
261
+
262
+ @classmethod
263
+ def is_scoped(
264
+ cls,
265
+ call_node: Node,
266
+ callee_text: str,
267
+ language: str,
268
+ source_bytes: bytes,
269
+ ) -> bool:
270
+ """Check if resource open call is properly scoped by language construct."""
271
+ if cls._is_returned(call_node, source_bytes, language):
272
+ return True
273
+
274
+ if language == "python":
275
+ curr = call_node.parent
276
+ while curr is not None:
277
+ if curr.type in ("with_clause", "with_item"):
278
+ return True
279
+ if curr.type == "function_definition":
280
+ break
281
+ curr = curr.parent
282
+
283
+ # Support Python try ... finally: ...close()
284
+ enclosing = call_node.parent
285
+ while enclosing is not None:
286
+ if enclosing.type in ("function_definition", "module"):
287
+ break
288
+ enclosing = enclosing.parent
289
+
290
+ if enclosing is not None:
291
+ var_name = cls._extract_assigned_var(call_node, source_bytes, language)
292
+ if var_name:
293
+ def has_finally_close(n: Node) -> bool:
294
+ if n.type == "try_statement":
295
+ for c in n.children:
296
+ if c.type == "finally_clause":
297
+ txt = source_bytes[c.start_byte : c.end_byte].decode("utf-8", errors="ignore")
298
+ if "close" in txt.lower():
299
+ if re.search(rf"\b{re.escape(var_name)}\b", txt):
300
+ return True
301
+ for ch in n.children:
302
+ if ch.type == "function_definition":
303
+ continue
304
+ if has_finally_close(ch):
305
+ return True
306
+ return False
307
+
308
+ if has_finally_close(enclosing):
309
+ return True
310
+
311
+ return False
312
+
313
+ elif language in ("typescript", "javascript"):
314
+ var_name = cls._extract_assigned_var(call_node, source_bytes, language)
315
+
316
+ # 1. Ancestor check: inside try block with finally
317
+ curr = call_node.parent
318
+ while curr is not None:
319
+ if curr.type == "try_statement":
320
+ for c in curr.children:
321
+ if c.type == "finally_clause":
322
+ if var_name:
323
+ txt = source_bytes[c.start_byte : c.end_byte].decode("utf-8", errors="ignore")
324
+ if re.search(rf"\b{re.escape(var_name)}\b", txt):
325
+ return True
326
+ if curr.type in ("function_declaration", "arrow_function", "method_definition"):
327
+ break
328
+ curr = curr.parent
329
+
330
+ # 2. Chained with .pipe / .on
331
+ if call_node.parent and call_node.parent.type == "member_expression":
332
+ prop = call_node.parent.child_by_field_name("property")
333
+ if prop:
334
+ prop_name = source_bytes[prop.start_byte : prop.end_byte].decode("utf-8", errors="ignore").strip()
335
+ if prop_name in ("pipe", "on", "then"):
336
+ return True
337
+
338
+ # Passed directly to .pipe(...) as argument
339
+ if call_node.parent and call_node.parent.type == "arguments":
340
+ p_call = call_node.parent.parent
341
+ if p_call and p_call.type == "call_expression":
342
+ fn = p_call.child_by_field_name("function")
343
+ if fn and fn.type == "member_expression":
344
+ prop = fn.child_by_field_name("property")
345
+ if prop:
346
+ prop_name = source_bytes[prop.start_byte : prop.end_byte].decode("utf-8", errors="ignore").strip()
347
+ if prop_name == "pipe":
348
+ return True
349
+
350
+ # 3. Check enclosing block for try/finally or close/destroy/pipe
351
+ enclosing = call_node.parent
352
+ while enclosing is not None:
353
+ if enclosing.type in ("statement_block", "program", "function_declaration", "arrow_function", "method_definition"):
354
+ break
355
+ enclosing = enclosing.parent
356
+
357
+ if enclosing is not None and var_name:
358
+ def check_block(n: Node) -> bool:
359
+ if n.type == "try_statement":
360
+ for c in n.children:
361
+ if c.type == "finally_clause":
362
+ txt = source_bytes[c.start_byte : c.end_byte].decode("utf-8", errors="ignore")
363
+ if re.search(rf"\b{re.escape(var_name)}\b", txt):
364
+ return True
365
+ elif n.type == "call_expression":
366
+ fn = n.child_by_field_name("function")
367
+ if fn and fn.type == "member_expression":
368
+ prop = fn.child_by_field_name("property")
369
+ prop_name = source_bytes[prop.start_byte : prop.end_byte].decode("utf-8", errors="ignore").strip() if prop else ""
370
+ if prop_name == "pipe":
371
+ obj = fn.child_by_field_name("object")
372
+ if obj:
373
+ obj_name = source_bytes[obj.start_byte : obj.end_byte].decode("utf-8", errors="ignore").strip()
374
+ if obj_name == var_name:
375
+ return True
376
+ args = n.child_by_field_name("arguments")
377
+ if args:
378
+ args_txt = source_bytes[args.start_byte : args.end_byte].decode("utf-8", errors="ignore")
379
+ if re.search(rf"\b{re.escape(var_name)}\b", args_txt):
380
+ return True
381
+ for ch in n.children:
382
+ if ch.type in ("function_declaration", "arrow_function", "method_definition"):
383
+ continue
384
+ if check_block(ch):
385
+ return True
386
+ return False
387
+
388
+ if check_block(enclosing):
389
+ return True
390
+
391
+ return False
392
+
393
+ elif language == "go":
394
+ curr = call_node.parent
395
+ enclosing_func: Optional[Node] = None
396
+ while curr is not None:
397
+ if curr.type in ("function_declaration", "method_declaration", "func_literal"):
398
+ enclosing_func = curr
399
+ break
400
+ curr = curr.parent
401
+
402
+ if enclosing_func is None:
403
+ return False
404
+
405
+ var_name = cls._extract_assigned_var(call_node, source_bytes, language)
406
+ if not var_name:
407
+ return False
408
+
409
+ def has_defer_or_close(n: Node) -> bool:
410
+ if n.type in ("defer_statement", "call_expression"):
411
+ txt = source_bytes[n.start_byte : n.end_byte].decode("utf-8", errors="ignore")
412
+ if "Close" in txt and re.search(rf"\b{re.escape(var_name)}\b", txt):
413
+ return True
414
+ for ch in n.children:
415
+ if ch.type in ("function_declaration", "method_declaration", "func_literal"):
416
+ continue
417
+ if has_defer_or_close(ch):
418
+ return True
419
+ return False
420
+
421
+ return has_defer_or_close(enclosing_func)
422
+
423
+ elif language == "rust":
424
+ return False
425
+
426
+ return True
427
+
428
+ @classmethod
429
+ def check(
430
+ cls,
431
+ call_node: Node,
432
+ callee_text: str,
433
+ language: str,
434
+ source_bytes: bytes,
435
+ file_path: str,
436
+ lines: List[str],
437
+ ) -> Optional[PerfDiagnostic]:
438
+ """Evaluate if resource is opened without scoped cleanup."""
439
+ if not cls.is_open_call(callee_text, language):
440
+ return None
441
+
442
+ if cls.is_scoped(call_node, callee_text, language, source_bytes):
443
+ return None
444
+
445
+ lineno = call_node.start_point.row + 1
446
+ end_lineno = call_node.end_point.row + 1
447
+ col = call_node.start_point.column
448
+ end_col = call_node.end_point.column
449
+ ctx = lines[lineno - 1].strip() if 1 <= lineno <= len(lines) else None
450
+
451
+ scope_mechanism = (
452
+ "'with' context manager"
453
+ if language == "python"
454
+ else (
455
+ "'defer ...Close()'"
456
+ if language == "go"
457
+ else "'try/finally'"
458
+ )
459
+ )
460
+ if language == "rust":
461
+ msg = f"Potential resource or memory leak via '{callee_text}'"
462
+ else:
463
+ msg = f"Resource '{callee_text}' opened without scoped {scope_mechanism}"
464
+
465
+ return PerfDiagnostic(
466
+ rule_id=cls.RULE_ID,
467
+ message=msg,
468
+ severity=Severity.ERROR,
469
+ file_path=file_path,
470
+ lineno=lineno,
471
+ end_lineno=end_lineno,
472
+ col_offset=col,
473
+ end_col_offset=end_col,
474
+ context_line=ctx,
475
+ )
476
+
477
+
478
+ def check_unclosed_resource(
479
+ call_node: Node,
480
+ callee_text: str,
481
+ language: str,
482
+ source_bytes: bytes,
483
+ file_path: str,
484
+ lines: List[str],
485
+ ) -> Optional[PerfDiagnostic]:
486
+ """Convenience helper for PERF003 evaluation."""
487
+ return UnclosedResourceRule.check(
488
+ call_node=call_node,
489
+ callee_text=callee_text,
490
+ language=language,
491
+ source_bytes=source_bytes,
492
+ file_path=file_path,
493
+ lines=lines,
494
+ )