1use std::collections::{HashMap, HashSet};
36use std::path::PathBuf;
37use std::time::Instant;
38
39use clap::Args;
40use tree_sitter::{Node, Parser};
41
42use tldr_core::ast::ParserPool;
43use tldr_core::types::Language;
44
45use super::error::{PatternsError, PatternsResult};
46use super::types::{
47 ContextSuggestion, DoubleCloseInfo, LeakInfo, OutputFormat, ResourceConstraint, ResourceInfo,
48 ResourceReport, ResourceSummary, UseAfterCloseInfo,
49};
50use super::validation::{read_file_safe, validate_file_path, validate_file_path_in_project};
51use crate::output::OutputFormat as GlobalOutputFormat;
52
53pub const MAX_PATHS: usize = 1000;
59
60struct LangResourcePatterns {
66 creators: &'static [(&'static str, &'static str)], closers: &'static [&'static str],
70 function_kinds: &'static [&'static str],
72 name_field: &'static str,
74 body_kinds: &'static [&'static str],
76 assignment_kinds: &'static [&'static str],
78 return_kinds: &'static [&'static str],
80 if_kinds: &'static [&'static str],
82 loop_kinds: &'static [&'static str],
84 try_kinds: &'static [&'static str],
86 cleanup_block_kinds: &'static [&'static str],
88}
89
90fn get_resource_patterns(lang: Language) -> LangResourcePatterns {
91 match lang {
92 Language::Python => LangResourcePatterns {
93 creators: &[
94 ("open", "file"),
95 ("socket", "socket"),
96 ("create_connection", "socket"),
97 ("connect", "connection"),
98 ("cursor", "cursor"),
99 ("urlopen", "url_connection"),
100 ("request", "http_connection"),
101 ("popen", "process"),
102 ("Popen", "process"),
103 ("Lock", "lock"),
104 ("RLock", "lock"),
105 ("Semaphore", "semaphore"),
106 ("Event", "event"),
107 ("Condition", "condition"),
108 ],
109 closers: &[
110 "close",
111 "shutdown",
112 "disconnect",
113 "release",
114 "dispose",
115 "cleanup",
116 "terminate",
117 "__exit__",
118 ],
119 function_kinds: &["function_definition"],
120 name_field: "name",
121 body_kinds: &["block"],
122 assignment_kinds: &["assignment"],
123 return_kinds: &["return_statement", "raise_statement"],
124 if_kinds: &["if_statement"],
125 loop_kinds: &["for_statement", "while_statement"],
126 try_kinds: &["try_statement"],
127 cleanup_block_kinds: &["with_statement"],
128 },
129 Language::Go => LangResourcePatterns {
130 creators: &[
131 ("Open", "file"),
132 ("Create", "file"),
133 ("OpenFile", "file"),
134 ("NewFile", "file"),
135 ("Dial", "connection"),
136 ("DialTCP", "connection"),
137 ("DialUDP", "connection"),
138 ("DialTimeout", "connection"),
139 ("Listen", "listener"),
140 ("ListenTCP", "listener"),
141 ("ListenAndServe", "server"),
142 ("NewReader", "reader"),
143 ("NewWriter", "writer"),
144 ("NewScanner", "scanner"),
145 ("Get", "http_response"),
146 ("Post", "http_response"),
147 ("NewRequest", "http_request"),
148 ("Connect", "connection"),
149 ("NewClient", "client"),
150 ("Pipe", "pipe"),
151 ("TempFile", "file"),
152 ],
153 closers: &["Close", "Shutdown", "Stop", "Release", "Flush"],
154 function_kinds: &["function_declaration", "method_declaration"],
155 name_field: "name",
156 body_kinds: &["block"],
157 assignment_kinds: &["short_var_declaration", "assignment_statement"],
158 return_kinds: &["return_statement"],
159 if_kinds: &["if_statement"],
160 loop_kinds: &["for_statement"],
161 try_kinds: &[],
162 cleanup_block_kinds: &["defer_statement"],
163 },
164 Language::Rust => LangResourcePatterns {
165 creators: &[
166 ("open", "file"),
167 ("create", "file"),
168 ("connect", "connection"),
169 ("bind", "listener"),
170 ("lock", "mutex_guard"),
171 ("read_lock", "rwlock_guard"),
172 ("write_lock", "rwlock_guard"),
173 ("try_lock", "mutex_guard"),
174 ("spawn", "thread_handle"),
175 ("new", "resource"),
176 ("from_raw_fd", "file_descriptor"),
177 ("into_raw_fd", "file_descriptor"),
178 ("TcpStream", "connection"),
179 ("TcpListener", "listener"),
180 ("UdpSocket", "socket"),
181 ("File", "file"),
182 ("BufReader", "reader"),
183 ("BufWriter", "writer"),
184 ],
185 closers: &["drop", "close", "shutdown", "flush", "sync_all"],
186 function_kinds: &["function_item"],
187 name_field: "name",
188 body_kinds: &["block"],
189 assignment_kinds: &["let_declaration"],
190 return_kinds: &["return_expression"],
191 if_kinds: &["if_expression"],
192 loop_kinds: &["for_expression", "while_expression", "loop_expression"],
193 try_kinds: &[],
194 cleanup_block_kinds: &[],
195 },
196 Language::Java => LangResourcePatterns {
197 creators: &[
198 ("FileInputStream", "file_stream"),
199 ("FileOutputStream", "file_stream"),
200 ("FileReader", "reader"),
201 ("FileWriter", "writer"),
202 ("BufferedReader", "reader"),
203 ("BufferedWriter", "writer"),
204 ("InputStreamReader", "reader"),
205 ("OutputStreamWriter", "writer"),
206 ("PrintWriter", "writer"),
207 ("Scanner", "scanner"),
208 ("Socket", "socket"),
209 ("ServerSocket", "server_socket"),
210 ("Connection", "connection"),
211 ("getConnection", "connection"),
212 ("prepareStatement", "statement"),
213 ("createStatement", "statement"),
214 ("openConnection", "connection"),
215 ("newInputStream", "stream"),
216 ("newOutputStream", "stream"),
217 ("RandomAccessFile", "file"),
218 ],
219 closers: &[
220 "close",
221 "shutdown",
222 "disconnect",
223 "dispose",
224 "release",
225 "flush",
226 ],
227 function_kinds: &["method_declaration", "constructor_declaration"],
228 name_field: "name",
229 body_kinds: &["block"],
230 assignment_kinds: &["local_variable_declaration"],
231 return_kinds: &["return_statement", "throw_statement"],
232 if_kinds: &["if_statement"],
233 loop_kinds: &[
234 "for_statement",
235 "enhanced_for_statement",
236 "while_statement",
237 "do_statement",
238 ],
239 try_kinds: &["try_statement", "try_with_resources_statement"],
240 cleanup_block_kinds: &["try_with_resources_statement"],
241 },
242 Language::TypeScript | Language::JavaScript => LangResourcePatterns {
243 creators: &[
244 ("open", "file"),
245 ("openSync", "file"),
246 ("createReadStream", "stream"),
247 ("createWriteStream", "stream"),
248 ("createServer", "server"),
249 ("connect", "connection"),
250 ("createConnection", "connection"),
251 ("fetch", "response"),
252 ("request", "request"),
253 ("get", "request"),
254 ("post", "request"),
255 ("WebSocket", "websocket"),
256 ("createPool", "pool"),
257 ("getConnection", "connection"),
258 ],
259 closers: &[
260 "close",
261 "end",
262 "destroy",
263 "disconnect",
264 "release",
265 "abort",
266 "unref",
267 ],
268 function_kinds: &[
269 "function_declaration",
270 "arrow_function",
271 "method_definition",
272 "function",
273 ],
274 name_field: "name",
275 body_kinds: &["statement_block"],
276 assignment_kinds: &[
277 "variable_declaration",
278 "lexical_declaration",
279 "assignment_expression",
280 ],
281 return_kinds: &["return_statement", "throw_statement"],
282 if_kinds: &["if_statement"],
283 loop_kinds: &[
284 "for_statement",
285 "for_in_statement",
286 "while_statement",
287 "do_statement",
288 ],
289 try_kinds: &["try_statement"],
290 cleanup_block_kinds: &[],
291 },
292 Language::C => LangResourcePatterns {
293 creators: &[
294 ("fopen", "file"),
295 ("fdopen", "file"),
296 ("tmpfile", "file"),
297 ("open", "file_descriptor"),
298 ("creat", "file_descriptor"),
299 ("socket", "socket"),
300 ("accept", "socket"),
301 ("malloc", "memory"),
302 ("calloc", "memory"),
303 ("realloc", "memory"),
304 ("strdup", "memory"),
305 ("mmap", "memory_map"),
306 ("opendir", "directory"),
307 ("popen", "process"),
308 ("dlopen", "dynamic_lib"),
309 ("CreateFile", "file_handle"),
310 ],
311 closers: &[
312 "fclose",
313 "close",
314 "free",
315 "munmap",
316 "closedir",
317 "pclose",
318 "dlclose",
319 "shutdown",
320 "CloseHandle",
321 ],
322 function_kinds: &["function_definition"],
323 name_field: "declarator",
324 body_kinds: &["compound_statement"],
325 assignment_kinds: &["declaration", "assignment_expression"],
326 return_kinds: &["return_statement"],
327 if_kinds: &["if_statement"],
328 loop_kinds: &["for_statement", "while_statement", "do_statement"],
329 try_kinds: &[],
330 cleanup_block_kinds: &[],
331 },
332 Language::Cpp => LangResourcePatterns {
333 creators: &[
334 ("fopen", "file"),
335 ("open", "file_descriptor"),
336 ("socket", "socket"),
337 ("malloc", "memory"),
338 ("calloc", "memory"),
339 ("realloc", "memory"),
340 ("new", "heap_object"),
341 ("make_unique", "unique_ptr"),
342 ("make_shared", "shared_ptr"),
343 ("ifstream", "file_stream"),
344 ("ofstream", "file_stream"),
345 ("fstream", "file_stream"),
346 ("CreateFile", "file_handle"),
347 ("connect", "connection"),
348 ],
349 closers: &[
350 "fclose",
351 "close",
352 "free",
353 "delete",
354 "shutdown",
355 "release",
356 "CloseHandle",
357 "destroy",
358 ],
359 function_kinds: &["function_definition"],
360 name_field: "declarator",
361 body_kinds: &["compound_statement"],
362 assignment_kinds: &["declaration", "assignment_expression"],
363 return_kinds: &["return_statement", "throw_statement"],
364 if_kinds: &["if_statement"],
365 loop_kinds: &[
366 "for_statement",
367 "while_statement",
368 "do_statement",
369 "for_range_loop",
370 ],
371 try_kinds: &["try_statement"],
372 cleanup_block_kinds: &[],
373 },
374 Language::Ruby => LangResourcePatterns {
375 creators: &[
376 ("open", "file"),
377 ("new", "resource"),
378 ("popen", "process"),
379 ("TCPSocket", "socket"),
380 ("UNIXSocket", "socket"),
381 ("connect", "connection"),
382 ],
383 closers: &["close", "shutdown", "disconnect", "release"],
384 function_kinds: &["method", "singleton_method"],
385 name_field: "name",
386 body_kinds: &["body_statement"],
387 assignment_kinds: &["assignment"],
388 return_kinds: &["return", "raise"],
389 if_kinds: &["if", "unless"],
390 loop_kinds: &["for", "while", "until"],
391 try_kinds: &["begin"],
392 cleanup_block_kinds: &["do_block"],
393 },
394 Language::CSharp => LangResourcePatterns {
395 creators: &[
396 ("FileStream", "file_stream"),
397 ("StreamReader", "reader"),
398 ("StreamWriter", "writer"),
399 ("File.Open", "file"),
400 ("File.OpenRead", "file"),
401 ("File.OpenWrite", "file"),
402 ("SqlConnection", "connection"),
403 ("HttpClient", "http_client"),
404 ("TcpClient", "tcp_client"),
405 ("Socket", "socket"),
406 ],
407 closers: &["Close", "Dispose", "Shutdown", "Release", "Flush"],
408 function_kinds: &["method_declaration", "constructor_declaration"],
409 name_field: "name",
410 body_kinds: &["block"],
411 assignment_kinds: &["local_declaration_statement", "assignment_expression"],
412 return_kinds: &["return_statement", "throw_statement"],
413 if_kinds: &["if_statement"],
414 loop_kinds: &[
415 "for_statement",
416 "foreach_statement",
417 "while_statement",
418 "do_statement",
419 ],
420 try_kinds: &["try_statement"],
421 cleanup_block_kinds: &["using_statement"],
422 },
423 Language::Php => LangResourcePatterns {
424 creators: &[
425 ("fopen", "file"),
426 ("tmpfile", "file"),
427 ("fsockopen", "socket"),
428 ("pfsockopen", "socket"),
429 ("curl_init", "curl"),
430 ("mysqli_connect", "connection"),
431 ("PDO", "connection"),
432 ("popen", "process"),
433 ("opendir", "directory"),
434 ],
435 closers: &[
436 "fclose",
437 "curl_close",
438 "mysqli_close",
439 "pclose",
440 "closedir",
441 "close",
442 ],
443 function_kinds: &["function_definition", "method_declaration"],
444 name_field: "name",
445 body_kinds: &["compound_statement"],
446 assignment_kinds: &["assignment_expression"],
447 return_kinds: &["return_statement", "throw_expression"],
448 if_kinds: &["if_statement"],
449 loop_kinds: &[
450 "for_statement",
451 "foreach_statement",
452 "while_statement",
453 "do_statement",
454 ],
455 try_kinds: &["try_statement"],
456 cleanup_block_kinds: &[],
457 },
458 Language::Elixir => LangResourcePatterns {
459 creators: &[
460 ("open", "file"),
461 ("open!", "file"),
462 ("connect", "connection"),
463 ("start_link", "process"),
464 ("start", "process"),
465 ],
466 closers: &["close", "stop", "disconnect"],
467 function_kinds: &["call"], name_field: "target",
469 body_kinds: &["do_block"],
470 assignment_kinds: &["binary_operator"], return_kinds: &[],
472 if_kinds: &["call"], loop_kinds: &["call"], try_kinds: &["call"], cleanup_block_kinds: &[],
476 },
477 Language::Scala => LangResourcePatterns {
478 creators: &[
479 ("Source", "source"),
480 ("fromFile", "source"),
481 ("FileInputStream", "stream"),
482 ("FileOutputStream", "stream"),
483 ("BufferedSource", "source"),
484 ("getConnection", "connection"),
485 ],
486 closers: &["close", "shutdown", "disconnect", "dispose"],
487 function_kinds: &["function_definition"],
488 name_field: "name",
489 body_kinds: &["block"],
490 assignment_kinds: &["val_definition", "var_definition"],
491 return_kinds: &["return_expression"],
492 if_kinds: &["if_expression"],
493 loop_kinds: &["for_expression", "while_expression"],
494 try_kinds: &["try_expression"],
495 cleanup_block_kinds: &[],
496 },
497 Language::Kotlin => LangResourcePatterns {
498 creators: &[
499 ("FileInputStream", "file_stream"),
500 ("FileOutputStream", "file_stream"),
501 ("FileReader", "reader"),
502 ("FileWriter", "writer"),
503 ("BufferedReader", "reader"),
504 ("BufferedWriter", "writer"),
505 ("InputStreamReader", "reader"),
506 ("OutputStreamWriter", "writer"),
507 ("PrintWriter", "writer"),
508 ("Scanner", "scanner"),
509 ("Socket", "socket"),
510 ("ServerSocket", "server_socket"),
511 ("getConnection", "connection"),
512 ("openConnection", "connection"),
513 ("File", "file"),
514 ("RandomAccessFile", "file"),
515 ],
516 closers: &["close", "shutdown", "dispose", "use"],
517 function_kinds: &["function_declaration"],
518 name_field: "name",
519 body_kinds: &["function_body"],
520 assignment_kinds: &["property_declaration", "assignment"],
521 return_kinds: &["jump_expression"],
522 if_kinds: &["if_expression"],
523 loop_kinds: &["for_statement", "while_statement"],
524 try_kinds: &["try_expression"],
525 cleanup_block_kinds: &["call_expression"], },
527 Language::Swift => LangResourcePatterns {
528 creators: &[
529 ("FileHandle", "file_handle"),
530 ("OutputStream", "stream"),
531 ("InputStream", "stream"),
532 ("URLSession", "session"),
533 ("FileManager", "file_manager"),
534 ("fopen", "file"),
535 ("open", "file"),
536 ("Socket", "socket"),
537 ("NWConnection", "connection"),
538 ],
539 closers: &[
540 "closeFile",
541 "close",
542 "shutdown",
543 "invalidateAndCancel",
544 "cancel",
545 ],
546 function_kinds: &["function_declaration"],
547 name_field: "name",
548 body_kinds: &["function_body"],
549 assignment_kinds: &["property_declaration", "directly_assignable_expression"],
550 return_kinds: &["control_transfer_statement"],
551 if_kinds: &["if_statement"],
552 loop_kinds: &["for_statement", "while_statement"],
553 try_kinds: &["do_statement"], cleanup_block_kinds: &[], },
556 Language::Ocaml => LangResourcePatterns {
557 creators: &[
558 ("open_in", "input_channel"),
559 ("open_out", "output_channel"),
560 ("open_in_bin", "input_channel"),
561 ("open_out_bin", "output_channel"),
562 ("Unix.openfile", "file_descriptor"),
563 ("Unix.socket", "socket"),
564 ("open_connection", "connection"),
565 ("connect", "connection"),
566 ],
567 closers: &[
568 "close_in",
569 "close_out",
570 "close_in_noerr",
571 "close_out_noerr",
572 "Unix.close",
573 "close_connection",
574 ],
575 function_kinds: &["let_binding", "value_definition"],
576 name_field: "pattern",
577 body_kinds: &["let_expression", "sequence_expression"],
578 assignment_kinds: &["let_binding"],
579 return_kinds: &[],
580 if_kinds: &["if_expression"],
581 loop_kinds: &["for_expression", "while_expression"],
582 try_kinds: &["try_expression"],
583 cleanup_block_kinds: &[],
584 },
585 Language::Lua | Language::Luau => LangResourcePatterns {
586 creators: &[
587 ("io.open", "file"),
588 ("io.popen", "process"),
589 ("io.tmpfile", "file"),
590 ("socket.tcp", "socket"),
591 ("socket.udp", "socket"),
592 ("socket.connect", "connection"),
593 ("open", "file"),
594 ],
595 closers: &["close"],
596 function_kinds: &["function_declaration", "function_definition"],
597 name_field: "name",
598 body_kinds: &["body"],
599 assignment_kinds: &["assignment_statement", "variable_declaration"],
600 return_kinds: &["return_statement"],
601 if_kinds: &["if_statement"],
602 loop_kinds: &["for_statement", "for_in_statement", "while_statement"],
603 try_kinds: &[],
604 cleanup_block_kinds: &[],
605 },
606 }
607}
608
609pub const RESOURCE_CREATORS: &[&str] = &[
611 "open",
612 "socket",
613 "create_connection",
614 "connect",
615 "cursor",
616 "urlopen",
617 "request",
618 "popen",
619 "Popen",
620 "Lock",
621 "RLock",
622 "Semaphore",
623 "Event",
624 "Condition",
625 "contextlib.closing",
626];
627
628pub const RESOURCE_CLOSERS: &[&str] = &[
630 "close",
631 "shutdown",
632 "disconnect",
633 "release",
634 "dispose",
635 "cleanup",
636 "terminate",
637 "__exit__",
638];
639
640const RESOURCE_TYPE_MAP: &[(&str, &str)] = &[
642 ("open", "file"),
643 ("socket", "socket"),
644 ("create_connection", "socket"),
645 ("connect", "connection"),
646 ("cursor", "cursor"),
647 ("urlopen", "url_connection"),
648 ("request", "http_connection"),
649 ("popen", "process"),
650 ("Popen", "process"),
651 ("Lock", "lock"),
652 ("RLock", "lock"),
653 ("Semaphore", "semaphore"),
654 ("Event", "event"),
655 ("Condition", "condition"),
656];
657
658#[derive(Debug, Args, Clone)]
664pub struct ResourcesArgs {
665 pub file: PathBuf,
667
668 pub function: Option<String>,
670
671 #[arg(long, short = 'l')]
673 pub lang: Option<Language>,
674
675 #[arg(long, default_value = "true")]
677 pub check_leaks: bool,
678
679 #[arg(long)]
681 pub check_double_close: bool,
682
683 #[arg(long)]
685 pub check_use_after_close: bool,
686
687 #[arg(long)]
689 pub check_all: bool,
690
691 #[arg(long)]
693 pub suggest_context: bool,
694
695 #[arg(long)]
697 pub show_paths: bool,
698
699 #[arg(long)]
701 pub constraints: bool,
702
703 #[arg(long)]
705 pub summary: bool,
706
707 #[arg(
709 long = "output",
710 short = 'o',
711 hide = true,
712 default_value = "json",
713 value_enum
714 )]
715 pub output_format: OutputFormat,
716
717 #[arg(long)]
719 pub project_root: Option<PathBuf>,
720}
721
722impl ResourcesArgs {
723 pub fn run(&self, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
725 run(self.clone(), global_format)
726 }
727}
728
729#[derive(Debug, Clone)]
735pub struct BasicBlock {
736 pub id: usize,
738 pub stmts: Vec<(usize, usize, String, String)>,
740 pub lines: Vec<u32>,
742 pub preds: Vec<usize>,
744 pub succs: Vec<usize>,
746 pub is_entry: bool,
748 pub is_exit: bool,
750 pub exception_handlers: Vec<usize>,
752}
753
754impl BasicBlock {
755 fn new(id: usize) -> Self {
756 Self {
757 id,
758 stmts: Vec::new(),
759 lines: Vec::new(),
760 preds: Vec::new(),
761 succs: Vec::new(),
762 is_entry: false,
763 is_exit: false,
764 exception_handlers: Vec::new(),
765 }
766 }
767}
768
769#[derive(Debug)]
771pub struct SimpleCfg {
772 pub blocks: HashMap<usize, BasicBlock>,
774 pub entry_block: usize,
776 pub exit_blocks: Vec<usize>,
778 next_id: usize,
780}
781
782impl SimpleCfg {
783 fn new() -> Self {
784 Self {
785 blocks: HashMap::new(),
786 entry_block: 0,
787 exit_blocks: Vec::new(),
788 next_id: 0,
789 }
790 }
791
792 fn new_block(&mut self) -> usize {
793 let id = self.next_id;
794 self.next_id += 1;
795 self.blocks.insert(id, BasicBlock::new(id));
796 id
797 }
798
799 fn add_edge(&mut self, from: usize, to: usize) {
800 if let Some(block) = self.blocks.get_mut(&from) {
801 if !block.succs.contains(&to) {
802 block.succs.push(to);
803 }
804 }
805 if let Some(block) = self.blocks.get_mut(&to) {
806 if !block.preds.contains(&from) {
807 block.preds.push(from);
808 }
809 }
810 }
811
812 fn mark_exit(&mut self, id: usize) {
813 if let Some(block) = self.blocks.get_mut(&id) {
814 block.is_exit = true;
815 }
816 if !self.exit_blocks.contains(&id) {
817 self.exit_blocks.push(id);
818 }
819 }
820}
821
822pub fn build_cfg(func_node: Node, source: &[u8]) -> SimpleCfg {
828 let mut cfg = SimpleCfg::new();
829 let entry_id = cfg.new_block();
830 cfg.entry_block = entry_id;
831
832 if let Some(block) = cfg.blocks.get_mut(&entry_id) {
833 block.is_entry = true;
834 }
835
836 let body = func_node
838 .children(&mut func_node.walk())
839 .find(|n| n.kind() == "block");
840
841 if let Some(body_node) = body {
842 let exit_id = process_statements(&mut cfg, body_node, source, entry_id);
843 if let Some(exit) = exit_id {
844 if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
846 cfg.mark_exit(exit);
847 }
848 }
849 } else {
850 cfg.mark_exit(entry_id);
852 }
853
854 cfg
855}
856
857fn process_statements(
858 cfg: &mut SimpleCfg,
859 node: Node,
860 source: &[u8],
861 mut current: usize,
862) -> Option<usize> {
863 let mut cursor = node.walk();
864 for child in node.children(&mut cursor) {
865 match child.kind() {
866 "expression_statement"
868 | "assignment"
869 | "augmented_assignment"
870 | "return_statement"
871 | "pass_statement"
872 | "break_statement"
873 | "continue_statement"
874 | "raise_statement"
875 | "assert_statement"
876 | "global_statement"
877 | "nonlocal_statement"
878 | "import_statement"
879 | "import_from_statement"
880 | "delete_statement" => {
881 let text = node_text(child, source).to_string();
882 let line = child.start_position().row as u32 + 1;
883 if let Some(block) = cfg.blocks.get_mut(¤t) {
884 block.stmts.push((
885 child.start_byte(),
886 child.end_byte(),
887 child.kind().to_string(),
888 text,
889 ));
890 block.lines.push(line);
891 }
892
893 if child.kind() == "return_statement" || child.kind() == "raise_statement" {
895 cfg.mark_exit(current);
896 return None; }
898 }
899
900 "if_statement" => {
902 current = process_if_statement(cfg, child, source, current)?;
903 }
904
905 "for_statement" | "while_statement" => {
907 current = process_loop(cfg, child, source, current)?;
908 }
909
910 "try_statement" => {
912 current = process_try(cfg, child, source, current)?;
913 }
914
915 "with_statement" => {
917 current = process_with(cfg, child, source, current)?;
918 }
919
920 _ => {
921 let text = node_text(child, source).to_string();
923 let line = child.start_position().row as u32 + 1;
924 if let Some(block) = cfg.blocks.get_mut(¤t) {
925 block.stmts.push((
926 child.start_byte(),
927 child.end_byte(),
928 child.kind().to_string(),
929 text,
930 ));
931 block.lines.push(line);
932 }
933 }
934 }
935 }
936
937 Some(current)
938}
939
940fn process_if_statement(
941 cfg: &mut SimpleCfg,
942 node: Node,
943 source: &[u8],
944 current: usize,
945) -> Option<usize> {
946 if let Some(cond) = node.child_by_field_name("condition") {
948 let text = node_text(cond, source).to_string();
949 let line = cond.start_position().row as u32 + 1;
950 if let Some(block) = cfg.blocks.get_mut(¤t) {
951 block.stmts.push((
952 cond.start_byte(),
953 cond.end_byte(),
954 "condition".to_string(),
955 text,
956 ));
957 block.lines.push(line);
958 }
959 }
960
961 let true_block = cfg.new_block();
963 cfg.add_edge(current, true_block);
964
965 let mut cursor = node.walk();
967 let consequence = node.children(&mut cursor).find(|n| n.kind() == "block");
968 let true_exit = if let Some(body) = consequence {
969 process_statements(cfg, body, source, true_block)
970 } else {
971 Some(true_block)
972 };
973
974 let mut cursor = node.walk();
976 let alternative = node
977 .children(&mut cursor)
978 .find(|n| n.kind() == "else_clause" || n.kind() == "elif_clause");
979
980 let false_exit = if let Some(alt) = alternative {
981 let false_block = cfg.new_block();
982 cfg.add_edge(current, false_block);
983
984 if let Some(alt_body) = alt.children(&mut alt.walk()).find(|n| n.kind() == "block") {
986 process_statements(cfg, alt_body, source, false_block)
987 } else {
988 Some(false_block)
989 }
990 } else {
991 None
993 };
994
995 let merge = cfg.new_block();
997
998 if let Some(te) = true_exit {
999 cfg.add_edge(te, merge);
1000 }
1001 if let Some(fe) = false_exit {
1002 cfg.add_edge(fe, merge);
1003 }
1004 if alternative.is_none() {
1005 cfg.add_edge(current, merge);
1007 }
1008
1009 Some(merge)
1010}
1011
1012fn process_loop(cfg: &mut SimpleCfg, node: Node, source: &[u8], current: usize) -> Option<usize> {
1013 let header = cfg.new_block();
1015 cfg.add_edge(current, header);
1016
1017 if let Some(cond) = node.child_by_field_name("condition") {
1019 let text = node_text(cond, source).to_string();
1020 let line = cond.start_position().row as u32 + 1;
1021 if let Some(block) = cfg.blocks.get_mut(&header) {
1022 block.stmts.push((
1023 cond.start_byte(),
1024 cond.end_byte(),
1025 "loop_condition".to_string(),
1026 text,
1027 ));
1028 block.lines.push(line);
1029 }
1030 }
1031
1032 let body_block = cfg.new_block();
1034 cfg.add_edge(header, body_block);
1035
1036 let body = node
1038 .children(&mut node.walk())
1039 .find(|n| n.kind() == "block");
1040 let body_exit = if let Some(body_node) = body {
1041 process_statements(cfg, body_node, source, body_block)
1042 } else {
1043 Some(body_block)
1044 };
1045
1046 if let Some(be) = body_exit {
1048 cfg.add_edge(be, header);
1049 }
1050
1051 let exit = cfg.new_block();
1053 cfg.add_edge(header, exit); Some(exit)
1056}
1057
1058fn process_try(cfg: &mut SimpleCfg, node: Node, source: &[u8], current: usize) -> Option<usize> {
1059 let try_block = cfg.new_block();
1061 cfg.add_edge(current, try_block);
1062
1063 let try_body = node
1065 .children(&mut node.walk())
1066 .find(|n| n.kind() == "block");
1067 let try_exit = if let Some(body) = try_body {
1068 process_statements(cfg, body, source, try_block)
1069 } else {
1070 Some(try_block)
1071 };
1072
1073 let mut cursor = node.walk();
1075 let mut handler_exits = Vec::new();
1076 for child in node.children(&mut cursor) {
1077 if child.kind() == "except_clause" {
1078 let handler_block = cfg.new_block();
1079 cfg.add_edge(try_block, handler_block);
1081 if let Some(block) = cfg.blocks.get_mut(&try_block) {
1082 block.exception_handlers.push(handler_block);
1083 }
1084
1085 if let Some(handler_body) = child
1087 .children(&mut child.walk())
1088 .find(|n| n.kind() == "block")
1089 {
1090 if let Some(exit) = process_statements(cfg, handler_body, source, handler_block) {
1091 handler_exits.push(exit);
1092 }
1093 } else {
1094 handler_exits.push(handler_block);
1095 }
1096 }
1097 }
1098
1099 let finally_clause = node
1101 .children(&mut node.walk())
1102 .find(|n| n.kind() == "finally_clause");
1103
1104 let merge = cfg.new_block();
1106
1107 if let Some(te) = try_exit {
1108 if let Some(finally) = finally_clause {
1109 let finally_block = cfg.new_block();
1111 cfg.add_edge(te, finally_block);
1112 if let Some(finally_body) = finally
1113 .children(&mut finally.walk())
1114 .find(|n| n.kind() == "block")
1115 {
1116 if let Some(exit) = process_statements(cfg, finally_body, source, finally_block) {
1117 cfg.add_edge(exit, merge);
1118 }
1119 } else {
1120 cfg.add_edge(finally_block, merge);
1121 }
1122 } else {
1123 cfg.add_edge(te, merge);
1124 }
1125 }
1126
1127 for he in handler_exits {
1128 cfg.add_edge(he, merge);
1129 }
1130
1131 Some(merge)
1132}
1133
1134fn process_with(cfg: &mut SimpleCfg, node: Node, source: &[u8], current: usize) -> Option<usize> {
1135 let text = node_text(node, source).to_string();
1137 let line = node.start_position().row as u32 + 1;
1138 if let Some(block) = cfg.blocks.get_mut(¤t) {
1139 block.stmts.push((
1140 node.start_byte(),
1141 node.end_byte(),
1142 "with_statement".to_string(),
1143 text,
1144 ));
1145 block.lines.push(line);
1146 }
1147
1148 let body = node
1150 .children(&mut node.walk())
1151 .find(|n| n.kind() == "block");
1152 if let Some(body_node) = body {
1153 process_statements(cfg, body_node, source, current)
1154 } else {
1155 Some(current)
1156 }
1157}
1158
1159#[derive(Debug, Clone)]
1165struct DetectedResource {
1166 name: String,
1168 resource_type: String,
1170 line: u32,
1172 in_context_manager: bool,
1174}
1175
1176pub struct ResourceDetector {
1178 resources: Vec<DetectedResource>,
1179 context_manager_vars: HashSet<String>,
1180 lang: Language,
1181}
1182
1183impl ResourceDetector {
1184 pub fn new() -> Self {
1185 Self {
1186 resources: Vec::new(),
1187 context_manager_vars: HashSet::new(),
1188 lang: Language::Python,
1189 }
1190 }
1191
1192 pub fn with_language(lang: Language) -> Self {
1193 Self {
1194 resources: Vec::new(),
1195 context_manager_vars: HashSet::new(),
1196 lang,
1197 }
1198 }
1199
1200 pub fn detect(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1202 self.resources.clear();
1203 self.context_manager_vars.clear();
1204 self.visit_node(func_node, source, false);
1205
1206 self.resources
1207 .iter()
1208 .map(|r| ResourceInfo {
1209 name: r.name.clone(),
1210 resource_type: r.resource_type.clone(),
1211 line: r.line,
1212 closed: r.in_context_manager,
1213 })
1214 .collect()
1215 }
1216
1217 pub fn detect_with_patterns(
1219 &mut self,
1220 func_node: Node,
1221 source: &[u8],
1222 ) -> Vec<ResourceInfo> {
1223 let patterns = get_resource_patterns(self.lang);
1224 self.resources.clear();
1225 self.context_manager_vars.clear();
1226 self.visit_node_multilang(func_node, source, false, &patterns);
1227
1228 self.resources
1229 .iter()
1230 .map(|r| ResourceInfo {
1231 name: r.name.clone(),
1232 resource_type: r.resource_type.clone(),
1233 line: r.line,
1234 closed: r.in_context_manager,
1235 })
1236 .collect()
1237 }
1238
1239 fn visit_node(&mut self, node: Node, source: &[u8], in_with: bool) {
1240 match node.kind() {
1241 "with_statement" => {
1242 let mut cursor = node.walk();
1244 for child in node.children(&mut cursor) {
1245 if child.kind() == "with_item" {
1246 self.visit_with_item(child, source);
1247 } else if child.kind() == "with_clause" {
1248 let mut inner_cursor = child.walk();
1250 for item in child.children(&mut inner_cursor) {
1251 if item.kind() == "with_item" {
1252 self.visit_with_item(item, source);
1253 }
1254 }
1255 }
1256 }
1257 let mut cursor = node.walk();
1259 for child in node.children(&mut cursor) {
1260 self.visit_node(child, source, true);
1261 }
1262 }
1263 "assignment" => {
1264 self.check_assignment(node, source, in_with);
1265 }
1266 _ => {
1267 let mut cursor = node.walk();
1269 for child in node.children(&mut cursor) {
1270 self.visit_node(child, source, in_with);
1271 }
1272 }
1273 }
1274 }
1275
1276 fn visit_with_item(&mut self, node: Node, source: &[u8]) {
1277 let mut cursor = node.walk();
1290 for child in node.children(&mut cursor) {
1291 if child.kind() == "as_pattern" {
1292 let mut as_cursor = child.walk();
1293 let mut call_node: Option<Node> = None;
1294 let mut target_node: Option<Node> = None;
1295
1296 for as_child in child.children(&mut as_cursor) {
1297 if as_child.kind() == "call" {
1298 call_node = Some(as_child);
1299 } else if as_child.kind() == "as_pattern_target" {
1300 if let Some(ident) = as_child.child(0) {
1302 if ident.kind() == "identifier" {
1303 target_node = Some(ident);
1304 }
1305 }
1306 }
1307 }
1308
1309 if let (Some(call), Some(target)) = (call_node, target_node) {
1310 let var_name = node_text(target, source).to_string();
1311 self.context_manager_vars.insert(var_name.clone());
1312
1313 if let Some(resource_type) = self.get_resource_type_from_call(call, source) {
1314 self.resources.push(DetectedResource {
1315 name: var_name,
1316 resource_type,
1317 line: node.start_position().row as u32 + 1,
1318 in_context_manager: true,
1319 });
1320 }
1321 }
1322 }
1323 }
1324
1325 if let Some(target) = node.child_by_field_name("alias") {
1327 let var_name = node_text(target, source).to_string();
1328 if !self.context_manager_vars.contains(&var_name) {
1329 self.context_manager_vars.insert(var_name.clone());
1330
1331 if let Some(value) = node.child_by_field_name("value") {
1332 if let Some(resource_type) = self.get_resource_type_from_call(value, source) {
1333 self.resources.push(DetectedResource {
1334 name: var_name,
1335 resource_type,
1336 line: node.start_position().row as u32 + 1,
1337 in_context_manager: true,
1338 });
1339 }
1340 }
1341 }
1342 }
1343 }
1344
1345 fn check_assignment(&mut self, node: Node, source: &[u8], in_with: bool) {
1346 if let Some(left) = node.child_by_field_name("left") {
1348 if let Some(right) = node.child_by_field_name("right") {
1349 let var_name = node_text(left, source).to_string();
1350
1351 if let Some(resource_type) = self.get_resource_type_from_call(right, source) {
1352 let in_context = in_with || self.context_manager_vars.contains(&var_name);
1353 self.resources.push(DetectedResource {
1354 name: var_name,
1355 resource_type,
1356 line: node.start_position().row as u32 + 1,
1357 in_context_manager: in_context,
1358 });
1359 }
1360 }
1361 }
1362 }
1363
1364 fn get_resource_type_from_call(&self, node: Node, source: &[u8]) -> Option<String> {
1365 if node.kind() != "call" {
1366 return None;
1367 }
1368
1369 let func = node.child_by_field_name("function")?;
1371 let func_text = node_text(func, source);
1372
1373 let func_name = func_text.split('.').next_back().unwrap_or(func_text);
1375
1376 for &creator in RESOURCE_CREATORS {
1378 if func_name == creator {
1379 for &(name, rtype) in RESOURCE_TYPE_MAP {
1381 if func_name == name {
1382 return Some(rtype.to_string());
1383 }
1384 }
1385 return Some(func_name.to_string());
1387 }
1388 }
1389
1390 None
1391 }
1392
1393 fn visit_node_multilang(
1398 &mut self,
1399 node: Node,
1400 source: &[u8],
1401 in_cleanup: bool,
1402 patterns: &LangResourcePatterns,
1403 ) {
1404 let kind = node.kind();
1405
1406 if patterns.cleanup_block_kinds.contains(&kind) {
1408 match self.lang {
1409 Language::Python => {
1410 let mut cursor = node.walk();
1412 for child in node.children(&mut cursor) {
1413 if child.kind() == "with_item" {
1414 self.visit_with_item(child, source);
1415 } else if child.kind() == "with_clause" {
1416 let mut inner_cursor = child.walk();
1417 for item in child.children(&mut inner_cursor) {
1418 if item.kind() == "with_item" {
1419 self.visit_with_item(item, source);
1420 }
1421 }
1422 }
1423 }
1424 let mut cursor = node.walk();
1426 for child in node.children(&mut cursor) {
1427 self.visit_node_multilang(child, source, true, patterns);
1428 }
1429 return;
1430 }
1431 Language::Go => {
1432 let mut cursor = node.walk();
1435 for child in node.children(&mut cursor) {
1436 self.visit_node_multilang(child, source, true, patterns);
1437 }
1438 return;
1439 }
1440 Language::CSharp => {
1441 let mut cursor = node.walk();
1443 for child in node.children(&mut cursor) {
1444 self.visit_node_multilang(child, source, true, patterns);
1445 }
1446 return;
1447 }
1448 Language::Java => {
1449 let mut cursor = node.walk();
1451 for child in node.children(&mut cursor) {
1452 self.visit_node_multilang(child, source, true, patterns);
1453 }
1454 return;
1455 }
1456 _ => {}
1457 }
1458 }
1459
1460 if patterns.assignment_kinds.contains(&kind) {
1462 self.check_assignment_multilang(node, source, in_cleanup, patterns);
1463 }
1464
1465 let mut cursor = node.walk();
1467 for child in node.children(&mut cursor) {
1468 self.visit_node_multilang(child, source, in_cleanup, patterns);
1469 }
1470 }
1471
1472 fn check_assignment_multilang(
1473 &mut self,
1474 node: Node,
1475 source: &[u8],
1476 in_cleanup: bool,
1477 patterns: &LangResourcePatterns,
1478 ) {
1479 match self.lang {
1480 Language::Python => {
1481 if let Some(left) = node.child_by_field_name("left") {
1483 if let Some(right) = node.child_by_field_name("right") {
1484 let var_name = node_text(left, source).to_string();
1485 if let Some(resource_type) =
1486 self.get_resource_type_from_call_multilang(right, source, patterns)
1487 {
1488 let in_context =
1489 in_cleanup || self.context_manager_vars.contains(&var_name);
1490 self.resources.push(DetectedResource {
1491 name: var_name,
1492 resource_type,
1493 line: node.start_position().row as u32 + 1,
1494 in_context_manager: in_context,
1495 });
1496 }
1497 }
1498 }
1499 }
1500 Language::Go => {
1501 if let Some(left) = node.child_by_field_name("left") {
1505 if let Some(right) = node.child_by_field_name("right") {
1506 let var_name = if left.kind() == "expression_list" {
1508 left.child(0).map(|c| node_text(c, source).to_string())
1510 } else {
1511 Some(node_text(left, source).to_string())
1512 };
1513 if let Some(var_name) = var_name {
1514 if var_name != "_" && var_name != "err" {
1515 let call_node = if right.kind() == "expression_list" {
1517 right.child(0)
1518 } else {
1519 Some(right)
1520 };
1521 if let Some(call_node) = call_node {
1522 if let Some(resource_type) = self
1523 .get_resource_type_from_call_multilang(
1524 call_node, source, patterns,
1525 )
1526 {
1527 self.resources.push(DetectedResource {
1528 name: var_name,
1529 resource_type,
1530 line: node.start_position().row as u32 + 1,
1531 in_context_manager: in_cleanup,
1532 });
1533 }
1534 }
1535 }
1536 }
1537 }
1538 }
1539 }
1540 Language::Rust => {
1541 if let Some(pattern) = node.child_by_field_name("pattern") {
1544 if let Some(value) = node.child_by_field_name("value") {
1545 let var_name = node_text(pattern, source).to_string();
1546 if let Some(resource_type) =
1549 self.get_resource_type_from_call_multilang(value, source, patterns)
1550 {
1551 self.resources.push(DetectedResource {
1552 name: var_name,
1553 resource_type,
1554 line: node.start_position().row as u32 + 1,
1555 in_context_manager: true, });
1557 }
1558 }
1559 }
1560 }
1561 Language::Java | Language::CSharp => {
1562 let mut cursor = node.walk();
1565 for child in node.children(&mut cursor) {
1566 if child.kind() == "variable_declarator" {
1567 if let Some(name_node) = child.child_by_field_name("name") {
1568 if let Some(value) = child.child_by_field_name("value") {
1569 let var_name = node_text(name_node, source).to_string();
1570 if let Some(resource_type) = self
1571 .get_resource_type_from_call_multilang(value, source, patterns)
1572 {
1573 self.resources.push(DetectedResource {
1574 name: var_name,
1575 resource_type,
1576 line: node.start_position().row as u32 + 1,
1577 in_context_manager: in_cleanup,
1578 });
1579 }
1580 }
1581 }
1582 }
1583 }
1584 }
1585 Language::TypeScript | Language::JavaScript => {
1586 let mut cursor = node.walk();
1589 for child in node.children(&mut cursor) {
1590 if child.kind() == "variable_declarator" {
1591 if let Some(name_node) = child.child_by_field_name("name") {
1592 if let Some(value) = child.child_by_field_name("value") {
1593 let var_name = node_text(name_node, source).to_string();
1594 if let Some(resource_type) = self
1595 .get_resource_type_from_call_multilang(value, source, patterns)
1596 {
1597 self.resources.push(DetectedResource {
1598 name: var_name,
1599 resource_type,
1600 line: node.start_position().row as u32 + 1,
1601 in_context_manager: in_cleanup,
1602 });
1603 }
1604 }
1605 }
1606 }
1607 }
1608 if node.kind() == "assignment_expression" {
1610 if let Some(left) = node.child_by_field_name("left") {
1611 if let Some(right) = node.child_by_field_name("right") {
1612 let var_name = node_text(left, source).to_string();
1613 if let Some(resource_type) =
1614 self.get_resource_type_from_call_multilang(right, source, patterns)
1615 {
1616 self.resources.push(DetectedResource {
1617 name: var_name,
1618 resource_type,
1619 line: node.start_position().row as u32 + 1,
1620 in_context_manager: in_cleanup,
1621 });
1622 }
1623 }
1624 }
1625 }
1626 }
1627 Language::C | Language::Cpp => {
1628 let mut cursor = node.walk();
1631 for child in node.children(&mut cursor) {
1632 if child.kind() == "init_declarator" {
1633 if let Some(declarator) = child.child_by_field_name("declarator") {
1634 if let Some(value) = child.child_by_field_name("value") {
1635 let var_name = extract_c_declarator_name(declarator, source);
1637 if let Some(var_name) = var_name {
1638 if let Some(resource_type) = self
1639 .get_resource_type_from_call_multilang(
1640 value, source, patterns,
1641 )
1642 {
1643 self.resources.push(DetectedResource {
1644 name: var_name,
1645 resource_type,
1646 line: node.start_position().row as u32 + 1,
1647 in_context_manager: in_cleanup,
1648 });
1649 }
1650 }
1651 }
1652 }
1653 }
1654 }
1655 if node.kind() == "assignment_expression" {
1657 if let Some(left) = node.child_by_field_name("left") {
1658 if let Some(right) = node.child_by_field_name("right") {
1659 let var_name = node_text(left, source).to_string();
1660 if let Some(resource_type) =
1661 self.get_resource_type_from_call_multilang(right, source, patterns)
1662 {
1663 self.resources.push(DetectedResource {
1664 name: var_name,
1665 resource_type,
1666 line: node.start_position().row as u32 + 1,
1667 in_context_manager: in_cleanup,
1668 });
1669 }
1670 }
1671 }
1672 }
1673 }
1674 Language::Kotlin => {
1675 if node.kind() == "property_declaration" {
1679 let mut cursor = node.walk();
1680 for child in node.children(&mut cursor) {
1681 if child.kind() == "variable_declaration" {
1682 if let Some(name_node) =
1683 child.child_by_field_name("name").or_else(|| child.child(0))
1684 {
1685 let var_name = node_text(name_node, source).to_string();
1686 let mut inner_cursor = node.walk();
1690 for sibling in node.children(&mut inner_cursor) {
1691 if let Some(resource_type) = self
1692 .get_resource_type_from_call_multilang(
1693 sibling, source, patterns,
1694 )
1695 {
1696 self.resources.push(DetectedResource {
1697 name: var_name.clone(),
1698 resource_type,
1699 line: node.start_position().row as u32 + 1,
1700 in_context_manager: in_cleanup,
1701 });
1702 break;
1703 }
1704 }
1705 }
1706 }
1707 }
1708 } else if node.kind() == "assignment" {
1709 if let Some(left) = node.child_by_field_name("left").or_else(|| node.child(0)) {
1710 if let Some(right) = node.child_by_field_name("right") {
1711 let var_name = node_text(left, source).to_string();
1712 if let Some(resource_type) =
1713 self.get_resource_type_from_call_multilang(right, source, patterns)
1714 {
1715 self.resources.push(DetectedResource {
1716 name: var_name,
1717 resource_type,
1718 line: node.start_position().row as u32 + 1,
1719 in_context_manager: in_cleanup,
1720 });
1721 }
1722 }
1723 }
1724 }
1725 }
1726 Language::Swift => {
1727 if node.kind() == "property_declaration"
1730 || node.kind() == "directly_assignable_expression"
1731 {
1732 if let Some(pattern) = node
1733 .child_by_field_name("pattern")
1734 .or_else(|| node.child_by_field_name("name"))
1735 {
1736 let var_name = node_text(pattern, source).to_string();
1737 let mut cursor = node.walk();
1739 for child in node.children(&mut cursor) {
1740 if let Some(resource_type) =
1741 self.get_resource_type_from_call_multilang(child, source, patterns)
1742 {
1743 self.resources.push(DetectedResource {
1744 name: var_name.clone(),
1745 resource_type,
1746 line: node.start_position().row as u32 + 1,
1747 in_context_manager: in_cleanup,
1748 });
1749 break;
1750 }
1751 }
1752 }
1753 }
1754 }
1755 Language::Ocaml => {
1756 if node.kind() == "let_binding" {
1759 if let Some(pattern) = node.child_by_field_name("pattern") {
1760 let var_name = node_text(pattern, source).to_string();
1761 if let Some(body) = node.child_by_field_name("body") {
1763 if let Some(resource_type) =
1764 self.get_resource_type_from_call_multilang(body, source, patterns)
1765 {
1766 self.resources.push(DetectedResource {
1767 name: var_name,
1768 resource_type,
1769 line: node.start_position().row as u32 + 1,
1770 in_context_manager: in_cleanup,
1771 });
1772 }
1773 }
1774 }
1775 }
1776 }
1777 Language::Lua | Language::Luau => {
1778 if let Some(right) = node
1782 .child_by_field_name("values")
1783 .or_else(|| node.child_by_field_name("right"))
1784 {
1785 if let Some(left) = node
1786 .child_by_field_name("variables")
1787 .or_else(|| node.child_by_field_name("left"))
1788 .or_else(|| node.child_by_field_name("name"))
1789 {
1790 let var_name =
1792 if left.kind() == "variable_list" || left.kind() == "identifier_list" {
1793 left.child(0).map(|c| node_text(c, source).to_string())
1794 } else {
1795 Some(node_text(left, source).to_string())
1796 };
1797 if let Some(var_name) = var_name {
1798 let call_node = if right.kind() == "expression_list" {
1800 right.child(0)
1801 } else {
1802 Some(right)
1803 };
1804 if let Some(call_node) = call_node {
1805 if let Some(resource_type) = self
1806 .get_resource_type_from_call_multilang(
1807 call_node, source, patterns,
1808 )
1809 {
1810 self.resources.push(DetectedResource {
1811 name: var_name,
1812 resource_type,
1813 line: node.start_position().row as u32 + 1,
1814 in_context_manager: in_cleanup,
1815 });
1816 }
1817 }
1818 }
1819 }
1820 }
1821 }
1822 _ => {
1823 if let Some(left) = node.child_by_field_name("left") {
1825 if let Some(right) = node.child_by_field_name("right") {
1826 let var_name = node_text(left, source).to_string();
1827 if let Some(resource_type) =
1828 self.get_resource_type_from_call_multilang(right, source, patterns)
1829 {
1830 self.resources.push(DetectedResource {
1831 name: var_name,
1832 resource_type,
1833 line: node.start_position().row as u32 + 1,
1834 in_context_manager: in_cleanup,
1835 });
1836 }
1837 }
1838 }
1839 }
1840 }
1841 }
1842
1843 fn get_resource_type_from_call_multilang(
1845 &self,
1846 node: Node,
1847 source: &[u8],
1848 patterns: &LangResourcePatterns,
1849 ) -> Option<String> {
1850 let func_name = extract_call_name(node, source)?;
1852
1853 for &(creator, rtype) in patterns.creators {
1855 if func_name == creator
1856 || func_name.ends_with(&format!("::{}", creator))
1857 || func_name.ends_with(&format!(".{}", creator))
1858 {
1859 return Some(rtype.to_string());
1860 }
1861 }
1862
1863 if matches!(self.lang, Language::C | Language::Cpp) {
1865 if node.kind() == "call_expression" {
1866 let text = node_text(node, source);
1867 for &(creator, rtype) in patterns.creators {
1868 if text.starts_with(creator) {
1869 return Some(rtype.to_string());
1870 }
1871 }
1872 }
1873 if node.kind() == "new_expression" {
1875 return Some("heap_object".to_string());
1876 }
1877 }
1878
1879 if matches!(self.lang, Language::Kotlin) {
1881 let text = node_text(node, source);
1883 for &(creator, rtype) in patterns.creators {
1884 if text.starts_with(creator) {
1885 return Some(rtype.to_string());
1886 }
1887 }
1888 }
1889
1890 if matches!(self.lang, Language::Swift) {
1892 let text = node_text(node, source);
1893 for &(creator, rtype) in patterns.creators {
1894 if text.starts_with(creator) {
1895 return Some(rtype.to_string());
1896 }
1897 }
1898 if node.kind() == "force_unwrap_expression" || node.kind() == "try_expression" {
1900 if let Some(child) = node.child(0) {
1901 return self.get_resource_type_from_call_multilang(child, source, patterns);
1902 }
1903 }
1904 }
1905
1906 if matches!(self.lang, Language::Ocaml) {
1908 if node.kind() == "application" {
1910 if let Some(func_node) = node
1911 .child_by_field_name("function")
1912 .or_else(|| node.child(0))
1913 {
1914 let func_text = node_text(func_node, source);
1915 for &(creator, rtype) in patterns.creators {
1916 if func_text == creator || func_text.ends_with(&format!(".{}", creator)) {
1917 return Some(rtype.to_string());
1918 }
1919 }
1920 }
1921 }
1922 let text = node_text(node, source);
1924 let first_word = text.split_whitespace().next().unwrap_or("");
1925 for &(creator, rtype) in patterns.creators {
1926 if first_word == creator {
1927 return Some(rtype.to_string());
1928 }
1929 }
1930 }
1931
1932 if matches!(self.lang, Language::Lua | Language::Luau) {
1934 let text = node_text(node, source);
1935 for &(creator, rtype) in patterns.creators {
1936 if text.starts_with(creator) {
1937 return Some(rtype.to_string());
1938 }
1939 }
1940 }
1941
1942 if matches!(self.lang, Language::Java | Language::CSharp)
1944 && node.kind() == "object_creation_expression"
1945 {
1946 if let Some(type_node) = node.child_by_field_name("type") {
1948 let type_name = node_text(type_node, source);
1949 for &(creator, rtype) in patterns.creators {
1950 if type_name == creator || type_name.contains(creator) {
1951 return Some(rtype.to_string());
1952 }
1953 }
1954 }
1955 }
1956
1957 None
1958 }
1959}
1960
1961impl Default for ResourceDetector {
1962 fn default() -> Self {
1963 Self::new()
1964 }
1965}
1966
1967pub struct LeakDetector {
1973 max_paths: usize,
1975 paths_enumerated: usize,
1977 hit_limit: bool,
1979}
1980
1981impl LeakDetector {
1982 pub fn new() -> Self {
1983 Self {
1984 max_paths: MAX_PATHS,
1985 paths_enumerated: 0,
1986 hit_limit: false,
1987 }
1988 }
1989
1990 pub fn detect(
1992 &mut self,
1993 cfg: &SimpleCfg,
1994 resources: &[ResourceInfo],
1995 source: &[u8],
1996 show_paths: bool,
1997 ) -> Vec<LeakInfo> {
1998 let mut leaks = Vec::new();
1999 self.paths_enumerated = 0;
2000 self.hit_limit = false;
2001
2002 for resource in resources {
2003 if resource.closed {
2005 continue;
2006 }
2007
2008 let paths = self.enumerate_paths(cfg, resource, source);
2010
2011 for path in &paths {
2013 if !self.path_has_close(path, &resource.name) {
2014 leaks.push(LeakInfo {
2015 resource: resource.name.clone(),
2016 line: resource.line,
2017 paths: if show_paths {
2018 Some(vec![self.format_path(path)])
2019 } else {
2020 None
2021 },
2022 });
2023 break; }
2025 }
2026 }
2027
2028 leaks
2029 }
2030
2031 pub fn detect_multilang(
2034 &mut self,
2035 cfg: &SimpleCfg,
2036 resources: &[ResourceInfo],
2037 source: &[u8],
2038 show_paths: bool,
2039 ) -> Vec<LeakInfo> {
2040 self.detect(cfg, resources, source, show_paths)
2041 }
2042
2043 fn enumerate_paths(
2045 &mut self,
2046 cfg: &SimpleCfg,
2047 resource: &ResourceInfo,
2048 _source: &[u8],
2049 ) -> Vec<Vec<usize>> {
2050 let mut paths = Vec::new();
2051
2052 let start_block = self.find_block_with_line(cfg, resource.line);
2054 if start_block.is_none() {
2055 return paths;
2056 }
2057 let start = start_block.unwrap();
2058
2059 for &exit_id in &cfg.exit_blocks {
2061 if self.hit_limit {
2062 break;
2063 }
2064 self.find_paths_dfs(cfg, start, exit_id, &mut Vec::new(), &mut paths);
2065 }
2066
2067 paths
2068 }
2069
2070 fn find_block_with_line(&self, cfg: &SimpleCfg, line: u32) -> Option<usize> {
2071 for (id, block) in &cfg.blocks {
2072 if block.lines.contains(&line) {
2073 return Some(*id);
2074 }
2075 }
2076 Some(cfg.entry_block)
2078 }
2079
2080 fn find_paths_dfs(
2081 &mut self,
2082 cfg: &SimpleCfg,
2083 current: usize,
2084 target: usize,
2085 current_path: &mut Vec<usize>,
2086 paths: &mut Vec<Vec<usize>>,
2087 ) {
2088 if self.paths_enumerated >= self.max_paths {
2090 self.hit_limit = true;
2091 return;
2092 }
2093
2094 if current_path.contains(¤t) {
2096 return;
2097 }
2098
2099 current_path.push(current);
2100
2101 if current == target {
2102 paths.push(current_path.clone());
2103 self.paths_enumerated += 1;
2104 } else if let Some(block) = cfg.blocks.get(¤t) {
2105 for &succ in &block.succs {
2106 self.find_paths_dfs(cfg, succ, target, current_path, paths);
2107 if self.hit_limit {
2108 break;
2109 }
2110 }
2111 }
2112
2113 current_path.pop();
2114 }
2115
2116 fn path_has_close(&self, path: &[usize], resource_name: &str) -> bool {
2117 let _ = (path, resource_name);
2122 false
2123 }
2124
2125 fn format_path(&self, path: &[usize]) -> String {
2126 path.iter()
2127 .map(|id| id.to_string())
2128 .collect::<Vec<_>>()
2129 .join(" -> ")
2130 }
2131}
2132
2133impl Default for LeakDetector {
2134 fn default() -> Self {
2135 Self::new()
2136 }
2137}
2138
2139pub struct DoubleCloseDetector {
2145 lang: Language,
2146}
2147
2148impl DoubleCloseDetector {
2149 pub fn new() -> Self {
2150 Self {
2151 lang: Language::Python,
2152 }
2153 }
2154
2155 pub fn with_language(lang: Language) -> Self {
2156 Self { lang }
2157 }
2158
2159 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2161 let mut issues = Vec::new();
2162 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2163
2164 self.find_closes(func_node, source, &mut close_sites);
2165
2166 for (resource, lines) in close_sites {
2167 if lines.len() > 1 {
2168 issues.push(DoubleCloseInfo {
2169 resource,
2170 first_close: lines[0],
2171 second_close: lines[1],
2172 });
2173 }
2174 }
2175
2176 issues
2177 }
2178
2179 pub fn detect_multilang(
2181 &self,
2182 func_node: Node,
2183 source: &[u8],
2184 ) -> Vec<DoubleCloseInfo> {
2185 let mut issues = Vec::new();
2186 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2187 let patterns = get_resource_patterns(self.lang);
2188
2189 self.find_closes_multilang(func_node, source, &mut close_sites, &patterns);
2190
2191 for (resource, lines) in close_sites {
2192 if lines.len() > 1 {
2193 issues.push(DoubleCloseInfo {
2194 resource,
2195 first_close: lines[0],
2196 second_close: lines[1],
2197 });
2198 }
2199 }
2200
2201 issues
2202 }
2203
2204 fn find_closes(&self, node: Node, source: &[u8], closes: &mut HashMap<String, Vec<u32>>) {
2205 if node.kind() == "call" {
2206 if let Some(func) = node.child_by_field_name("function") {
2207 if func.kind() == "attribute" {
2208 if let Some(attr) = func.child_by_field_name("attribute") {
2209 let method = node_text(attr, source);
2210 if RESOURCE_CLOSERS.contains(&method) {
2211 if let Some(obj) = func.child_by_field_name("object") {
2212 let var_name = node_text(obj, source).to_string();
2213 let line = node.start_position().row as u32 + 1;
2214 closes.entry(var_name).or_default().push(line);
2215 }
2216 }
2217 }
2218 }
2219 }
2220 }
2221
2222 let mut cursor = node.walk();
2223 for child in node.children(&mut cursor) {
2224 self.find_closes(child, source, closes);
2225 }
2226 }
2227
2228 fn find_closes_multilang(
2229 &self,
2230 node: Node,
2231 source: &[u8],
2232 closes: &mut HashMap<String, Vec<u32>>,
2233 patterns: &LangResourcePatterns,
2234 ) {
2235 let kind = node.kind();
2236 if kind == "call"
2238 || kind == "call_expression"
2239 || kind == "method_invocation"
2240 || kind == "invocation_expression"
2241 {
2242 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2243 if patterns.closers.contains(&method.as_str()) {
2244 let line = node.start_position().row as u32 + 1;
2245 closes.entry(var_name).or_default().push(line);
2246 }
2247 }
2248 }
2249
2250 let mut cursor = node.walk();
2251 for child in node.children(&mut cursor) {
2252 self.find_closes_multilang(child, source, closes, patterns);
2253 }
2254 }
2255}
2256
2257impl Default for DoubleCloseDetector {
2258 fn default() -> Self {
2259 Self::new()
2260 }
2261}
2262
2263pub struct UseAfterCloseDetector {
2269 lang: Language,
2270}
2271
2272impl UseAfterCloseDetector {
2273 pub fn new() -> Self {
2274 Self {
2275 lang: Language::Python,
2276 }
2277 }
2278
2279 pub fn with_language(lang: Language) -> Self {
2280 Self { lang }
2281 }
2282
2283 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2285 let mut issues = Vec::new();
2286 let mut close_lines: HashMap<String, u32> = HashMap::new();
2287 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2288
2289 self.analyze(func_node, source, &mut close_lines, &mut uses_after_close);
2290
2291 for (resource, close_line, use_line) in uses_after_close {
2292 issues.push(UseAfterCloseInfo {
2293 resource,
2294 close_line,
2295 use_line,
2296 });
2297 }
2298
2299 issues
2300 }
2301
2302 pub fn detect_multilang(
2304 &self,
2305 func_node: Node,
2306 source: &[u8],
2307 ) -> Vec<UseAfterCloseInfo> {
2308 let mut issues = Vec::new();
2309 let mut close_lines: HashMap<String, u32> = HashMap::new();
2310 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2311 let patterns = get_resource_patterns(self.lang);
2312
2313 self.analyze_multilang(
2314 func_node,
2315 source,
2316 &mut close_lines,
2317 &mut uses_after_close,
2318 &patterns,
2319 );
2320
2321 for (resource, close_line, use_line) in uses_after_close {
2322 issues.push(UseAfterCloseInfo {
2323 resource,
2324 close_line,
2325 use_line,
2326 });
2327 }
2328
2329 issues
2330 }
2331
2332 fn analyze(
2333 &self,
2334 node: Node,
2335 source: &[u8],
2336 close_lines: &mut HashMap<String, u32>,
2337 uses_after: &mut Vec<(String, u32, u32)>,
2338 ) {
2339 let line = node.start_position().row as u32 + 1;
2340
2341 if node.kind() == "call" {
2342 if let Some(func) = node.child_by_field_name("function") {
2343 if func.kind() == "attribute" {
2344 if let Some(attr) = func.child_by_field_name("attribute") {
2345 let method = node_text(attr, source);
2346 if RESOURCE_CLOSERS.contains(&method) {
2347 if let Some(obj) = func.child_by_field_name("object") {
2348 let var_name = node_text(obj, source).to_string();
2349 close_lines.insert(var_name, line);
2350 }
2351 } else if let Some(obj) = func.child_by_field_name("object") {
2352 let var_name = node_text(obj, source).to_string();
2353 if let Some(&close_line) = close_lines.get(&var_name) {
2354 if line > close_line {
2355 uses_after.push((var_name, close_line, line));
2356 }
2357 }
2358 }
2359 }
2360 }
2361 }
2362 }
2363
2364 if node.kind() == "attribute" {
2365 if let Some(obj) = node.child_by_field_name("object") {
2366 if obj.kind() == "identifier" {
2367 let var_name = node_text(obj, source).to_string();
2368 if let Some(&close_line) = close_lines.get(&var_name) {
2369 if line > close_line {
2370 uses_after.push((var_name, close_line, line));
2371 }
2372 }
2373 }
2374 }
2375 }
2376
2377 let mut cursor = node.walk();
2378 for child in node.children(&mut cursor) {
2379 self.analyze(child, source, close_lines, uses_after);
2380 }
2381 }
2382
2383 fn analyze_multilang(
2384 &self,
2385 node: Node,
2386 source: &[u8],
2387 close_lines: &mut HashMap<String, u32>,
2388 uses_after: &mut Vec<(String, u32, u32)>,
2389 patterns: &LangResourcePatterns,
2390 ) {
2391 let line = node.start_position().row as u32 + 1;
2392 let kind = node.kind();
2393
2394 if kind == "call"
2396 || kind == "call_expression"
2397 || kind == "method_invocation"
2398 || kind == "invocation_expression"
2399 {
2400 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2401 if patterns.closers.contains(&method.as_str()) {
2402 close_lines.insert(var_name, line);
2403 } else {
2404 if let Some((obj_name, _)) = extract_close_call(node, source, self.lang) {
2407 if let Some(&close_line) = close_lines.get(&obj_name) {
2408 if line > close_line {
2409 uses_after.push((obj_name, close_line, line));
2410 }
2411 }
2412 }
2413 }
2414 }
2415 }
2416
2417 if kind == "attribute"
2419 || kind == "member_expression"
2420 || kind == "field_expression"
2421 || kind == "selector_expression"
2422 {
2423 if let Some(obj) = node
2424 .child_by_field_name("object")
2425 .or_else(|| node.child_by_field_name("operand"))
2426 .or_else(|| node.child(0))
2427 {
2428 if obj.kind() == "identifier" {
2429 let var_name = node_text(obj, source).to_string();
2430 if let Some(&close_line) = close_lines.get(&var_name) {
2431 if line > close_line {
2432 uses_after.push((var_name, close_line, line));
2433 }
2434 }
2435 }
2436 }
2437 }
2438
2439 let mut cursor = node.walk();
2440 for child in node.children(&mut cursor) {
2441 self.analyze_multilang(child, source, close_lines, uses_after, patterns);
2442 }
2443 }
2444}
2445
2446impl Default for UseAfterCloseDetector {
2447 fn default() -> Self {
2448 Self::new()
2449 }
2450}
2451
2452pub fn suggest_context_manager(resources: &[ResourceInfo]) -> Vec<ContextSuggestion> {
2458 resources
2459 .iter()
2460 .filter(|r| !r.closed) .map(|r| {
2462 let suggestion = match r.resource_type.as_str() {
2463 "file" => format!("with open(...) as {}:", r.name),
2464 "connection" => format!("with connect(...) as {}:", r.name),
2465 "cursor" => format!("with connection.cursor() as {}:", r.name),
2466 "socket" => format!("with socket.socket(...) as {}:", r.name),
2467 _ => format!("with {} as {}:", r.resource_type, r.name),
2468 };
2469 ContextSuggestion {
2470 resource: r.name.clone(),
2471 suggestion,
2472 }
2473 })
2474 .collect()
2475}
2476
2477pub fn suggest_context_manager_multilang(
2479 resources: &[ResourceInfo],
2480 lang: Language,
2481) -> Vec<ContextSuggestion> {
2482 resources
2483 .iter()
2484 .filter(|r| !r.closed)
2485 .map(|r| {
2486 let suggestion = match lang {
2487 Language::Python => match r.resource_type.as_str() {
2488 "file" => format!("with open(...) as {}:", r.name),
2489 "connection" => format!("with connect(...) as {}:", r.name),
2490 "cursor" => format!("with connection.cursor() as {}:", r.name),
2491 "socket" => format!("with socket.socket(...) as {}:", r.name),
2492 _ => format!("with {} as {}:", r.resource_type, r.name),
2493 },
2494 Language::Go => format!("defer {}.Close()", r.name),
2495 Language::Rust => format!("// {}: Drop trait handles cleanup automatically. Consider wrapping in a scope block.", r.name),
2496 Language::Java => match r.resource_type.as_str() {
2497 "file_stream" | "reader" | "writer" | "scanner" | "stream" =>
2498 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2499 "connection" | "statement" =>
2500 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2501 _ => format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2502 },
2503 Language::CSharp => format!("using (var {} = ...) {{ ... }}", r.name),
2504 Language::TypeScript | Language::JavaScript =>
2505 format!("try {{ ... }} finally {{ {}.close(); }}", r.name),
2506 Language::C => match r.resource_type.as_str() {
2507 "file" => format!("// Ensure fclose({}) on all paths", r.name),
2508 "memory" => format!("// Ensure free({}) on all paths", r.name),
2509 _ => format!("// Ensure cleanup of {} on all paths", r.name),
2510 },
2511 Language::Cpp => match r.resource_type.as_str() {
2512 "heap_object" => format!("// Use std::unique_ptr or std::shared_ptr instead of raw new for {}", r.name),
2513 "memory" => format!("// Use RAII wrapper or smart pointer for {}", r.name),
2514 _ => format!("// Consider RAII wrapper for {}", r.name),
2515 },
2516 Language::Ruby => format!("File.open(...) do |{}| ... end", r.name),
2517 Language::Php => format!("// Ensure {}() cleanup in finally block", r.name),
2518 Language::Kotlin => format!("{}.use {{ {} -> ... }}", r.name, r.name),
2519 Language::Swift => format!("defer {{ {}.closeFile() }}", r.name),
2520 Language::Ocaml => format!("Fun.protect ~finally:(fun () -> close_in {}) (fun () -> ...)", r.name),
2521 Language::Lua | Language::Luau => format!("// Ensure {}:close() is called, consider pcall for cleanup", r.name),
2522 _ => format!("// Ensure {} is properly closed/released", r.name),
2523 };
2524 ContextSuggestion {
2525 resource: r.name.clone(),
2526 suggestion,
2527 }
2528 })
2529 .collect()
2530}
2531
2532pub fn generate_constraints(
2538 file: &str,
2539 function: Option<&str>,
2540 resources: &[ResourceInfo],
2541 leaks: &[LeakInfo],
2542 double_closes: &[DoubleCloseInfo],
2543 use_after_closes: &[UseAfterCloseInfo],
2544) -> Vec<ResourceConstraint> {
2545 let mut constraints = Vec::new();
2546 let context = function.unwrap_or("module").to_string();
2547
2548 for leak in leaks {
2550 constraints.push(ResourceConstraint {
2551 rule: format!(
2552 "Resource '{}' opened at line {} must be closed on all control flow paths",
2553 leak.resource, leak.line
2554 ),
2555 context: format!("{} in {}", context, file),
2556 confidence: 0.9,
2557 });
2558 }
2559
2560 for dc in double_closes {
2562 constraints.push(ResourceConstraint {
2563 rule: format!(
2564 "Resource '{}' must not be closed twice (lines {} and {})",
2565 dc.resource, dc.first_close, dc.second_close
2566 ),
2567 context: format!("{} in {}", context, file),
2568 confidence: 0.95,
2569 });
2570 }
2571
2572 for uac in use_after_closes {
2574 constraints.push(ResourceConstraint {
2575 rule: format!(
2576 "Resource '{}' must not be used at line {} after being closed at line {}",
2577 uac.resource, uac.use_line, uac.close_line
2578 ),
2579 context: format!("{} in {}", context, file),
2580 confidence: 0.95,
2581 });
2582 }
2583
2584 for resource in resources {
2586 if !resource.closed {
2587 constraints.push(ResourceConstraint {
2588 rule: format!(
2589 "Resource '{}' ({}) should use context manager pattern (with statement)",
2590 resource.name, resource.resource_type
2591 ),
2592 context: format!("{} in {}", context, file),
2593 confidence: 0.85,
2594 });
2595 }
2596 }
2597
2598 constraints
2599}
2600
2601pub fn format_resources_text(report: &ResourceReport) -> String {
2607 let mut lines = Vec::new();
2608
2609 lines.push(format!("Resource Analysis: {}", report.file));
2610 lines.push(format!("Language: {}", report.language));
2611 if let Some(ref func) = report.function {
2612 lines.push(format!("Function: {}", func));
2613 }
2614 lines.push(String::new());
2615
2616 lines.push(format!("Resources detected: {}", report.resources.len()));
2618 for r in &report.resources {
2619 let status = if r.closed { "closed" } else { "open" };
2620 lines.push(format!(
2621 " - {}: {} at line {} [{}]",
2622 r.name, r.resource_type, r.line, status
2623 ));
2624 }
2625 lines.push(String::new());
2626
2627 if !report.leaks.is_empty() {
2629 lines.push(format!("Leaks found: {}", report.leaks.len()));
2630 for leak in &report.leaks {
2631 lines.push(format!(" - {} at line {}", leak.resource, leak.line));
2632 if let Some(ref paths) = leak.paths {
2633 for path in paths {
2634 lines.push(format!(" Path: {}", path));
2635 }
2636 }
2637 }
2638 } else {
2639 lines.push("Leaks found: 0".to_string());
2640 }
2641
2642 if !report.double_closes.is_empty() {
2644 lines.push(String::new());
2645 lines.push(format!(
2646 "Double-close errors: {}",
2647 report.double_closes.len()
2648 ));
2649 for dc in &report.double_closes {
2650 lines.push(format!(
2651 " - {}: first close at {}, second close at {}",
2652 dc.resource, dc.first_close, dc.second_close
2653 ));
2654 }
2655 }
2656
2657 if !report.use_after_closes.is_empty() {
2659 lines.push(String::new());
2660 lines.push(format!(
2661 "Use-after-close errors: {}",
2662 report.use_after_closes.len()
2663 ));
2664 for uac in &report.use_after_closes {
2665 lines.push(format!(
2666 " - {}: closed at {}, used at {}",
2667 uac.resource, uac.close_line, uac.use_line
2668 ));
2669 }
2670 }
2671
2672 if !report.suggestions.is_empty() {
2674 lines.push(String::new());
2675 lines.push(format!("Suggestions: {}", report.suggestions.len()));
2676 for s in &report.suggestions {
2677 lines.push(format!(" - {}: {}", s.resource, s.suggestion));
2678 }
2679 }
2680
2681 if !report.constraints.is_empty() {
2683 lines.push(String::new());
2684 lines.push(format!("Constraints: {}", report.constraints.len()));
2685 for c in &report.constraints {
2686 lines.push(format!(" - {} (confidence: {:.2})", c.rule, c.confidence));
2687 }
2688 }
2689
2690 lines.push(String::new());
2692 lines.push("Summary:".to_string());
2693 lines.push(format!(
2694 " resources_detected: {}",
2695 report.summary.resources_detected
2696 ));
2697 lines.push(format!(" leaks_found: {}", report.summary.leaks_found));
2698 lines.push(format!(
2699 " double_closes_found: {}",
2700 report.summary.double_closes_found
2701 ));
2702 lines.push(format!(
2703 " use_after_closes_found: {}",
2704 report.summary.use_after_closes_found
2705 ));
2706 lines.push(String::new());
2707 lines.push(format!(
2708 "Analysis completed in {}ms",
2709 report.analysis_time_ms
2710 ));
2711
2712 lines.join("\n")
2713}
2714
2715fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2720 std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
2721}
2722
2723fn extract_call_name(node: Node, source: &[u8]) -> Option<String> {
2726 match node.kind() {
2728 "call" | "call_expression" | "method_invocation" | "invocation_expression" => {
2730 if let Some(func) = node
2731 .child_by_field_name("function")
2732 .or_else(|| node.child_by_field_name("name"))
2733 .or_else(|| node.child_by_field_name("method"))
2734 {
2735 let func_text = node_text(func, source);
2736 let func_name = func_text
2738 .split('.')
2739 .next_back()
2740 .unwrap_or(func_text)
2741 .rsplit("::")
2742 .next()
2743 .unwrap_or(func_text);
2744 return Some(func_name.to_string());
2745 }
2746 if let Some(first_child) = node.child(0) {
2748 let text = node_text(first_child, source);
2749 let name = text
2750 .split('.')
2751 .next_back()
2752 .unwrap_or(text)
2753 .rsplit("::")
2754 .next()
2755 .unwrap_or(text);
2756 return Some(name.to_string());
2757 }
2758 }
2759 "composite_literal" => {
2761 }
2763 _ => {}
2764 }
2765
2766 let text = node_text(node, source);
2768 if text.contains('(') {
2769 let name_part = text.split('(').next()?;
2770 let func_name = name_part
2771 .split('.')
2772 .next_back()
2773 .unwrap_or(name_part)
2774 .rsplit("::")
2775 .next()
2776 .unwrap_or(name_part)
2777 .trim();
2778 if !func_name.is_empty() {
2779 return Some(func_name.to_string());
2780 }
2781 }
2782
2783 None
2784}
2785
2786fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
2788 match declarator.kind() {
2789 "identifier" => Some(node_text(declarator, source).to_string()),
2790 "pointer_declarator" => {
2791 let mut cursor = declarator.walk();
2793 for child in declarator.children(&mut cursor) {
2794 if child.kind() == "identifier" {
2795 return Some(node_text(child, source).to_string());
2796 }
2797 if child.kind() == "pointer_declarator" {
2798 return extract_c_declarator_name(child, source);
2799 }
2800 }
2801 None
2802 }
2803 _ => Some(node_text(declarator, source).to_string()),
2804 }
2805}
2806
2807fn extract_close_call(node: Node, source: &[u8], lang: Language) -> Option<(String, String)> {
2809 match lang {
2810 Language::Python
2811 | Language::Ruby
2812 | Language::Java
2813 | Language::CSharp
2814 | Language::TypeScript
2815 | Language::JavaScript
2816 | Language::Scala
2817 | Language::Kotlin
2818 | Language::Swift => {
2819 if let Some(func) = node
2821 .child_by_field_name("function")
2822 .or_else(|| node.child_by_field_name("method"))
2823 .or_else(|| node.child_by_field_name("name"))
2824 {
2825 if func.kind() == "attribute"
2827 || func.kind() == "member_expression"
2828 || func.kind() == "selector_expression"
2829 || func.kind() == "field_access"
2830 {
2831 let obj = func.child_by_field_name("object").or_else(|| func.child(0));
2832 let attr = func
2833 .child_by_field_name("attribute")
2834 .or_else(|| func.child_by_field_name("field"))
2835 .or_else(|| func.child_by_field_name("name"));
2836
2837 if let (Some(obj), Some(attr)) = (obj, attr) {
2838 let var_name = node_text(obj, source).to_string();
2839 let method = node_text(attr, source).to_string();
2840 return Some((var_name, method));
2841 }
2842 }
2843 }
2844 None
2845 }
2846 Language::Go => {
2847 if let Some(func) = node.child_by_field_name("function") {
2849 if func.kind() == "selector_expression" {
2850 if let Some(operand) = func.child_by_field_name("operand") {
2851 if let Some(field) = func.child_by_field_name("field") {
2852 let var_name = node_text(operand, source).to_string();
2853 let method = node_text(field, source).to_string();
2854 return Some((var_name, method));
2855 }
2856 }
2857 }
2858 }
2859 None
2860 }
2861 Language::C | Language::Cpp => {
2862 if let Some(func) = node
2864 .child_by_field_name("function")
2865 .or_else(|| node.child(0))
2866 {
2867 let func_name = node_text(func, source).to_string();
2868 if let Some(args) = node.child_by_field_name("arguments") {
2870 if let Some(first_arg) = args.child(1) {
2871 let var_name = node_text(first_arg, source).to_string();
2873 return Some((var_name, func_name));
2874 }
2875 }
2876 }
2877 None
2878 }
2879 _ => {
2880 if let Some(func) = node.child_by_field_name("function") {
2882 if let Some(obj) = func.child_by_field_name("object").or_else(|| func.child(0)) {
2883 if let Some(attr) = func.child_by_field_name("attribute") {
2884 let var_name = node_text(obj, source).to_string();
2885 let method = node_text(attr, source).to_string();
2886 return Some((var_name, method));
2887 }
2888 }
2889 }
2890 None
2891 }
2892 }
2893}
2894
2895pub fn build_cfg_multilang(
2901 func_node: Node,
2902 source: &[u8],
2903 lang: Language,
2904) -> SimpleCfg {
2905 let patterns = get_resource_patterns(lang);
2906 let mut cfg = SimpleCfg::new();
2907 let entry_id = cfg.new_block();
2908 cfg.entry_block = entry_id;
2909
2910 if let Some(block) = cfg.blocks.get_mut(&entry_id) {
2911 block.is_entry = true;
2912 }
2913
2914 let body = func_node
2916 .children(&mut func_node.walk())
2917 .find(|n| patterns.body_kinds.contains(&n.kind()));
2918
2919 if let Some(body_node) = body {
2920 let exit_id = process_statements_multilang(&mut cfg, body_node, source, entry_id, &patterns);
2921 if let Some(exit) = exit_id {
2922 if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
2923 cfg.mark_exit(exit);
2924 }
2925 }
2926 } else {
2927 cfg.mark_exit(entry_id);
2929 }
2930
2931 cfg
2932}
2933
2934fn process_statements_multilang(
2935 cfg: &mut SimpleCfg,
2936 node: Node,
2937 source: &[u8],
2938 mut current: usize,
2939 patterns: &LangResourcePatterns,
2940) -> Option<usize> {
2941 let mut cursor = node.walk();
2942 for child in node.children(&mut cursor) {
2943 let kind = child.kind();
2944
2945 if patterns.return_kinds.contains(&kind) {
2946 let text = node_text(child, source).to_string();
2948 let line = child.start_position().row as u32 + 1;
2949 if let Some(block) = cfg.blocks.get_mut(¤t) {
2950 block
2951 .stmts
2952 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2953 block.lines.push(line);
2954 }
2955 cfg.mark_exit(current);
2956 return None;
2957 } else if patterns.if_kinds.contains(&kind) {
2958 current = process_if_multilang(cfg, child, source, current, patterns)?;
2960 } else if patterns.loop_kinds.contains(&kind) {
2961 current = process_loop_multilang(cfg, child, source, current, patterns)?;
2963 } else if patterns.try_kinds.contains(&kind) {
2964 current = process_try_multilang(cfg, child, source, current, patterns)?;
2966 } else if patterns.cleanup_block_kinds.contains(&kind) {
2967 current = process_cleanup_block_multilang(cfg, child, source, current, patterns)?;
2969 } else {
2970 let text = node_text(child, source).to_string();
2972 let line = child.start_position().row as u32 + 1;
2973 if let Some(block) = cfg.blocks.get_mut(¤t) {
2974 block
2975 .stmts
2976 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2977 block.lines.push(line);
2978 }
2979 }
2980 }
2981 Some(current)
2982}
2983
2984fn process_if_multilang(
2985 cfg: &mut SimpleCfg,
2986 node: Node,
2987 source: &[u8],
2988 current: usize,
2989 patterns: &LangResourcePatterns,
2990) -> Option<usize> {
2991 if let Some(cond) = node.child_by_field_name("condition") {
2993 let text = node_text(cond, source).to_string();
2994 let line = cond.start_position().row as u32 + 1;
2995 if let Some(block) = cfg.blocks.get_mut(¤t) {
2996 block.stmts.push((
2997 cond.start_byte(),
2998 cond.end_byte(),
2999 "condition".to_string(),
3000 text,
3001 ));
3002 block.lines.push(line);
3003 }
3004 }
3005
3006 let true_block = cfg.new_block();
3007 cfg.add_edge(current, true_block);
3008
3009 let mut cursor = node.walk();
3011 let consequence = node
3012 .children(&mut cursor)
3013 .find(|n| patterns.body_kinds.contains(&n.kind()));
3014 let true_exit = if let Some(body) = consequence {
3015 process_statements_multilang(cfg, body, source, true_block, patterns)
3016 } else {
3017 Some(true_block)
3018 };
3019
3020 let mut cursor = node.walk();
3022 let alternative = node
3023 .children(&mut cursor)
3024 .find(|n| n.kind() == "else_clause" || n.kind() == "elif_clause" || n.kind() == "else");
3025
3026 let false_exit = if let Some(alt) = alternative {
3027 let false_block = cfg.new_block();
3028 cfg.add_edge(current, false_block);
3029 let alt_body = alt
3030 .children(&mut alt.walk())
3031 .find(|n| patterns.body_kinds.contains(&n.kind()));
3032 if let Some(alt_body) = alt_body {
3033 process_statements_multilang(cfg, alt_body, source, false_block, patterns)
3034 } else {
3035 Some(false_block)
3036 }
3037 } else {
3038 None
3039 };
3040
3041 let merge = cfg.new_block();
3042 if let Some(te) = true_exit {
3043 cfg.add_edge(te, merge);
3044 }
3045 if let Some(fe) = false_exit {
3046 cfg.add_edge(fe, merge);
3047 }
3048 if alternative.is_none() {
3049 cfg.add_edge(current, merge);
3050 }
3051
3052 Some(merge)
3053}
3054
3055fn process_loop_multilang(
3056 cfg: &mut SimpleCfg,
3057 node: Node,
3058 source: &[u8],
3059 current: usize,
3060 patterns: &LangResourcePatterns,
3061) -> Option<usize> {
3062 let header = cfg.new_block();
3063 cfg.add_edge(current, header);
3064
3065 if let Some(cond) = node.child_by_field_name("condition") {
3066 let text = node_text(cond, source).to_string();
3067 let line = cond.start_position().row as u32 + 1;
3068 if let Some(block) = cfg.blocks.get_mut(&header) {
3069 block.stmts.push((
3070 cond.start_byte(),
3071 cond.end_byte(),
3072 "loop_condition".to_string(),
3073 text,
3074 ));
3075 block.lines.push(line);
3076 }
3077 }
3078
3079 let body_block = cfg.new_block();
3080 cfg.add_edge(header, body_block);
3081
3082 let body = node
3083 .children(&mut node.walk())
3084 .find(|n| patterns.body_kinds.contains(&n.kind()));
3085 let body_exit = if let Some(body_node) = body {
3086 process_statements_multilang(cfg, body_node, source, body_block, patterns)
3087 } else {
3088 Some(body_block)
3089 };
3090
3091 if let Some(be) = body_exit {
3092 cfg.add_edge(be, header);
3093 }
3094
3095 let exit = cfg.new_block();
3096 cfg.add_edge(header, exit);
3097 Some(exit)
3098}
3099
3100fn process_try_multilang(
3101 cfg: &mut SimpleCfg,
3102 node: Node,
3103 source: &[u8],
3104 current: usize,
3105 patterns: &LangResourcePatterns,
3106) -> Option<usize> {
3107 let try_block = cfg.new_block();
3108 cfg.add_edge(current, try_block);
3109
3110 let try_body = node
3111 .children(&mut node.walk())
3112 .find(|n| patterns.body_kinds.contains(&n.kind()));
3113 let try_exit = if let Some(body) = try_body {
3114 process_statements_multilang(cfg, body, source, try_block, patterns)
3115 } else {
3116 Some(try_block)
3117 };
3118
3119 let mut cursor = node.walk();
3120 let mut handler_exits = Vec::new();
3121 for child in node.children(&mut cursor) {
3122 let ck = child.kind();
3123 if ck == "except_clause" || ck == "catch_clause" || ck == "rescue" {
3124 let handler_block = cfg.new_block();
3125 cfg.add_edge(try_block, handler_block);
3126 if let Some(block) = cfg.blocks.get_mut(&try_block) {
3127 block.exception_handlers.push(handler_block);
3128 }
3129 let handler_body = child
3130 .children(&mut child.walk())
3131 .find(|n| patterns.body_kinds.contains(&n.kind()));
3132 if let Some(hb) = handler_body {
3133 if let Some(exit) =
3134 process_statements_multilang(cfg, hb, source, handler_block, patterns)
3135 {
3136 handler_exits.push(exit);
3137 }
3138 } else {
3139 handler_exits.push(handler_block);
3140 }
3141 }
3142 }
3143
3144 let finally_clause = node
3145 .children(&mut node.walk())
3146 .find(|n| n.kind() == "finally_clause" || n.kind() == "finally");
3147
3148 let merge = cfg.new_block();
3149 if let Some(te) = try_exit {
3150 if let Some(finally) = finally_clause {
3151 let finally_block = cfg.new_block();
3152 cfg.add_edge(te, finally_block);
3153 let finally_body = finally
3154 .children(&mut finally.walk())
3155 .find(|n| patterns.body_kinds.contains(&n.kind()));
3156 if let Some(fb) = finally_body {
3157 if let Some(exit) =
3158 process_statements_multilang(cfg, fb, source, finally_block, patterns)
3159 {
3160 cfg.add_edge(exit, merge);
3161 }
3162 } else {
3163 cfg.add_edge(finally_block, merge);
3164 }
3165 } else {
3166 cfg.add_edge(te, merge);
3167 }
3168 }
3169 for he in handler_exits {
3170 cfg.add_edge(he, merge);
3171 }
3172
3173 Some(merge)
3174}
3175
3176fn process_cleanup_block_multilang(
3177 cfg: &mut SimpleCfg,
3178 node: Node,
3179 source: &[u8],
3180 current: usize,
3181 patterns: &LangResourcePatterns,
3182) -> Option<usize> {
3183 let text = node_text(node, source).to_string();
3184 let line = node.start_position().row as u32 + 1;
3185 if let Some(block) = cfg.blocks.get_mut(¤t) {
3186 block.stmts.push((
3187 node.start_byte(),
3188 node.end_byte(),
3189 node.kind().to_string(),
3190 text,
3191 ));
3192 block.lines.push(line);
3193 }
3194
3195 let body = node
3196 .children(&mut node.walk())
3197 .find(|n| patterns.body_kinds.contains(&n.kind()));
3198 if let Some(body_node) = body {
3199 process_statements_multilang(cfg, body_node, source, current, patterns)
3200 } else {
3201 Some(current)
3202 }
3203}
3204
3205#[cfg(test)]
3206fn get_python_parser() -> PatternsResult<Parser> {
3207 get_parser_for_language(Language::Python)
3208}
3209
3210fn get_parser_for_language(lang: Language) -> PatternsResult<Parser> {
3212 let mut parser = Parser::new();
3213 let ts_lang =
3214 ParserPool::get_ts_language(lang).ok_or_else(|| PatternsError::UnsupportedLanguage {
3215 language: lang.as_str().to_string(),
3216 })?;
3217 parser
3218 .set_language(&ts_lang)
3219 .map_err(|e| PatternsError::ParseError {
3220 file: PathBuf::from("<internal>"),
3221 message: format!("Failed to set {} language: {}", lang.as_str(), e),
3222 })?;
3223 Ok(parser)
3224}
3225
3226fn get_function_name_from_node(
3230 node: Node,
3231 source: &[u8],
3232 patterns: &LangResourcePatterns,
3233) -> Option<String> {
3234 if node.kind() == "value_definition" {
3236 let mut cursor = node.walk();
3237 for child in node.children(&mut cursor) {
3238 if child.kind() == "let_binding" {
3239 if let Some(pattern) = child.child_by_field_name("pattern") {
3240 return Some(node_text(pattern, source).to_string());
3241 }
3242 }
3243 }
3244 return None;
3245 }
3246
3247 if let Some(name_node) = node.child_by_field_name(patterns.name_field) {
3249 if name_node.kind() == "function_declarator" {
3252 if let Some(inner) = name_node.child_by_field_name("declarator") {
3253 return Some(node_text(inner, source).to_string());
3254 }
3255 }
3256 if name_node.kind() == "pointer_declarator" {
3258 let mut cursor = name_node.walk();
3259 for child in name_node.children(&mut cursor) {
3260 if child.kind() == "function_declarator" {
3261 if let Some(inner) = child.child_by_field_name("declarator") {
3262 return Some(node_text(inner, source).to_string());
3263 }
3264 }
3265 }
3266 }
3267 return Some(node_text(name_node, source).to_string());
3268 }
3269 None
3270}
3271
3272#[cfg(test)]
3273fn find_function_node<'a>(
3274 tree: &'a tree_sitter::Tree,
3275 function_name: &str,
3276 source: &[u8],
3277) -> Option<Node<'a>> {
3278 let root = tree.root_node();
3279 let patterns = get_resource_patterns(Language::Python);
3281 find_function_recursive(root, function_name, source, &patterns)
3282}
3283
3284fn find_function_node_multilang<'a>(
3285 tree: &'a tree_sitter::Tree,
3286 function_name: &str,
3287 source: &[u8],
3288 lang: Language,
3289) -> Option<Node<'a>> {
3290 let root = tree.root_node();
3291 let patterns = get_resource_patterns(lang);
3292 find_function_recursive(root, function_name, source, &patterns)
3293}
3294
3295fn find_function_recursive<'a>(
3296 node: Node<'a>,
3297 function_name: &str,
3298 source: &[u8],
3299 patterns: &LangResourcePatterns,
3300) -> Option<Node<'a>> {
3301 let kind = node.kind();
3302 if patterns.function_kinds.contains(&kind) {
3303 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3304 if name == function_name {
3305 return Some(node);
3306 }
3307 }
3308 }
3309
3310 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3313 let mut decl_cursor = node.walk();
3314 for child in node.children(&mut decl_cursor) {
3315 if child.kind() == "variable_declarator" {
3316 if let Some(name_node) = child.child_by_field_name("name") {
3317 let var_name = name_node.utf8_text(source).unwrap_or("");
3318 if var_name == function_name {
3319 if let Some(value_node) = child.child_by_field_name("value") {
3320 if matches!(
3321 value_node.kind(),
3322 "arrow_function"
3323 | "function"
3324 | "function_expression"
3325 | "generator_function"
3326 ) {
3327 return Some(value_node);
3328 }
3329 }
3330 }
3331 }
3332 }
3333 }
3334 }
3335
3336 let mut cursor = node.walk();
3337 for child in node.children(&mut cursor) {
3338 if let Some(found) = find_function_recursive(child, function_name, source, patterns) {
3339 return Some(found);
3340 }
3341 }
3342
3343 None
3344}
3345
3346fn find_all_functions_multilang<'a>(
3347 tree: &'a tree_sitter::Tree,
3348 source: &[u8],
3349 lang: Language,
3350) -> Vec<(String, Node<'a>)> {
3351 let mut functions = Vec::new();
3352 let patterns = get_resource_patterns(lang);
3353 collect_functions(tree.root_node(), source, &mut functions, &patterns);
3354 functions
3355}
3356
3357fn collect_functions<'a>(
3358 node: Node<'a>,
3359 source: &[u8],
3360 functions: &mut Vec<(String, Node<'a>)>,
3361 patterns: &LangResourcePatterns,
3362) {
3363 let kind = node.kind();
3364 if patterns.function_kinds.contains(&kind) {
3365 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3366 functions.push((name, node));
3367 }
3368 }
3369
3370 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3373 let mut decl_cursor = node.walk();
3374 for child in node.children(&mut decl_cursor) {
3375 if child.kind() == "variable_declarator" {
3376 if let Some(name_node) = child.child_by_field_name("name") {
3377 if let Some(value_node) = child.child_by_field_name("value") {
3378 if matches!(
3379 value_node.kind(),
3380 "arrow_function"
3381 | "function"
3382 | "function_expression"
3383 | "generator_function"
3384 ) {
3385 let var_name = name_node.utf8_text(source).unwrap_or("").to_string();
3386 functions.push((var_name, value_node));
3387 }
3388 }
3389 }
3390 }
3391 }
3392 }
3393
3394 let mut cursor = node.walk();
3395 for child in node.children(&mut cursor) {
3396 collect_functions(child, source, functions, patterns);
3397 }
3398}
3399
3400fn analyze_function_with_lang(
3405 func_node: Node,
3406 source: &[u8],
3407 args: &ResourcesArgs,
3408 lang: Language,
3409) -> (
3410 Vec<ResourceInfo>,
3411 Vec<LeakInfo>,
3412 Vec<DoubleCloseInfo>,
3413 Vec<UseAfterCloseInfo>,
3414) {
3415 let check_leaks = args.check_leaks || args.check_all;
3416 let check_double_close = args.check_double_close || args.check_all;
3417 let check_use_after_close = args.check_use_after_close || args.check_all;
3418 let mut detector = ResourceDetector::with_language(lang);
3420 let resources = detector.detect_with_patterns(func_node, source);
3421
3422 let leaks = if check_leaks {
3424 let cfg = build_cfg_multilang(func_node, source, lang);
3425 let mut leak_detector = LeakDetector::new();
3426 leak_detector.detect_multilang(&cfg, &resources, source, args.show_paths)
3427 } else {
3428 Vec::new()
3429 };
3430
3431 let double_closes = if check_double_close {
3433 let detector = DoubleCloseDetector::with_language(lang);
3434 detector.detect_multilang(func_node, source)
3435 } else {
3436 Vec::new()
3437 };
3438
3439 let use_after_closes = if check_use_after_close {
3441 let detector = UseAfterCloseDetector::with_language(lang);
3442 detector.detect_multilang(func_node, source)
3443 } else {
3444 Vec::new()
3445 };
3446
3447 (resources, leaks, double_closes, use_after_closes)
3448}
3449
3450pub fn run(args: ResourcesArgs, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
3456 let start_time = Instant::now();
3457
3458 let path = if let Some(ref root) = args.project_root {
3460 validate_file_path_in_project(&args.file, root)?
3461 } else {
3462 validate_file_path(&args.file)?
3463 };
3464
3465 let source = read_file_safe(&path)?;
3467 let source_bytes = source.as_bytes();
3468
3469 let lang: Language = match args.lang {
3471 Some(l) => l,
3472 None => Language::from_path(&path).ok_or_else(|| {
3473 let ext = path
3474 .extension()
3475 .and_then(|e| e.to_str())
3476 .unwrap_or("unknown")
3477 .to_string();
3478 PatternsError::UnsupportedLanguage { language: ext }
3479 })?,
3480 };
3481
3482 let mut parser = get_parser_for_language(lang)?;
3484 let tree = parser
3485 .parse(&source, None)
3486 .ok_or_else(|| PatternsError::ParseError {
3487 file: path.clone(),
3488 message: format!("Failed to parse {} file", lang.as_str()),
3489 })?;
3490
3491 let mut all_resources = Vec::new();
3493 let mut all_leaks = Vec::new();
3494 let mut all_double_closes = Vec::new();
3495 let mut all_use_after_closes = Vec::new();
3496
3497 if let Some(ref func_name) = args.function {
3498 if let Some(func_node) = find_function_node_multilang(&tree, func_name, source_bytes, lang)
3500 {
3501 let (resources, leaks, double_closes, use_after_closes) =
3502 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3503 all_resources = resources;
3504 all_leaks = leaks;
3505 all_double_closes = double_closes;
3506 all_use_after_closes = use_after_closes;
3507 } else {
3508 return Err(PatternsError::FunctionNotFound {
3509 function: func_name.clone(),
3510 file: path.clone(),
3511 }
3512 .into());
3513 }
3514 } else {
3515 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3517 for (_name, func_node) in functions {
3518 let (resources, leaks, double_closes, use_after_closes) =
3519 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3520 all_resources.extend(resources);
3521 all_leaks.extend(leaks);
3522 all_double_closes.extend(double_closes);
3523 all_use_after_closes.extend(use_after_closes);
3524 }
3525 }
3526
3527 let suggestions = if args.suggest_context {
3529 suggest_context_manager_multilang(&all_resources, lang)
3530 } else {
3531 Vec::new()
3532 };
3533
3534 let constraints = if args.constraints {
3536 generate_constraints(
3537 path.to_str().unwrap_or(""),
3538 args.function.as_deref(),
3539 &all_resources,
3540 &all_leaks,
3541 &all_double_closes,
3542 &all_use_after_closes,
3543 )
3544 } else {
3545 Vec::new()
3546 };
3547
3548 let summary = ResourceSummary {
3550 resources_detected: all_resources.len() as u32,
3551 leaks_found: all_leaks.len() as u32,
3552 double_closes_found: all_double_closes.len() as u32,
3553 use_after_closes_found: all_use_after_closes.len() as u32,
3554 };
3555
3556 let elapsed_ms = start_time.elapsed().as_millis() as u64;
3557
3558 let report = ResourceReport {
3560 file: path.to_string_lossy().to_string(),
3561 language: lang.as_str().to_string(),
3562 function: args.function.clone(),
3563 resources: all_resources,
3564 leaks: all_leaks,
3565 double_closes: all_double_closes,
3566 use_after_closes: all_use_after_closes,
3567 suggestions,
3568 constraints,
3569 summary,
3570 analysis_time_ms: elapsed_ms,
3571 };
3572
3573 let use_text = matches!(global_format, GlobalOutputFormat::Text)
3575 || matches!(args.output_format, OutputFormat::Text);
3576 let output = if use_text {
3577 format_resources_text(&report)
3578 } else {
3579 serde_json::to_string_pretty(&report)?
3580 };
3581
3582 println!("{}", output);
3583
3584 let has_issues = report.summary.leaks_found > 0
3586 || report.summary.double_closes_found > 0
3587 || report.summary.use_after_closes_found > 0;
3588
3589 if has_issues {
3590 std::process::exit(3);
3591 }
3592
3593 Ok(())
3594}
3595
3596pub struct ResourceAnalysisResults {
3605 pub leaks: Vec<(String, LeakInfo)>,
3607 pub double_closes: Vec<(String, DoubleCloseInfo)>,
3609 pub use_after_closes: Vec<(String, UseAfterCloseInfo)>,
3611}
3612
3613pub fn analyze_source_for_resource_issues(
3629 source: &str,
3630 lang: Language,
3631) -> PatternsResult<ResourceAnalysisResults> {
3632 let source_bytes = source.as_bytes();
3633
3634 let mut parser = get_parser_for_language(lang)?;
3636 let tree = parser
3637 .parse(source, None)
3638 .ok_or_else(|| PatternsError::ParseError {
3639 file: PathBuf::from("<in-memory>"),
3640 message: format!("Failed to parse {} source for resource analysis", lang.as_str()),
3641 })?;
3642
3643 let args = ResourcesArgs {
3645 file: PathBuf::from("<in-memory>"),
3646 function: None,
3647 lang: Some(lang),
3648 check_leaks: true,
3649 check_double_close: true,
3650 check_use_after_close: true,
3651 check_all: true,
3652 suggest_context: false,
3653 show_paths: false,
3654 constraints: false,
3655 summary: false,
3656 output_format: OutputFormat::Json,
3657 project_root: None,
3658 };
3659
3660 let mut all_leaks = Vec::new();
3661 let mut all_double_closes = Vec::new();
3662 let mut all_use_after_closes = Vec::new();
3663
3664 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3666 for (func_name, func_node) in functions {
3667 let (_resources, leaks, double_closes, use_after_closes) =
3668 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3669
3670 for leak in leaks {
3671 all_leaks.push((func_name.clone(), leak));
3672 }
3673 for dc in double_closes {
3674 all_double_closes.push((func_name.clone(), dc));
3675 }
3676 for uac in use_after_closes {
3677 all_use_after_closes.push((func_name.clone(), uac));
3678 }
3679 }
3680
3681 Ok(ResourceAnalysisResults {
3682 leaks: all_leaks,
3683 double_closes: all_double_closes,
3684 use_after_closes: all_use_after_closes,
3685 })
3686}
3687
3688#[cfg(test)]
3693mod tests {
3694 use super::*;
3695
3696 const TEST_LEAKY_FUNCTION: &str = r#"
3697def leaky_function(path):
3698 f = open(path)
3699 if some_condition():
3700 return None
3701 content = f.read()
3702 f.close()
3703 return content
3704"#;
3705
3706 const TEST_SAFE_WITH_CONTEXT: &str = r#"
3707def safe_with_context(path):
3708 with open(path) as f:
3709 return f.read()
3710"#;
3711
3712 const TEST_DOUBLE_CLOSE: &str = r#"
3713def double_close(path):
3714 f = open(path)
3715 content = f.read()
3716 f.close()
3717 f.close()
3718 return content
3719"#;
3720
3721 const TEST_USE_AFTER_CLOSE: &str = r#"
3722def use_after_close(path):
3723 f = open(path)
3724 f.close()
3725 content = f.read()
3726 return content
3727"#;
3728
3729 #[test]
3730 fn test_resource_creators_constant() {
3731 assert!(RESOURCE_CREATORS.contains(&"open"));
3732 assert!(RESOURCE_CREATORS.contains(&"socket"));
3733 assert!(RESOURCE_CREATORS.contains(&"connect"));
3734 assert!(RESOURCE_CREATORS.contains(&"cursor"));
3735 }
3736
3737 #[test]
3738 fn test_resource_closers_constant() {
3739 assert!(RESOURCE_CLOSERS.contains(&"close"));
3740 assert!(RESOURCE_CLOSERS.contains(&"shutdown"));
3741 assert!(RESOURCE_CLOSERS.contains(&"disconnect"));
3742 }
3743
3744 #[test]
3745 fn test_max_paths_constant() {
3746 assert_eq!(MAX_PATHS, 1000);
3747 }
3748
3749 #[test]
3750 fn test_resource_detector_finds_open() {
3751 let mut parser = get_python_parser().unwrap();
3752 let tree = parser.parse(TEST_LEAKY_FUNCTION, None).unwrap();
3753 let source = TEST_LEAKY_FUNCTION.as_bytes();
3754
3755 let func_node = find_function_node(&tree, "leaky_function", source).unwrap();
3756 let mut detector = ResourceDetector::new();
3757 let resources = detector.detect(func_node, source);
3758
3759 assert_eq!(resources.len(), 1);
3760 assert_eq!(resources[0].name, "f");
3761 assert_eq!(resources[0].resource_type, "file");
3762 assert!(!resources[0].closed);
3763 }
3764
3765 #[test]
3766 fn test_resource_detector_context_manager() {
3767 let mut parser = get_python_parser().unwrap();
3768 let tree = parser.parse(TEST_SAFE_WITH_CONTEXT, None).unwrap();
3769 let source = TEST_SAFE_WITH_CONTEXT.as_bytes();
3770
3771 let func_node = find_function_node(&tree, "safe_with_context", source).unwrap();
3772 let mut detector = ResourceDetector::new();
3773 let resources = detector.detect(func_node, source);
3774
3775 assert_eq!(resources.len(), 1);
3776 assert!(
3777 resources[0].closed,
3778 "Context manager resource should be marked as closed"
3779 );
3780 }
3781
3782 #[test]
3783 fn test_double_close_detector() {
3784 let mut parser = get_python_parser().unwrap();
3785 let tree = parser.parse(TEST_DOUBLE_CLOSE, None).unwrap();
3786 let source = TEST_DOUBLE_CLOSE.as_bytes();
3787
3788 let func_node = find_function_node(&tree, "double_close", source).unwrap();
3789 let detector = DoubleCloseDetector::new();
3790 let issues = detector.detect(func_node, source);
3791
3792 assert_eq!(issues.len(), 1);
3793 assert_eq!(issues[0].resource, "f");
3794 }
3795
3796 #[test]
3797 fn test_use_after_close_detector() {
3798 let mut parser = get_python_parser().unwrap();
3799 let tree = parser.parse(TEST_USE_AFTER_CLOSE, None).unwrap();
3800 let source = TEST_USE_AFTER_CLOSE.as_bytes();
3801
3802 let func_node = find_function_node(&tree, "use_after_close", source).unwrap();
3803 let detector = UseAfterCloseDetector::new();
3804 let issues = detector.detect(func_node, source);
3805
3806 assert!(!issues.is_empty());
3807 assert_eq!(issues[0].resource, "f");
3808 }
3809
3810 #[test]
3811 fn test_suggest_context_manager() {
3812 let resources = vec![ResourceInfo {
3813 name: "f".to_string(),
3814 resource_type: "file".to_string(),
3815 line: 2,
3816 closed: false,
3817 }];
3818
3819 let suggestions = suggest_context_manager(&resources);
3820 assert_eq!(suggestions.len(), 1);
3821 assert!(suggestions[0].suggestion.contains("with open"));
3822 }
3823
3824 #[test]
3825 fn test_generate_constraints_for_leak() {
3826 let resources = vec![ResourceInfo {
3827 name: "f".to_string(),
3828 resource_type: "file".to_string(),
3829 line: 2,
3830 closed: false,
3831 }];
3832 let leaks = vec![LeakInfo {
3833 resource: "f".to_string(),
3834 line: 2,
3835 paths: None,
3836 }];
3837
3838 let constraints =
3839 generate_constraints("test.py", Some("test_func"), &resources, &leaks, &[], &[]);
3840
3841 assert!(!constraints.is_empty());
3842 assert!(constraints[0].rule.contains("must be closed"));
3843 }
3844
3845 #[test]
3846 fn test_leak_detector_path_limit() {
3847 let detector = LeakDetector::new();
3848 assert_eq!(detector.max_paths, MAX_PATHS);
3849 }
3850
3851 #[test]
3852 fn test_cfg_builder_basic() {
3853 let mut parser = get_python_parser().unwrap();
3854 let source = r#"
3855def simple():
3856 x = 1
3857 return x
3858"#;
3859 let tree = parser.parse(source, None).unwrap();
3860 let func_node = find_function_node(&tree, "simple", source.as_bytes()).unwrap();
3861 let cfg = build_cfg(func_node, source.as_bytes());
3862
3863 assert!(!cfg.blocks.is_empty());
3864 assert!(!cfg.exit_blocks.is_empty());
3865 }
3866
3867 #[test]
3868 fn test_cfg_builder_with_if() {
3869 let mut parser = get_python_parser().unwrap();
3870 let source = r#"
3871def with_if(x):
3872 if x > 0:
3873 return x
3874 return -x
3875"#;
3876 let tree = parser.parse(source, None).unwrap();
3877 let func_node = find_function_node(&tree, "with_if", source.as_bytes()).unwrap();
3878 let cfg = build_cfg(func_node, source.as_bytes());
3879
3880 assert!(cfg.blocks.len() > 1);
3882 }
3883
3884 #[test]
3885 fn test_format_resources_text() {
3886 let report = ResourceReport {
3887 file: "test.py".to_string(),
3888 language: "python".to_string(),
3889 function: Some("test".to_string()),
3890 resources: vec![ResourceInfo {
3891 name: "f".to_string(),
3892 resource_type: "file".to_string(),
3893 line: 2,
3894 closed: false,
3895 }],
3896 leaks: vec![],
3897 double_closes: vec![],
3898 use_after_closes: vec![],
3899 suggestions: vec![],
3900 constraints: vec![],
3901 summary: ResourceSummary::default(),
3902 analysis_time_ms: 10,
3903 };
3904
3905 let text = format_resources_text(&report);
3906 assert!(text.contains("Resource Analysis: test.py"));
3907 assert!(text.contains("Function: test"));
3908 assert!(text.contains("file"));
3909 }
3910
3911 #[test]
3912 fn test_find_ts_arrow_function_resources() {
3913 let ts_source = r#"
3914const getDuration = (start: Date, end: Date): number => {
3915 const conn = createConnection();
3916 const result = end.getTime() - start.getTime();
3917 conn.close();
3918 return result;
3919};
3920
3921function regularFunc(x: number): number {
3922 return x * 2;
3923}
3924"#;
3925 let tree = tldr_core::ast::parser::parse(ts_source, Language::TypeScript).unwrap();
3926 let source_bytes = ts_source.as_bytes();
3927
3928 let regular =
3930 find_function_node_multilang(&tree, "regularFunc", source_bytes, Language::TypeScript);
3931 assert!(regular.is_some(), "Should find regular TS function");
3932
3933 let arrow =
3935 find_function_node_multilang(&tree, "getDuration", source_bytes, Language::TypeScript);
3936 assert!(
3937 arrow.is_some(),
3938 "Should find TS arrow function 'getDuration'"
3939 );
3940 }
3941
3942 #[test]
3943 fn test_resources_args_lang_flag() {
3944 let args = ResourcesArgs {
3946 file: PathBuf::from("src/db.go"),
3947 function: None,
3948 lang: Some(Language::Go),
3949 check_leaks: true,
3950 check_double_close: false,
3951 check_use_after_close: false,
3952 check_all: false,
3953 suggest_context: false,
3954 show_paths: false,
3955 constraints: false,
3956 summary: false,
3957 output_format: OutputFormat::Json,
3958 project_root: None,
3959 };
3960 assert_eq!(args.lang, Some(Language::Go));
3961
3962 let args_auto = ResourcesArgs {
3964 file: PathBuf::from("src/db.py"),
3965 function: None,
3966 lang: None,
3967 check_leaks: true,
3968 check_double_close: false,
3969 check_use_after_close: false,
3970 check_all: false,
3971 suggest_context: false,
3972 show_paths: false,
3973 constraints: false,
3974 summary: false,
3975 output_format: OutputFormat::Json,
3976 project_root: None,
3977 };
3978 assert_eq!(args_auto.lang, None);
3979 }
3980}