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.
- code_oracle/__init__.py +30 -0
- code_oracle/cli.py +795 -0
- code_oracle/config.py +145 -0
- code_oracle/dataset.py +5325 -0
- code_oracle/dead_code/__init__.py +32 -0
- code_oracle/dead_code/detector.py +379 -0
- code_oracle/dead_code/entrypoints.py +333 -0
- code_oracle/dead_code/models.py +255 -0
- code_oracle/dead_code/semantics.py +416 -0
- code_oracle/decision.py +906 -0
- code_oracle/engine.py +430 -0
- code_oracle/export_onnx.py +436 -0
- code_oracle/hook.py +531 -0
- code_oracle/indexer.py +894 -0
- code_oracle/languages/__init__.py +114 -0
- code_oracle/languages/common.py +127 -0
- code_oracle/languages/go.py +395 -0
- code_oracle/languages/python.py +336 -0
- code_oracle/languages/rust.py +474 -0
- code_oracle/languages/typescript.py +775 -0
- code_oracle/linearizer.py +166 -0
- code_oracle/locator.py +301 -0
- code_oracle/models.py +237 -0
- code_oracle/perf_lint/__init__.py +38 -0
- code_oracle/perf_lint/engine.py +234 -0
- code_oracle/perf_lint/models.py +229 -0
- code_oracle/perf_lint/rules/__init__.py +31 -0
- code_oracle/perf_lint/rules/async_blocking.py +143 -0
- code_oracle/perf_lint/rules/n_plus_one.py +232 -0
- code_oracle/perf_lint/rules/nested_loops.py +137 -0
- code_oracle/perf_lint/rules/unclosed_res.py +494 -0
- code_oracle/perf_lint/visitor.py +299 -0
- code_oracle/server.py +184 -0
- code_oracle/slicer.py +225 -0
- code_oracle/symbolic.py +459 -0
- code_oracle-0.1.0.dist-info/METADATA +225 -0
- code_oracle-0.1.0.dist-info/RECORD +40 -0
- code_oracle-0.1.0.dist-info/WHEEL +4 -0
- code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|