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(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1219 let patterns = get_resource_patterns(self.lang);
1220 self.resources.clear();
1221 self.context_manager_vars.clear();
1222 self.visit_node_multilang(func_node, source, false, &patterns);
1223
1224 self.resources
1225 .iter()
1226 .map(|r| ResourceInfo {
1227 name: r.name.clone(),
1228 resource_type: r.resource_type.clone(),
1229 line: r.line,
1230 closed: r.in_context_manager,
1231 })
1232 .collect()
1233 }
1234
1235 fn visit_node(&mut self, node: Node, source: &[u8], in_with: bool) {
1236 match node.kind() {
1237 "with_statement" => {
1238 let mut cursor = node.walk();
1240 for child in node.children(&mut cursor) {
1241 if child.kind() == "with_item" {
1242 self.visit_with_item(child, source);
1243 } else if child.kind() == "with_clause" {
1244 let mut inner_cursor = child.walk();
1246 for item in child.children(&mut inner_cursor) {
1247 if item.kind() == "with_item" {
1248 self.visit_with_item(item, source);
1249 }
1250 }
1251 }
1252 }
1253 let mut cursor = node.walk();
1255 for child in node.children(&mut cursor) {
1256 self.visit_node(child, source, true);
1257 }
1258 }
1259 "assignment" => {
1260 self.check_assignment(node, source, in_with);
1261 }
1262 _ => {
1263 let mut cursor = node.walk();
1265 for child in node.children(&mut cursor) {
1266 self.visit_node(child, source, in_with);
1267 }
1268 }
1269 }
1270 }
1271
1272 fn visit_with_item(&mut self, node: Node, source: &[u8]) {
1273 let mut cursor = node.walk();
1286 for child in node.children(&mut cursor) {
1287 if child.kind() == "as_pattern" {
1288 let mut as_cursor = child.walk();
1289 let mut call_node: Option<Node> = None;
1290 let mut target_node: Option<Node> = None;
1291
1292 for as_child in child.children(&mut as_cursor) {
1293 if as_child.kind() == "call" {
1294 call_node = Some(as_child);
1295 } else if as_child.kind() == "as_pattern_target" {
1296 if let Some(ident) = as_child.child(0) {
1298 if ident.kind() == "identifier" {
1299 target_node = Some(ident);
1300 }
1301 }
1302 }
1303 }
1304
1305 if let (Some(call), Some(target)) = (call_node, target_node) {
1306 let var_name = node_text(target, source).to_string();
1307 self.context_manager_vars.insert(var_name.clone());
1308
1309 if let Some(resource_type) = self.get_resource_type_from_call(call, source) {
1310 self.resources.push(DetectedResource {
1311 name: var_name,
1312 resource_type,
1313 line: node.start_position().row as u32 + 1,
1314 in_context_manager: true,
1315 });
1316 }
1317 }
1318 }
1319 }
1320
1321 if let Some(target) = node.child_by_field_name("alias") {
1323 let var_name = node_text(target, source).to_string();
1324 if !self.context_manager_vars.contains(&var_name) {
1325 self.context_manager_vars.insert(var_name.clone());
1326
1327 if let Some(value) = node.child_by_field_name("value") {
1328 if let Some(resource_type) = self.get_resource_type_from_call(value, source) {
1329 self.resources.push(DetectedResource {
1330 name: var_name,
1331 resource_type,
1332 line: node.start_position().row as u32 + 1,
1333 in_context_manager: true,
1334 });
1335 }
1336 }
1337 }
1338 }
1339 }
1340
1341 fn check_assignment(&mut self, node: Node, source: &[u8], in_with: bool) {
1342 if let Some(left) = node.child_by_field_name("left") {
1344 if let Some(right) = node.child_by_field_name("right") {
1345 let var_name = node_text(left, source).to_string();
1346
1347 if let Some(resource_type) = self.get_resource_type_from_call(right, source) {
1348 let in_context = in_with || self.context_manager_vars.contains(&var_name);
1349 self.resources.push(DetectedResource {
1350 name: var_name,
1351 resource_type,
1352 line: node.start_position().row as u32 + 1,
1353 in_context_manager: in_context,
1354 });
1355 }
1356 }
1357 }
1358 }
1359
1360 fn get_resource_type_from_call(&self, node: Node, source: &[u8]) -> Option<String> {
1361 if node.kind() != "call" {
1362 return None;
1363 }
1364
1365 let func = node.child_by_field_name("function")?;
1367 let func_text = node_text(func, source);
1368
1369 let func_name = func_text.split('.').next_back().unwrap_or(func_text);
1371
1372 for &creator in RESOURCE_CREATORS {
1374 if func_name == creator {
1375 for &(name, rtype) in RESOURCE_TYPE_MAP {
1377 if func_name == name {
1378 return Some(rtype.to_string());
1379 }
1380 }
1381 return Some(func_name.to_string());
1383 }
1384 }
1385
1386 None
1387 }
1388
1389 fn visit_node_multilang(
1394 &mut self,
1395 node: Node,
1396 source: &[u8],
1397 in_cleanup: bool,
1398 patterns: &LangResourcePatterns,
1399 ) {
1400 let kind = node.kind();
1401
1402 if patterns.cleanup_block_kinds.contains(&kind) {
1404 match self.lang {
1405 Language::Python => {
1406 let mut cursor = node.walk();
1408 for child in node.children(&mut cursor) {
1409 if child.kind() == "with_item" {
1410 self.visit_with_item(child, source);
1411 } else if child.kind() == "with_clause" {
1412 let mut inner_cursor = child.walk();
1413 for item in child.children(&mut inner_cursor) {
1414 if item.kind() == "with_item" {
1415 self.visit_with_item(item, source);
1416 }
1417 }
1418 }
1419 }
1420 let mut cursor = node.walk();
1422 for child in node.children(&mut cursor) {
1423 self.visit_node_multilang(child, source, true, patterns);
1424 }
1425 return;
1426 }
1427 Language::Go => {
1428 let mut cursor = node.walk();
1431 for child in node.children(&mut cursor) {
1432 self.visit_node_multilang(child, source, true, patterns);
1433 }
1434 return;
1435 }
1436 Language::CSharp => {
1437 let mut cursor = node.walk();
1439 for child in node.children(&mut cursor) {
1440 self.visit_node_multilang(child, source, true, patterns);
1441 }
1442 return;
1443 }
1444 Language::Java => {
1445 let mut cursor = node.walk();
1447 for child in node.children(&mut cursor) {
1448 self.visit_node_multilang(child, source, true, patterns);
1449 }
1450 return;
1451 }
1452 _ => {}
1453 }
1454 }
1455
1456 if patterns.assignment_kinds.contains(&kind) {
1458 self.check_assignment_multilang(node, source, in_cleanup, patterns);
1459 }
1460
1461 let mut cursor = node.walk();
1463 for child in node.children(&mut cursor) {
1464 self.visit_node_multilang(child, source, in_cleanup, patterns);
1465 }
1466 }
1467
1468 fn check_assignment_multilang(
1469 &mut self,
1470 node: Node,
1471 source: &[u8],
1472 in_cleanup: bool,
1473 patterns: &LangResourcePatterns,
1474 ) {
1475 match self.lang {
1476 Language::Python => {
1477 if let Some(left) = node.child_by_field_name("left") {
1479 if let Some(right) = node.child_by_field_name("right") {
1480 let var_name = node_text(left, source).to_string();
1481 if let Some(resource_type) =
1482 self.get_resource_type_from_call_multilang(right, source, patterns)
1483 {
1484 let in_context =
1485 in_cleanup || self.context_manager_vars.contains(&var_name);
1486 self.resources.push(DetectedResource {
1487 name: var_name,
1488 resource_type,
1489 line: node.start_position().row as u32 + 1,
1490 in_context_manager: in_context,
1491 });
1492 }
1493 }
1494 }
1495 }
1496 Language::Go => {
1497 if let Some(left) = node.child_by_field_name("left") {
1501 if let Some(right) = node.child_by_field_name("right") {
1502 let var_name = if left.kind() == "expression_list" {
1504 left.child(0).map(|c| node_text(c, source).to_string())
1506 } else {
1507 Some(node_text(left, source).to_string())
1508 };
1509 if let Some(var_name) = var_name {
1510 if var_name != "_" && var_name != "err" {
1511 let call_node = if right.kind() == "expression_list" {
1513 right.child(0)
1514 } else {
1515 Some(right)
1516 };
1517 if let Some(call_node) = call_node {
1518 if let Some(resource_type) = self
1519 .get_resource_type_from_call_multilang(
1520 call_node, source, patterns,
1521 )
1522 {
1523 self.resources.push(DetectedResource {
1524 name: var_name,
1525 resource_type,
1526 line: node.start_position().row as u32 + 1,
1527 in_context_manager: in_cleanup,
1528 });
1529 }
1530 }
1531 }
1532 }
1533 }
1534 }
1535 }
1536 Language::Rust => {
1537 if let Some(pattern) = node.child_by_field_name("pattern") {
1540 if let Some(value) = node.child_by_field_name("value") {
1541 let var_name = node_text(pattern, source).to_string();
1542 if let Some(resource_type) =
1545 self.get_resource_type_from_call_multilang(value, source, patterns)
1546 {
1547 self.resources.push(DetectedResource {
1548 name: var_name,
1549 resource_type,
1550 line: node.start_position().row as u32 + 1,
1551 in_context_manager: true, });
1553 }
1554 }
1555 }
1556 }
1557 Language::Java | Language::CSharp => {
1558 let mut cursor = node.walk();
1561 for child in node.children(&mut cursor) {
1562 if child.kind() == "variable_declarator" {
1563 if let Some(name_node) = child.child_by_field_name("name") {
1564 if let Some(value) = child.child_by_field_name("value") {
1565 let var_name = node_text(name_node, source).to_string();
1566 if let Some(resource_type) = self
1567 .get_resource_type_from_call_multilang(value, source, patterns)
1568 {
1569 self.resources.push(DetectedResource {
1570 name: var_name,
1571 resource_type,
1572 line: node.start_position().row as u32 + 1,
1573 in_context_manager: in_cleanup,
1574 });
1575 }
1576 }
1577 }
1578 }
1579 }
1580 }
1581 Language::TypeScript | Language::JavaScript => {
1582 let mut cursor = node.walk();
1585 for child in node.children(&mut cursor) {
1586 if child.kind() == "variable_declarator" {
1587 if let Some(name_node) = child.child_by_field_name("name") {
1588 if let Some(value) = child.child_by_field_name("value") {
1589 let var_name = node_text(name_node, source).to_string();
1590 if let Some(resource_type) = self
1591 .get_resource_type_from_call_multilang(value, source, patterns)
1592 {
1593 self.resources.push(DetectedResource {
1594 name: var_name,
1595 resource_type,
1596 line: node.start_position().row as u32 + 1,
1597 in_context_manager: in_cleanup,
1598 });
1599 }
1600 }
1601 }
1602 }
1603 }
1604 if node.kind() == "assignment_expression" {
1606 if let Some(left) = node.child_by_field_name("left") {
1607 if let Some(right) = node.child_by_field_name("right") {
1608 let var_name = node_text(left, source).to_string();
1609 if let Some(resource_type) =
1610 self.get_resource_type_from_call_multilang(right, source, patterns)
1611 {
1612 self.resources.push(DetectedResource {
1613 name: var_name,
1614 resource_type,
1615 line: node.start_position().row as u32 + 1,
1616 in_context_manager: in_cleanup,
1617 });
1618 }
1619 }
1620 }
1621 }
1622 }
1623 Language::C | Language::Cpp => {
1624 let mut cursor = node.walk();
1627 for child in node.children(&mut cursor) {
1628 if child.kind() == "init_declarator" {
1629 if let Some(declarator) = child.child_by_field_name("declarator") {
1630 if let Some(value) = child.child_by_field_name("value") {
1631 let var_name = extract_c_declarator_name(declarator, source);
1633 if let Some(var_name) = var_name {
1634 if let Some(resource_type) = self
1635 .get_resource_type_from_call_multilang(
1636 value, source, patterns,
1637 )
1638 {
1639 self.resources.push(DetectedResource {
1640 name: var_name,
1641 resource_type,
1642 line: node.start_position().row as u32 + 1,
1643 in_context_manager: in_cleanup,
1644 });
1645 }
1646 }
1647 }
1648 }
1649 }
1650 }
1651 if node.kind() == "assignment_expression" {
1653 if let Some(left) = node.child_by_field_name("left") {
1654 if let Some(right) = node.child_by_field_name("right") {
1655 let var_name = node_text(left, source).to_string();
1656 if let Some(resource_type) =
1657 self.get_resource_type_from_call_multilang(right, source, patterns)
1658 {
1659 self.resources.push(DetectedResource {
1660 name: var_name,
1661 resource_type,
1662 line: node.start_position().row as u32 + 1,
1663 in_context_manager: in_cleanup,
1664 });
1665 }
1666 }
1667 }
1668 }
1669 }
1670 Language::Kotlin => {
1671 if node.kind() == "property_declaration" {
1675 let mut cursor = node.walk();
1676 for child in node.children(&mut cursor) {
1677 if child.kind() == "variable_declaration" {
1678 if let Some(name_node) =
1679 child.child_by_field_name("name").or_else(|| child.child(0))
1680 {
1681 let var_name = node_text(name_node, source).to_string();
1682 let mut inner_cursor = node.walk();
1686 for sibling in node.children(&mut inner_cursor) {
1687 if let Some(resource_type) = self
1688 .get_resource_type_from_call_multilang(
1689 sibling, source, patterns,
1690 )
1691 {
1692 self.resources.push(DetectedResource {
1693 name: var_name.clone(),
1694 resource_type,
1695 line: node.start_position().row as u32 + 1,
1696 in_context_manager: in_cleanup,
1697 });
1698 break;
1699 }
1700 }
1701 }
1702 }
1703 }
1704 } else if node.kind() == "assignment" {
1705 if let Some(left) = node.child_by_field_name("left").or_else(|| node.child(0)) {
1706 if let Some(right) = node.child_by_field_name("right") {
1707 let var_name = node_text(left, source).to_string();
1708 if let Some(resource_type) =
1709 self.get_resource_type_from_call_multilang(right, source, patterns)
1710 {
1711 self.resources.push(DetectedResource {
1712 name: var_name,
1713 resource_type,
1714 line: node.start_position().row as u32 + 1,
1715 in_context_manager: in_cleanup,
1716 });
1717 }
1718 }
1719 }
1720 }
1721 }
1722 Language::Swift => {
1723 if node.kind() == "property_declaration"
1726 || node.kind() == "directly_assignable_expression"
1727 {
1728 if let Some(pattern) = node
1729 .child_by_field_name("pattern")
1730 .or_else(|| node.child_by_field_name("name"))
1731 {
1732 let var_name = node_text(pattern, source).to_string();
1733 let mut cursor = node.walk();
1735 for child in node.children(&mut cursor) {
1736 if let Some(resource_type) =
1737 self.get_resource_type_from_call_multilang(child, source, patterns)
1738 {
1739 self.resources.push(DetectedResource {
1740 name: var_name.clone(),
1741 resource_type,
1742 line: node.start_position().row as u32 + 1,
1743 in_context_manager: in_cleanup,
1744 });
1745 break;
1746 }
1747 }
1748 }
1749 }
1750 }
1751 Language::Ocaml => {
1752 if node.kind() == "let_binding" {
1755 if let Some(pattern) = node.child_by_field_name("pattern") {
1756 let var_name = node_text(pattern, source).to_string();
1757 if let Some(body) = node.child_by_field_name("body") {
1759 if let Some(resource_type) =
1760 self.get_resource_type_from_call_multilang(body, source, patterns)
1761 {
1762 self.resources.push(DetectedResource {
1763 name: var_name,
1764 resource_type,
1765 line: node.start_position().row as u32 + 1,
1766 in_context_manager: in_cleanup,
1767 });
1768 }
1769 }
1770 }
1771 }
1772 }
1773 Language::Lua | Language::Luau => {
1774 if let Some(right) = node
1778 .child_by_field_name("values")
1779 .or_else(|| node.child_by_field_name("right"))
1780 {
1781 if let Some(left) = node
1782 .child_by_field_name("variables")
1783 .or_else(|| node.child_by_field_name("left"))
1784 .or_else(|| node.child_by_field_name("name"))
1785 {
1786 let var_name =
1788 if left.kind() == "variable_list" || left.kind() == "identifier_list" {
1789 left.child(0).map(|c| node_text(c, source).to_string())
1790 } else {
1791 Some(node_text(left, source).to_string())
1792 };
1793 if let Some(var_name) = var_name {
1794 let call_node = if right.kind() == "expression_list" {
1796 right.child(0)
1797 } else {
1798 Some(right)
1799 };
1800 if let Some(call_node) = call_node {
1801 if let Some(resource_type) = self
1802 .get_resource_type_from_call_multilang(
1803 call_node, source, patterns,
1804 )
1805 {
1806 self.resources.push(DetectedResource {
1807 name: var_name,
1808 resource_type,
1809 line: node.start_position().row as u32 + 1,
1810 in_context_manager: in_cleanup,
1811 });
1812 }
1813 }
1814 }
1815 }
1816 }
1817 }
1818 _ => {
1819 if let Some(left) = node.child_by_field_name("left") {
1821 if let Some(right) = node.child_by_field_name("right") {
1822 let var_name = node_text(left, source).to_string();
1823 if let Some(resource_type) =
1824 self.get_resource_type_from_call_multilang(right, source, patterns)
1825 {
1826 self.resources.push(DetectedResource {
1827 name: var_name,
1828 resource_type,
1829 line: node.start_position().row as u32 + 1,
1830 in_context_manager: in_cleanup,
1831 });
1832 }
1833 }
1834 }
1835 }
1836 }
1837 }
1838
1839 fn get_resource_type_from_call_multilang(
1841 &self,
1842 node: Node,
1843 source: &[u8],
1844 patterns: &LangResourcePatterns,
1845 ) -> Option<String> {
1846 let func_name = extract_call_name(node, source)?;
1848
1849 for &(creator, rtype) in patterns.creators {
1851 if func_name == creator
1852 || func_name.ends_with(&format!("::{}", creator))
1853 || func_name.ends_with(&format!(".{}", creator))
1854 {
1855 return Some(rtype.to_string());
1856 }
1857 }
1858
1859 if matches!(self.lang, Language::C | Language::Cpp) {
1861 if node.kind() == "call_expression" {
1862 let text = node_text(node, source);
1863 for &(creator, rtype) in patterns.creators {
1864 if text.starts_with(creator) {
1865 return Some(rtype.to_string());
1866 }
1867 }
1868 }
1869 if node.kind() == "new_expression" {
1871 return Some("heap_object".to_string());
1872 }
1873 }
1874
1875 if matches!(self.lang, Language::Kotlin) {
1877 let text = node_text(node, source);
1879 for &(creator, rtype) in patterns.creators {
1880 if text.starts_with(creator) {
1881 return Some(rtype.to_string());
1882 }
1883 }
1884 }
1885
1886 if matches!(self.lang, Language::Swift) {
1888 let text = node_text(node, source);
1889 for &(creator, rtype) in patterns.creators {
1890 if text.starts_with(creator) {
1891 return Some(rtype.to_string());
1892 }
1893 }
1894 if node.kind() == "force_unwrap_expression" || node.kind() == "try_expression" {
1896 if let Some(child) = node.child(0) {
1897 return self.get_resource_type_from_call_multilang(child, source, patterns);
1898 }
1899 }
1900 }
1901
1902 if matches!(self.lang, Language::Ocaml) {
1904 if node.kind() == "application" {
1906 if let Some(func_node) = node
1907 .child_by_field_name("function")
1908 .or_else(|| node.child(0))
1909 {
1910 let func_text = node_text(func_node, source);
1911 for &(creator, rtype) in patterns.creators {
1912 if func_text == creator || func_text.ends_with(&format!(".{}", creator)) {
1913 return Some(rtype.to_string());
1914 }
1915 }
1916 }
1917 }
1918 let text = node_text(node, source);
1920 let first_word = text.split_whitespace().next().unwrap_or("");
1921 for &(creator, rtype) in patterns.creators {
1922 if first_word == creator {
1923 return Some(rtype.to_string());
1924 }
1925 }
1926 }
1927
1928 if matches!(self.lang, Language::Lua | Language::Luau) {
1930 let text = node_text(node, source);
1931 for &(creator, rtype) in patterns.creators {
1932 if text.starts_with(creator) {
1933 return Some(rtype.to_string());
1934 }
1935 }
1936 }
1937
1938 if matches!(self.lang, Language::Java | Language::CSharp)
1940 && node.kind() == "object_creation_expression"
1941 {
1942 if let Some(type_node) = node.child_by_field_name("type") {
1944 let type_name = node_text(type_node, source);
1945 for &(creator, rtype) in patterns.creators {
1946 if type_name == creator || type_name.contains(creator) {
1947 return Some(rtype.to_string());
1948 }
1949 }
1950 }
1951 }
1952
1953 None
1954 }
1955}
1956
1957impl Default for ResourceDetector {
1958 fn default() -> Self {
1959 Self::new()
1960 }
1961}
1962
1963pub struct LeakDetector {
1969 max_paths: usize,
1971 paths_enumerated: usize,
1973 hit_limit: bool,
1975}
1976
1977impl LeakDetector {
1978 pub fn new() -> Self {
1979 Self {
1980 max_paths: MAX_PATHS,
1981 paths_enumerated: 0,
1982 hit_limit: false,
1983 }
1984 }
1985
1986 pub fn detect(
1988 &mut self,
1989 cfg: &SimpleCfg,
1990 resources: &[ResourceInfo],
1991 source: &[u8],
1992 show_paths: bool,
1993 ) -> Vec<LeakInfo> {
1994 let mut leaks = Vec::new();
1995 self.paths_enumerated = 0;
1996 self.hit_limit = false;
1997
1998 for resource in resources {
1999 if resource.closed {
2001 continue;
2002 }
2003
2004 let paths = self.enumerate_paths(cfg, resource, source);
2006
2007 for path in &paths {
2009 if !self.path_has_close(path, &resource.name) {
2010 leaks.push(LeakInfo {
2011 resource: resource.name.clone(),
2012 line: resource.line,
2013 paths: if show_paths {
2014 Some(vec![self.format_path(path)])
2015 } else {
2016 None
2017 },
2018 });
2019 break; }
2021 }
2022 }
2023
2024 leaks
2025 }
2026
2027 pub fn detect_multilang(
2030 &mut self,
2031 cfg: &SimpleCfg,
2032 resources: &[ResourceInfo],
2033 source: &[u8],
2034 show_paths: bool,
2035 ) -> Vec<LeakInfo> {
2036 self.detect(cfg, resources, source, show_paths)
2037 }
2038
2039 fn enumerate_paths(
2041 &mut self,
2042 cfg: &SimpleCfg,
2043 resource: &ResourceInfo,
2044 _source: &[u8],
2045 ) -> Vec<Vec<usize>> {
2046 let mut paths = Vec::new();
2047
2048 let start_block = self.find_block_with_line(cfg, resource.line);
2050 if start_block.is_none() {
2051 return paths;
2052 }
2053 let start = start_block.unwrap();
2054
2055 for &exit_id in &cfg.exit_blocks {
2057 if self.hit_limit {
2058 break;
2059 }
2060 self.find_paths_dfs(cfg, start, exit_id, &mut Vec::new(), &mut paths);
2061 }
2062
2063 paths
2064 }
2065
2066 fn find_block_with_line(&self, cfg: &SimpleCfg, line: u32) -> Option<usize> {
2067 for (id, block) in &cfg.blocks {
2068 if block.lines.contains(&line) {
2069 return Some(*id);
2070 }
2071 }
2072 Some(cfg.entry_block)
2074 }
2075
2076 fn find_paths_dfs(
2077 &mut self,
2078 cfg: &SimpleCfg,
2079 current: usize,
2080 target: usize,
2081 current_path: &mut Vec<usize>,
2082 paths: &mut Vec<Vec<usize>>,
2083 ) {
2084 if self.paths_enumerated >= self.max_paths {
2086 self.hit_limit = true;
2087 return;
2088 }
2089
2090 if current_path.contains(¤t) {
2092 return;
2093 }
2094
2095 current_path.push(current);
2096
2097 if current == target {
2098 paths.push(current_path.clone());
2099 self.paths_enumerated += 1;
2100 } else if let Some(block) = cfg.blocks.get(¤t) {
2101 for &succ in &block.succs {
2102 self.find_paths_dfs(cfg, succ, target, current_path, paths);
2103 if self.hit_limit {
2104 break;
2105 }
2106 }
2107 }
2108
2109 current_path.pop();
2110 }
2111
2112 fn path_has_close(&self, path: &[usize], resource_name: &str) -> bool {
2113 let _ = (path, resource_name);
2118 false
2119 }
2120
2121 fn format_path(&self, path: &[usize]) -> String {
2122 path.iter()
2123 .map(|id| id.to_string())
2124 .collect::<Vec<_>>()
2125 .join(" -> ")
2126 }
2127}
2128
2129impl Default for LeakDetector {
2130 fn default() -> Self {
2131 Self::new()
2132 }
2133}
2134
2135pub struct DoubleCloseDetector {
2141 lang: Language,
2142}
2143
2144impl DoubleCloseDetector {
2145 pub fn new() -> Self {
2146 Self {
2147 lang: Language::Python,
2148 }
2149 }
2150
2151 pub fn with_language(lang: Language) -> Self {
2152 Self { lang }
2153 }
2154
2155 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2157 let mut issues = Vec::new();
2158 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2159
2160 self.find_closes(func_node, source, &mut close_sites);
2161
2162 for (resource, lines) in close_sites {
2163 if lines.len() > 1 {
2164 issues.push(DoubleCloseInfo {
2165 resource,
2166 first_close: lines[0],
2167 second_close: lines[1],
2168 });
2169 }
2170 }
2171
2172 issues
2173 }
2174
2175 pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2177 let mut issues = Vec::new();
2178 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2179 let patterns = get_resource_patterns(self.lang);
2180
2181 self.find_closes_multilang(func_node, source, &mut close_sites, &patterns);
2182
2183 for (resource, lines) in close_sites {
2184 if lines.len() > 1 {
2185 issues.push(DoubleCloseInfo {
2186 resource,
2187 first_close: lines[0],
2188 second_close: lines[1],
2189 });
2190 }
2191 }
2192
2193 issues
2194 }
2195
2196 fn find_closes(&self, node: Node, source: &[u8], closes: &mut HashMap<String, Vec<u32>>) {
2197 if node.kind() == "call" {
2198 if let Some(func) = node.child_by_field_name("function") {
2199 if func.kind() == "attribute" {
2200 if let Some(attr) = func.child_by_field_name("attribute") {
2201 let method = node_text(attr, source);
2202 if RESOURCE_CLOSERS.contains(&method) {
2203 if let Some(obj) = func.child_by_field_name("object") {
2204 let var_name = node_text(obj, source).to_string();
2205 let line = node.start_position().row as u32 + 1;
2206 closes.entry(var_name).or_default().push(line);
2207 }
2208 }
2209 }
2210 }
2211 }
2212 }
2213
2214 let mut cursor = node.walk();
2215 for child in node.children(&mut cursor) {
2216 self.find_closes(child, source, closes);
2217 }
2218 }
2219
2220 fn find_closes_multilang(
2221 &self,
2222 node: Node,
2223 source: &[u8],
2224 closes: &mut HashMap<String, Vec<u32>>,
2225 patterns: &LangResourcePatterns,
2226 ) {
2227 let kind = node.kind();
2228 if kind == "call"
2230 || kind == "call_expression"
2231 || kind == "method_invocation"
2232 || kind == "invocation_expression"
2233 {
2234 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2235 if patterns.closers.contains(&method.as_str()) {
2236 let line = node.start_position().row as u32 + 1;
2237 closes.entry(var_name).or_default().push(line);
2238 }
2239 }
2240 }
2241
2242 let mut cursor = node.walk();
2243 for child in node.children(&mut cursor) {
2244 self.find_closes_multilang(child, source, closes, patterns);
2245 }
2246 }
2247}
2248
2249impl Default for DoubleCloseDetector {
2250 fn default() -> Self {
2251 Self::new()
2252 }
2253}
2254
2255pub struct UseAfterCloseDetector {
2261 lang: Language,
2262}
2263
2264impl UseAfterCloseDetector {
2265 pub fn new() -> Self {
2266 Self {
2267 lang: Language::Python,
2268 }
2269 }
2270
2271 pub fn with_language(lang: Language) -> Self {
2272 Self { lang }
2273 }
2274
2275 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2277 let mut issues = Vec::new();
2278 let mut close_lines: HashMap<String, u32> = HashMap::new();
2279 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2280
2281 self.analyze(func_node, source, &mut close_lines, &mut uses_after_close);
2282
2283 for (resource, close_line, use_line) in uses_after_close {
2284 issues.push(UseAfterCloseInfo {
2285 resource,
2286 close_line,
2287 use_line,
2288 });
2289 }
2290
2291 issues
2292 }
2293
2294 pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2296 let mut issues = Vec::new();
2297 let mut close_lines: HashMap<String, u32> = HashMap::new();
2298 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2299 let patterns = get_resource_patterns(self.lang);
2300
2301 self.analyze_multilang(
2302 func_node,
2303 source,
2304 &mut close_lines,
2305 &mut uses_after_close,
2306 &patterns,
2307 );
2308
2309 for (resource, close_line, use_line) in uses_after_close {
2310 issues.push(UseAfterCloseInfo {
2311 resource,
2312 close_line,
2313 use_line,
2314 });
2315 }
2316
2317 issues
2318 }
2319
2320 fn analyze(
2321 &self,
2322 node: Node,
2323 source: &[u8],
2324 close_lines: &mut HashMap<String, u32>,
2325 uses_after: &mut Vec<(String, u32, u32)>,
2326 ) {
2327 let line = node.start_position().row as u32 + 1;
2328
2329 if node.kind() == "call" {
2330 if let Some(func) = node.child_by_field_name("function") {
2331 if func.kind() == "attribute" {
2332 if let Some(attr) = func.child_by_field_name("attribute") {
2333 let method = node_text(attr, source);
2334 if RESOURCE_CLOSERS.contains(&method) {
2335 if let Some(obj) = func.child_by_field_name("object") {
2336 let var_name = node_text(obj, source).to_string();
2337 close_lines.insert(var_name, line);
2338 }
2339 } else if let Some(obj) = func.child_by_field_name("object") {
2340 let var_name = node_text(obj, source).to_string();
2341 if let Some(&close_line) = close_lines.get(&var_name) {
2342 if line > close_line {
2343 uses_after.push((var_name, close_line, line));
2344 }
2345 }
2346 }
2347 }
2348 }
2349 }
2350 }
2351
2352 if node.kind() == "attribute" {
2353 if let Some(obj) = node.child_by_field_name("object") {
2354 if obj.kind() == "identifier" {
2355 let var_name = node_text(obj, source).to_string();
2356 if let Some(&close_line) = close_lines.get(&var_name) {
2357 if line > close_line {
2358 uses_after.push((var_name, close_line, line));
2359 }
2360 }
2361 }
2362 }
2363 }
2364
2365 let mut cursor = node.walk();
2366 for child in node.children(&mut cursor) {
2367 self.analyze(child, source, close_lines, uses_after);
2368 }
2369 }
2370
2371 fn analyze_multilang(
2372 &self,
2373 node: Node,
2374 source: &[u8],
2375 close_lines: &mut HashMap<String, u32>,
2376 uses_after: &mut Vec<(String, u32, u32)>,
2377 patterns: &LangResourcePatterns,
2378 ) {
2379 let line = node.start_position().row as u32 + 1;
2380 let kind = node.kind();
2381
2382 if kind == "call"
2384 || kind == "call_expression"
2385 || kind == "method_invocation"
2386 || kind == "invocation_expression"
2387 {
2388 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2389 if patterns.closers.contains(&method.as_str()) {
2390 close_lines.insert(var_name, line);
2391 } else {
2392 if let Some((obj_name, _)) = extract_close_call(node, source, self.lang) {
2395 if let Some(&close_line) = close_lines.get(&obj_name) {
2396 if line > close_line {
2397 uses_after.push((obj_name, close_line, line));
2398 }
2399 }
2400 }
2401 }
2402 }
2403 }
2404
2405 if kind == "attribute"
2407 || kind == "member_expression"
2408 || kind == "field_expression"
2409 || kind == "selector_expression"
2410 {
2411 if let Some(obj) = node
2412 .child_by_field_name("object")
2413 .or_else(|| node.child_by_field_name("operand"))
2414 .or_else(|| node.child(0))
2415 {
2416 if obj.kind() == "identifier" {
2417 let var_name = node_text(obj, source).to_string();
2418 if let Some(&close_line) = close_lines.get(&var_name) {
2419 if line > close_line {
2420 uses_after.push((var_name, close_line, line));
2421 }
2422 }
2423 }
2424 }
2425 }
2426
2427 let mut cursor = node.walk();
2428 for child in node.children(&mut cursor) {
2429 self.analyze_multilang(child, source, close_lines, uses_after, patterns);
2430 }
2431 }
2432}
2433
2434impl Default for UseAfterCloseDetector {
2435 fn default() -> Self {
2436 Self::new()
2437 }
2438}
2439
2440pub fn suggest_context_manager(resources: &[ResourceInfo]) -> Vec<ContextSuggestion> {
2446 resources
2447 .iter()
2448 .filter(|r| !r.closed) .map(|r| {
2450 let suggestion = match r.resource_type.as_str() {
2451 "file" => format!("with open(...) as {}:", r.name),
2452 "connection" => format!("with connect(...) as {}:", r.name),
2453 "cursor" => format!("with connection.cursor() as {}:", r.name),
2454 "socket" => format!("with socket.socket(...) as {}:", r.name),
2455 _ => format!("with {} as {}:", r.resource_type, r.name),
2456 };
2457 ContextSuggestion {
2458 resource: r.name.clone(),
2459 suggestion,
2460 }
2461 })
2462 .collect()
2463}
2464
2465pub fn suggest_context_manager_multilang(
2467 resources: &[ResourceInfo],
2468 lang: Language,
2469) -> Vec<ContextSuggestion> {
2470 resources
2471 .iter()
2472 .filter(|r| !r.closed)
2473 .map(|r| {
2474 let suggestion = match lang {
2475 Language::Python => match r.resource_type.as_str() {
2476 "file" => format!("with open(...) as {}:", r.name),
2477 "connection" => format!("with connect(...) as {}:", r.name),
2478 "cursor" => format!("with connection.cursor() as {}:", r.name),
2479 "socket" => format!("with socket.socket(...) as {}:", r.name),
2480 _ => format!("with {} as {}:", r.resource_type, r.name),
2481 },
2482 Language::Go => format!("defer {}.Close()", r.name),
2483 Language::Rust => format!("// {}: Drop trait handles cleanup automatically. Consider wrapping in a scope block.", r.name),
2484 Language::Java => match r.resource_type.as_str() {
2485 "file_stream" | "reader" | "writer" | "scanner" | "stream" =>
2486 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2487 "connection" | "statement" =>
2488 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2489 _ => format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2490 },
2491 Language::CSharp => format!("using (var {} = ...) {{ ... }}", r.name),
2492 Language::TypeScript | Language::JavaScript =>
2493 format!("try {{ ... }} finally {{ {}.close(); }}", r.name),
2494 Language::C => match r.resource_type.as_str() {
2495 "file" => format!("// Ensure fclose({}) on all paths", r.name),
2496 "memory" => format!("// Ensure free({}) on all paths", r.name),
2497 _ => format!("// Ensure cleanup of {} on all paths", r.name),
2498 },
2499 Language::Cpp => match r.resource_type.as_str() {
2500 "heap_object" => format!("// Use std::unique_ptr or std::shared_ptr instead of raw new for {}", r.name),
2501 "memory" => format!("// Use RAII wrapper or smart pointer for {}", r.name),
2502 _ => format!("// Consider RAII wrapper for {}", r.name),
2503 },
2504 Language::Ruby => format!("File.open(...) do |{}| ... end", r.name),
2505 Language::Php => format!("// Ensure {}() cleanup in finally block", r.name),
2506 Language::Kotlin => format!("{}.use {{ {} -> ... }}", r.name, r.name),
2507 Language::Swift => format!("defer {{ {}.closeFile() }}", r.name),
2508 Language::Ocaml => format!("Fun.protect ~finally:(fun () -> close_in {}) (fun () -> ...)", r.name),
2509 Language::Lua | Language::Luau => format!("// Ensure {}:close() is called, consider pcall for cleanup", r.name),
2510 _ => format!("// Ensure {} is properly closed/released", r.name),
2511 };
2512 ContextSuggestion {
2513 resource: r.name.clone(),
2514 suggestion,
2515 }
2516 })
2517 .collect()
2518}
2519
2520pub fn generate_constraints(
2526 file: &str,
2527 function: Option<&str>,
2528 resources: &[ResourceInfo],
2529 leaks: &[LeakInfo],
2530 double_closes: &[DoubleCloseInfo],
2531 use_after_closes: &[UseAfterCloseInfo],
2532) -> Vec<ResourceConstraint> {
2533 let mut constraints = Vec::new();
2534 let context = function.unwrap_or("module").to_string();
2535
2536 for leak in leaks {
2538 constraints.push(ResourceConstraint {
2539 rule: format!(
2540 "Resource '{}' opened at line {} must be closed on all control flow paths",
2541 leak.resource, leak.line
2542 ),
2543 context: format!("{} in {}", context, file),
2544 confidence: 0.9,
2545 });
2546 }
2547
2548 for dc in double_closes {
2550 constraints.push(ResourceConstraint {
2551 rule: format!(
2552 "Resource '{}' must not be closed twice (lines {} and {})",
2553 dc.resource, dc.first_close, dc.second_close
2554 ),
2555 context: format!("{} in {}", context, file),
2556 confidence: 0.95,
2557 });
2558 }
2559
2560 for uac in use_after_closes {
2562 constraints.push(ResourceConstraint {
2563 rule: format!(
2564 "Resource '{}' must not be used at line {} after being closed at line {}",
2565 uac.resource, uac.use_line, uac.close_line
2566 ),
2567 context: format!("{} in {}", context, file),
2568 confidence: 0.95,
2569 });
2570 }
2571
2572 for resource in resources {
2574 if !resource.closed {
2575 constraints.push(ResourceConstraint {
2576 rule: format!(
2577 "Resource '{}' ({}) should use context manager pattern (with statement)",
2578 resource.name, resource.resource_type
2579 ),
2580 context: format!("{} in {}", context, file),
2581 confidence: 0.85,
2582 });
2583 }
2584 }
2585
2586 constraints
2587}
2588
2589pub fn format_resources_text(report: &ResourceReport) -> String {
2595 let mut lines = Vec::new();
2596
2597 lines.push(format!("Resource Analysis: {}", report.file));
2598 lines.push(format!("Language: {}", report.language));
2599 if let Some(ref func) = report.function {
2600 lines.push(format!("Function: {}", func));
2601 }
2602 lines.push(String::new());
2603
2604 lines.push(format!("Resources detected: {}", report.resources.len()));
2606 for r in &report.resources {
2607 let status = if r.closed { "closed" } else { "open" };
2608 lines.push(format!(
2609 " - {}: {} at line {} [{}]",
2610 r.name, r.resource_type, r.line, status
2611 ));
2612 }
2613 lines.push(String::new());
2614
2615 if !report.leaks.is_empty() {
2617 lines.push(format!("Leaks found: {}", report.leaks.len()));
2618 for leak in &report.leaks {
2619 lines.push(format!(" - {} at line {}", leak.resource, leak.line));
2620 if let Some(ref paths) = leak.paths {
2621 for path in paths {
2622 lines.push(format!(" Path: {}", path));
2623 }
2624 }
2625 }
2626 } else {
2627 lines.push("Leaks found: 0".to_string());
2628 }
2629
2630 if !report.double_closes.is_empty() {
2632 lines.push(String::new());
2633 lines.push(format!(
2634 "Double-close errors: {}",
2635 report.double_closes.len()
2636 ));
2637 for dc in &report.double_closes {
2638 lines.push(format!(
2639 " - {}: first close at {}, second close at {}",
2640 dc.resource, dc.first_close, dc.second_close
2641 ));
2642 }
2643 }
2644
2645 if !report.use_after_closes.is_empty() {
2647 lines.push(String::new());
2648 lines.push(format!(
2649 "Use-after-close errors: {}",
2650 report.use_after_closes.len()
2651 ));
2652 for uac in &report.use_after_closes {
2653 lines.push(format!(
2654 " - {}: closed at {}, used at {}",
2655 uac.resource, uac.close_line, uac.use_line
2656 ));
2657 }
2658 }
2659
2660 if !report.suggestions.is_empty() {
2662 lines.push(String::new());
2663 lines.push(format!("Suggestions: {}", report.suggestions.len()));
2664 for s in &report.suggestions {
2665 lines.push(format!(" - {}: {}", s.resource, s.suggestion));
2666 }
2667 }
2668
2669 if !report.constraints.is_empty() {
2671 lines.push(String::new());
2672 lines.push(format!("Constraints: {}", report.constraints.len()));
2673 for c in &report.constraints {
2674 lines.push(format!(" - {} (confidence: {:.2})", c.rule, c.confidence));
2675 }
2676 }
2677
2678 lines.push(String::new());
2680 lines.push("Summary:".to_string());
2681 lines.push(format!(
2682 " resources_detected: {}",
2683 report.summary.resources_detected
2684 ));
2685 lines.push(format!(" leaks_found: {}", report.summary.leaks_found));
2686 lines.push(format!(
2687 " double_closes_found: {}",
2688 report.summary.double_closes_found
2689 ));
2690 lines.push(format!(
2691 " use_after_closes_found: {}",
2692 report.summary.use_after_closes_found
2693 ));
2694 lines.push(String::new());
2695 lines.push(format!(
2696 "Analysis completed in {}ms",
2697 report.analysis_time_ms
2698 ));
2699
2700 lines.join("\n")
2701}
2702
2703fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2708 std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
2709}
2710
2711fn extract_call_name(node: Node, source: &[u8]) -> Option<String> {
2714 match node.kind() {
2716 "call" | "call_expression" | "method_invocation" | "invocation_expression" => {
2718 if let Some(func) = node
2719 .child_by_field_name("function")
2720 .or_else(|| node.child_by_field_name("name"))
2721 .or_else(|| node.child_by_field_name("method"))
2722 {
2723 let func_text = node_text(func, source);
2724 let func_name = func_text
2726 .split('.')
2727 .next_back()
2728 .unwrap_or(func_text)
2729 .rsplit("::")
2730 .next()
2731 .unwrap_or(func_text);
2732 return Some(func_name.to_string());
2733 }
2734 if let Some(first_child) = node.child(0) {
2736 let text = node_text(first_child, source);
2737 let name = text
2738 .split('.')
2739 .next_back()
2740 .unwrap_or(text)
2741 .rsplit("::")
2742 .next()
2743 .unwrap_or(text);
2744 return Some(name.to_string());
2745 }
2746 }
2747 "composite_literal" => {
2749 }
2751 _ => {}
2752 }
2753
2754 let text = node_text(node, source);
2756 if text.contains('(') {
2757 let name_part = text.split('(').next()?;
2758 let func_name = name_part
2759 .split('.')
2760 .next_back()
2761 .unwrap_or(name_part)
2762 .rsplit("::")
2763 .next()
2764 .unwrap_or(name_part)
2765 .trim();
2766 if !func_name.is_empty() {
2767 return Some(func_name.to_string());
2768 }
2769 }
2770
2771 None
2772}
2773
2774fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
2776 match declarator.kind() {
2777 "identifier" => Some(node_text(declarator, source).to_string()),
2778 "pointer_declarator" => {
2779 let mut cursor = declarator.walk();
2781 for child in declarator.children(&mut cursor) {
2782 if child.kind() == "identifier" {
2783 return Some(node_text(child, source).to_string());
2784 }
2785 if child.kind() == "pointer_declarator" {
2786 return extract_c_declarator_name(child, source);
2787 }
2788 }
2789 None
2790 }
2791 _ => Some(node_text(declarator, source).to_string()),
2792 }
2793}
2794
2795fn extract_close_call(node: Node, source: &[u8], lang: Language) -> Option<(String, String)> {
2797 match lang {
2798 Language::Python
2799 | Language::Ruby
2800 | Language::Java
2801 | Language::CSharp
2802 | Language::TypeScript
2803 | Language::JavaScript
2804 | Language::Scala
2805 | Language::Kotlin
2806 | Language::Swift => {
2807 if let Some(func) = node
2809 .child_by_field_name("function")
2810 .or_else(|| node.child_by_field_name("method"))
2811 .or_else(|| node.child_by_field_name("name"))
2812 {
2813 if func.kind() == "attribute"
2815 || func.kind() == "member_expression"
2816 || func.kind() == "selector_expression"
2817 || func.kind() == "field_access"
2818 {
2819 let obj = func.child_by_field_name("object").or_else(|| func.child(0));
2820 let attr = func
2821 .child_by_field_name("attribute")
2822 .or_else(|| func.child_by_field_name("field"))
2823 .or_else(|| func.child_by_field_name("name"));
2824
2825 if let (Some(obj), Some(attr)) = (obj, attr) {
2826 let var_name = node_text(obj, source).to_string();
2827 let method = node_text(attr, source).to_string();
2828 return Some((var_name, method));
2829 }
2830 }
2831 }
2832 None
2833 }
2834 Language::Go => {
2835 if let Some(func) = node.child_by_field_name("function") {
2837 if func.kind() == "selector_expression" {
2838 if let Some(operand) = func.child_by_field_name("operand") {
2839 if let Some(field) = func.child_by_field_name("field") {
2840 let var_name = node_text(operand, source).to_string();
2841 let method = node_text(field, source).to_string();
2842 return Some((var_name, method));
2843 }
2844 }
2845 }
2846 }
2847 None
2848 }
2849 Language::C | Language::Cpp => {
2850 if let Some(func) = node
2852 .child_by_field_name("function")
2853 .or_else(|| node.child(0))
2854 {
2855 let func_name = node_text(func, source).to_string();
2856 if let Some(args) = node.child_by_field_name("arguments") {
2858 if let Some(first_arg) = args.child(1) {
2859 let var_name = node_text(first_arg, source).to_string();
2861 return Some((var_name, func_name));
2862 }
2863 }
2864 }
2865 None
2866 }
2867 _ => {
2868 if let Some(func) = node.child_by_field_name("function") {
2870 if let Some(obj) = func.child_by_field_name("object").or_else(|| func.child(0)) {
2871 if let Some(attr) = func.child_by_field_name("attribute") {
2872 let var_name = node_text(obj, source).to_string();
2873 let method = node_text(attr, source).to_string();
2874 return Some((var_name, method));
2875 }
2876 }
2877 }
2878 None
2879 }
2880 }
2881}
2882
2883pub fn build_cfg_multilang(func_node: Node, source: &[u8], lang: Language) -> SimpleCfg {
2889 let patterns = get_resource_patterns(lang);
2890 let mut cfg = SimpleCfg::new();
2891 let entry_id = cfg.new_block();
2892 cfg.entry_block = entry_id;
2893
2894 if let Some(block) = cfg.blocks.get_mut(&entry_id) {
2895 block.is_entry = true;
2896 }
2897
2898 let body = func_node
2900 .children(&mut func_node.walk())
2901 .find(|n| patterns.body_kinds.contains(&n.kind()));
2902
2903 if let Some(body_node) = body {
2904 let exit_id =
2905 process_statements_multilang(&mut cfg, body_node, source, entry_id, &patterns);
2906 if let Some(exit) = exit_id {
2907 if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
2908 cfg.mark_exit(exit);
2909 }
2910 }
2911 } else {
2912 cfg.mark_exit(entry_id);
2914 }
2915
2916 cfg
2917}
2918
2919fn process_statements_multilang(
2920 cfg: &mut SimpleCfg,
2921 node: Node,
2922 source: &[u8],
2923 mut current: usize,
2924 patterns: &LangResourcePatterns,
2925) -> Option<usize> {
2926 let mut cursor = node.walk();
2927 for child in node.children(&mut cursor) {
2928 let kind = child.kind();
2929
2930 if patterns.return_kinds.contains(&kind) {
2931 let text = node_text(child, source).to_string();
2933 let line = child.start_position().row as u32 + 1;
2934 if let Some(block) = cfg.blocks.get_mut(¤t) {
2935 block
2936 .stmts
2937 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2938 block.lines.push(line);
2939 }
2940 cfg.mark_exit(current);
2941 return None;
2942 } else if patterns.if_kinds.contains(&kind) {
2943 current = process_if_multilang(cfg, child, source, current, patterns)?;
2945 } else if patterns.loop_kinds.contains(&kind) {
2946 current = process_loop_multilang(cfg, child, source, current, patterns)?;
2948 } else if patterns.try_kinds.contains(&kind) {
2949 current = process_try_multilang(cfg, child, source, current, patterns)?;
2951 } else if patterns.cleanup_block_kinds.contains(&kind) {
2952 current = process_cleanup_block_multilang(cfg, child, source, current, patterns)?;
2954 } else {
2955 let text = node_text(child, source).to_string();
2957 let line = child.start_position().row as u32 + 1;
2958 if let Some(block) = cfg.blocks.get_mut(¤t) {
2959 block
2960 .stmts
2961 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2962 block.lines.push(line);
2963 }
2964 }
2965 }
2966 Some(current)
2967}
2968
2969fn process_if_multilang(
2970 cfg: &mut SimpleCfg,
2971 node: Node,
2972 source: &[u8],
2973 current: usize,
2974 patterns: &LangResourcePatterns,
2975) -> Option<usize> {
2976 if let Some(cond) = node.child_by_field_name("condition") {
2978 let text = node_text(cond, source).to_string();
2979 let line = cond.start_position().row as u32 + 1;
2980 if let Some(block) = cfg.blocks.get_mut(¤t) {
2981 block.stmts.push((
2982 cond.start_byte(),
2983 cond.end_byte(),
2984 "condition".to_string(),
2985 text,
2986 ));
2987 block.lines.push(line);
2988 }
2989 }
2990
2991 let true_block = cfg.new_block();
2992 cfg.add_edge(current, true_block);
2993
2994 let mut cursor = node.walk();
2996 let consequence = node
2997 .children(&mut cursor)
2998 .find(|n| patterns.body_kinds.contains(&n.kind()));
2999 let true_exit = if let Some(body) = consequence {
3000 process_statements_multilang(cfg, body, source, true_block, patterns)
3001 } else {
3002 Some(true_block)
3003 };
3004
3005 let mut cursor = node.walk();
3007 let alternative = node
3008 .children(&mut cursor)
3009 .find(|n| n.kind() == "else_clause" || n.kind() == "elif_clause" || n.kind() == "else");
3010
3011 let false_exit = if let Some(alt) = alternative {
3012 let false_block = cfg.new_block();
3013 cfg.add_edge(current, false_block);
3014 let alt_body = alt
3015 .children(&mut alt.walk())
3016 .find(|n| patterns.body_kinds.contains(&n.kind()));
3017 if let Some(alt_body) = alt_body {
3018 process_statements_multilang(cfg, alt_body, source, false_block, patterns)
3019 } else {
3020 Some(false_block)
3021 }
3022 } else {
3023 None
3024 };
3025
3026 let merge = cfg.new_block();
3027 if let Some(te) = true_exit {
3028 cfg.add_edge(te, merge);
3029 }
3030 if let Some(fe) = false_exit {
3031 cfg.add_edge(fe, merge);
3032 }
3033 if alternative.is_none() {
3034 cfg.add_edge(current, merge);
3035 }
3036
3037 Some(merge)
3038}
3039
3040fn process_loop_multilang(
3041 cfg: &mut SimpleCfg,
3042 node: Node,
3043 source: &[u8],
3044 current: usize,
3045 patterns: &LangResourcePatterns,
3046) -> Option<usize> {
3047 let header = cfg.new_block();
3048 cfg.add_edge(current, header);
3049
3050 if let Some(cond) = node.child_by_field_name("condition") {
3051 let text = node_text(cond, source).to_string();
3052 let line = cond.start_position().row as u32 + 1;
3053 if let Some(block) = cfg.blocks.get_mut(&header) {
3054 block.stmts.push((
3055 cond.start_byte(),
3056 cond.end_byte(),
3057 "loop_condition".to_string(),
3058 text,
3059 ));
3060 block.lines.push(line);
3061 }
3062 }
3063
3064 let body_block = cfg.new_block();
3065 cfg.add_edge(header, body_block);
3066
3067 let body = node
3068 .children(&mut node.walk())
3069 .find(|n| patterns.body_kinds.contains(&n.kind()));
3070 let body_exit = if let Some(body_node) = body {
3071 process_statements_multilang(cfg, body_node, source, body_block, patterns)
3072 } else {
3073 Some(body_block)
3074 };
3075
3076 if let Some(be) = body_exit {
3077 cfg.add_edge(be, header);
3078 }
3079
3080 let exit = cfg.new_block();
3081 cfg.add_edge(header, exit);
3082 Some(exit)
3083}
3084
3085fn process_try_multilang(
3086 cfg: &mut SimpleCfg,
3087 node: Node,
3088 source: &[u8],
3089 current: usize,
3090 patterns: &LangResourcePatterns,
3091) -> Option<usize> {
3092 let try_block = cfg.new_block();
3093 cfg.add_edge(current, try_block);
3094
3095 let try_body = node
3096 .children(&mut node.walk())
3097 .find(|n| patterns.body_kinds.contains(&n.kind()));
3098 let try_exit = if let Some(body) = try_body {
3099 process_statements_multilang(cfg, body, source, try_block, patterns)
3100 } else {
3101 Some(try_block)
3102 };
3103
3104 let mut cursor = node.walk();
3105 let mut handler_exits = Vec::new();
3106 for child in node.children(&mut cursor) {
3107 let ck = child.kind();
3108 if ck == "except_clause" || ck == "catch_clause" || ck == "rescue" {
3109 let handler_block = cfg.new_block();
3110 cfg.add_edge(try_block, handler_block);
3111 if let Some(block) = cfg.blocks.get_mut(&try_block) {
3112 block.exception_handlers.push(handler_block);
3113 }
3114 let handler_body = child
3115 .children(&mut child.walk())
3116 .find(|n| patterns.body_kinds.contains(&n.kind()));
3117 if let Some(hb) = handler_body {
3118 if let Some(exit) =
3119 process_statements_multilang(cfg, hb, source, handler_block, patterns)
3120 {
3121 handler_exits.push(exit);
3122 }
3123 } else {
3124 handler_exits.push(handler_block);
3125 }
3126 }
3127 }
3128
3129 let finally_clause = node
3130 .children(&mut node.walk())
3131 .find(|n| n.kind() == "finally_clause" || n.kind() == "finally");
3132
3133 let merge = cfg.new_block();
3134 if let Some(te) = try_exit {
3135 if let Some(finally) = finally_clause {
3136 let finally_block = cfg.new_block();
3137 cfg.add_edge(te, finally_block);
3138 let finally_body = finally
3139 .children(&mut finally.walk())
3140 .find(|n| patterns.body_kinds.contains(&n.kind()));
3141 if let Some(fb) = finally_body {
3142 if let Some(exit) =
3143 process_statements_multilang(cfg, fb, source, finally_block, patterns)
3144 {
3145 cfg.add_edge(exit, merge);
3146 }
3147 } else {
3148 cfg.add_edge(finally_block, merge);
3149 }
3150 } else {
3151 cfg.add_edge(te, merge);
3152 }
3153 }
3154 for he in handler_exits {
3155 cfg.add_edge(he, merge);
3156 }
3157
3158 Some(merge)
3159}
3160
3161fn process_cleanup_block_multilang(
3162 cfg: &mut SimpleCfg,
3163 node: Node,
3164 source: &[u8],
3165 current: usize,
3166 patterns: &LangResourcePatterns,
3167) -> Option<usize> {
3168 let text = node_text(node, source).to_string();
3169 let line = node.start_position().row as u32 + 1;
3170 if let Some(block) = cfg.blocks.get_mut(¤t) {
3171 block.stmts.push((
3172 node.start_byte(),
3173 node.end_byte(),
3174 node.kind().to_string(),
3175 text,
3176 ));
3177 block.lines.push(line);
3178 }
3179
3180 let body = node
3181 .children(&mut node.walk())
3182 .find(|n| patterns.body_kinds.contains(&n.kind()));
3183 if let Some(body_node) = body {
3184 process_statements_multilang(cfg, body_node, source, current, patterns)
3185 } else {
3186 Some(current)
3187 }
3188}
3189
3190#[cfg(test)]
3191fn get_python_parser() -> PatternsResult<Parser> {
3192 get_parser_for_language(Language::Python)
3193}
3194
3195fn get_parser_for_language(lang: Language) -> PatternsResult<Parser> {
3197 let mut parser = Parser::new();
3198 let ts_lang =
3199 ParserPool::get_ts_language(lang).ok_or_else(|| PatternsError::UnsupportedLanguage {
3200 language: lang.as_str().to_string(),
3201 })?;
3202 parser
3203 .set_language(&ts_lang)
3204 .map_err(|e| PatternsError::ParseError {
3205 file: PathBuf::from("<internal>"),
3206 message: format!("Failed to set {} language: {}", lang.as_str(), e),
3207 })?;
3208 Ok(parser)
3209}
3210
3211fn get_function_name_from_node(
3215 node: Node,
3216 source: &[u8],
3217 patterns: &LangResourcePatterns,
3218) -> Option<String> {
3219 if node.kind() == "value_definition" {
3221 let mut cursor = node.walk();
3222 for child in node.children(&mut cursor) {
3223 if child.kind() == "let_binding" {
3224 if let Some(pattern) = child.child_by_field_name("pattern") {
3225 return Some(node_text(pattern, source).to_string());
3226 }
3227 }
3228 }
3229 return None;
3230 }
3231
3232 if let Some(name_node) = node.child_by_field_name(patterns.name_field) {
3234 if name_node.kind() == "function_declarator" {
3237 if let Some(inner) = name_node.child_by_field_name("declarator") {
3238 return Some(node_text(inner, source).to_string());
3239 }
3240 }
3241 if name_node.kind() == "pointer_declarator" {
3243 let mut cursor = name_node.walk();
3244 for child in name_node.children(&mut cursor) {
3245 if child.kind() == "function_declarator" {
3246 if let Some(inner) = child.child_by_field_name("declarator") {
3247 return Some(node_text(inner, source).to_string());
3248 }
3249 }
3250 }
3251 }
3252 return Some(node_text(name_node, source).to_string());
3253 }
3254 None
3255}
3256
3257#[cfg(test)]
3258fn find_function_node<'a>(
3259 tree: &'a tree_sitter::Tree,
3260 function_name: &str,
3261 source: &[u8],
3262) -> Option<Node<'a>> {
3263 let root = tree.root_node();
3264 let patterns = get_resource_patterns(Language::Python);
3266 find_function_recursive(root, function_name, source, &patterns)
3267}
3268
3269fn find_function_node_multilang<'a>(
3270 tree: &'a tree_sitter::Tree,
3271 function_name: &str,
3272 source: &[u8],
3273 lang: Language,
3274) -> Option<Node<'a>> {
3275 let root = tree.root_node();
3276 let patterns = get_resource_patterns(lang);
3277 find_function_recursive(root, function_name, source, &patterns)
3278}
3279
3280fn find_function_recursive<'a>(
3281 node: Node<'a>,
3282 function_name: &str,
3283 source: &[u8],
3284 patterns: &LangResourcePatterns,
3285) -> Option<Node<'a>> {
3286 let kind = node.kind();
3287 if patterns.function_kinds.contains(&kind) {
3288 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3289 if name == function_name {
3290 return Some(node);
3291 }
3292 }
3293 }
3294
3295 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3298 let mut decl_cursor = node.walk();
3299 for child in node.children(&mut decl_cursor) {
3300 if child.kind() == "variable_declarator" {
3301 if let Some(name_node) = child.child_by_field_name("name") {
3302 let var_name = name_node.utf8_text(source).unwrap_or("");
3303 if var_name == function_name {
3304 if let Some(value_node) = child.child_by_field_name("value") {
3305 if matches!(
3306 value_node.kind(),
3307 "arrow_function"
3308 | "function"
3309 | "function_expression"
3310 | "generator_function"
3311 ) {
3312 return Some(value_node);
3313 }
3314 }
3315 }
3316 }
3317 }
3318 }
3319 }
3320
3321 let mut cursor = node.walk();
3322 for child in node.children(&mut cursor) {
3323 if let Some(found) = find_function_recursive(child, function_name, source, patterns) {
3324 return Some(found);
3325 }
3326 }
3327
3328 None
3329}
3330
3331fn find_all_functions_multilang<'a>(
3332 tree: &'a tree_sitter::Tree,
3333 source: &[u8],
3334 lang: Language,
3335) -> Vec<(String, Node<'a>)> {
3336 let mut functions = Vec::new();
3337 let patterns = get_resource_patterns(lang);
3338 collect_functions(tree.root_node(), source, &mut functions, &patterns);
3339 functions
3340}
3341
3342fn collect_functions<'a>(
3343 node: Node<'a>,
3344 source: &[u8],
3345 functions: &mut Vec<(String, Node<'a>)>,
3346 patterns: &LangResourcePatterns,
3347) {
3348 let kind = node.kind();
3349 if patterns.function_kinds.contains(&kind) {
3350 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3351 functions.push((name, node));
3352 }
3353 }
3354
3355 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3358 let mut decl_cursor = node.walk();
3359 for child in node.children(&mut decl_cursor) {
3360 if child.kind() == "variable_declarator" {
3361 if let Some(name_node) = child.child_by_field_name("name") {
3362 if let Some(value_node) = child.child_by_field_name("value") {
3363 if matches!(
3364 value_node.kind(),
3365 "arrow_function"
3366 | "function"
3367 | "function_expression"
3368 | "generator_function"
3369 ) {
3370 let var_name = name_node.utf8_text(source).unwrap_or("").to_string();
3371 functions.push((var_name, value_node));
3372 }
3373 }
3374 }
3375 }
3376 }
3377 }
3378
3379 let mut cursor = node.walk();
3380 for child in node.children(&mut cursor) {
3381 collect_functions(child, source, functions, patterns);
3382 }
3383}
3384
3385fn analyze_function_with_lang(
3390 func_node: Node,
3391 source: &[u8],
3392 args: &ResourcesArgs,
3393 lang: Language,
3394) -> (
3395 Vec<ResourceInfo>,
3396 Vec<LeakInfo>,
3397 Vec<DoubleCloseInfo>,
3398 Vec<UseAfterCloseInfo>,
3399) {
3400 let check_leaks = args.check_leaks || args.check_all;
3401 let check_double_close = args.check_double_close || args.check_all;
3402 let check_use_after_close = args.check_use_after_close || args.check_all;
3403 let mut detector = ResourceDetector::with_language(lang);
3405 let resources = detector.detect_with_patterns(func_node, source);
3406
3407 let leaks = if check_leaks {
3409 let cfg = build_cfg_multilang(func_node, source, lang);
3410 let mut leak_detector = LeakDetector::new();
3411 leak_detector.detect_multilang(&cfg, &resources, source, args.show_paths)
3412 } else {
3413 Vec::new()
3414 };
3415
3416 let double_closes = if check_double_close {
3418 let detector = DoubleCloseDetector::with_language(lang);
3419 detector.detect_multilang(func_node, source)
3420 } else {
3421 Vec::new()
3422 };
3423
3424 let use_after_closes = if check_use_after_close {
3426 let detector = UseAfterCloseDetector::with_language(lang);
3427 detector.detect_multilang(func_node, source)
3428 } else {
3429 Vec::new()
3430 };
3431
3432 (resources, leaks, double_closes, use_after_closes)
3433}
3434
3435pub fn run(args: ResourcesArgs, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
3441 let start_time = Instant::now();
3442
3443 let path = if let Some(ref root) = args.project_root {
3445 validate_file_path_in_project(&args.file, root)?
3446 } else {
3447 validate_file_path(&args.file)?
3448 };
3449
3450 let source = read_file_safe(&path)?;
3452 let source_bytes = source.as_bytes();
3453
3454 let lang: Language = match args.lang {
3456 Some(l) => l,
3457 None => Language::from_path(&path).ok_or_else(|| {
3458 let ext = path
3459 .extension()
3460 .and_then(|e| e.to_str())
3461 .unwrap_or("unknown")
3462 .to_string();
3463 PatternsError::UnsupportedLanguage { language: ext }
3464 })?,
3465 };
3466
3467 let mut parser = get_parser_for_language(lang)?;
3469 let tree = parser
3470 .parse(&source, None)
3471 .ok_or_else(|| PatternsError::ParseError {
3472 file: path.clone(),
3473 message: format!("Failed to parse {} file", lang.as_str()),
3474 })?;
3475
3476 let mut all_resources = Vec::new();
3478 let mut all_leaks = Vec::new();
3479 let mut all_double_closes = Vec::new();
3480 let mut all_use_after_closes = Vec::new();
3481
3482 if let Some(ref func_name) = args.function {
3483 if let Some(func_node) = find_function_node_multilang(&tree, func_name, source_bytes, lang)
3485 {
3486 let (resources, leaks, double_closes, use_after_closes) =
3487 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3488 all_resources = resources;
3489 all_leaks = leaks;
3490 all_double_closes = double_closes;
3491 all_use_after_closes = use_after_closes;
3492 } else {
3493 return Err(PatternsError::FunctionNotFound {
3494 function: func_name.clone(),
3495 file: path.clone(),
3496 }
3497 .into());
3498 }
3499 } else {
3500 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3502 for (_name, func_node) in functions {
3503 let (resources, leaks, double_closes, use_after_closes) =
3504 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3505 all_resources.extend(resources);
3506 all_leaks.extend(leaks);
3507 all_double_closes.extend(double_closes);
3508 all_use_after_closes.extend(use_after_closes);
3509 }
3510 }
3511
3512 let suggestions = if args.suggest_context {
3514 suggest_context_manager_multilang(&all_resources, lang)
3515 } else {
3516 Vec::new()
3517 };
3518
3519 let constraints = if args.constraints {
3521 generate_constraints(
3522 path.to_str().unwrap_or(""),
3523 args.function.as_deref(),
3524 &all_resources,
3525 &all_leaks,
3526 &all_double_closes,
3527 &all_use_after_closes,
3528 )
3529 } else {
3530 Vec::new()
3531 };
3532
3533 let summary = ResourceSummary {
3535 resources_detected: all_resources.len() as u32,
3536 leaks_found: all_leaks.len() as u32,
3537 double_closes_found: all_double_closes.len() as u32,
3538 use_after_closes_found: all_use_after_closes.len() as u32,
3539 };
3540
3541 let elapsed_ms = start_time.elapsed().as_millis() as u64;
3542
3543 let report = ResourceReport {
3545 file: path.to_string_lossy().to_string(),
3546 language: lang.as_str().to_string(),
3547 function: args.function.clone(),
3548 resources: all_resources,
3549 leaks: all_leaks,
3550 double_closes: all_double_closes,
3551 use_after_closes: all_use_after_closes,
3552 suggestions,
3553 constraints,
3554 summary,
3555 analysis_time_ms: elapsed_ms,
3556 };
3557
3558 let use_text = matches!(global_format, GlobalOutputFormat::Text)
3560 || matches!(args.output_format, OutputFormat::Text);
3561 let output = if use_text {
3562 format_resources_text(&report)
3563 } else {
3564 serde_json::to_string_pretty(&report)?
3565 };
3566
3567 println!("{}", output);
3568
3569 let has_issues = report.summary.leaks_found > 0
3571 || report.summary.double_closes_found > 0
3572 || report.summary.use_after_closes_found > 0;
3573
3574 if has_issues {
3575 std::process::exit(3);
3576 }
3577
3578 Ok(())
3579}
3580
3581pub struct ResourceAnalysisResults {
3590 pub leaks: Vec<(String, LeakInfo)>,
3592 pub double_closes: Vec<(String, DoubleCloseInfo)>,
3594 pub use_after_closes: Vec<(String, UseAfterCloseInfo)>,
3596}
3597
3598pub fn analyze_source_for_resource_issues(
3614 source: &str,
3615 lang: Language,
3616) -> PatternsResult<ResourceAnalysisResults> {
3617 let source_bytes = source.as_bytes();
3618
3619 let mut parser = get_parser_for_language(lang)?;
3621 let tree = parser
3622 .parse(source, None)
3623 .ok_or_else(|| PatternsError::ParseError {
3624 file: PathBuf::from("<in-memory>"),
3625 message: format!(
3626 "Failed to parse {} source for resource analysis",
3627 lang.as_str()
3628 ),
3629 })?;
3630
3631 let args = ResourcesArgs {
3633 file: PathBuf::from("<in-memory>"),
3634 function: None,
3635 lang: Some(lang),
3636 check_leaks: true,
3637 check_double_close: true,
3638 check_use_after_close: true,
3639 check_all: true,
3640 suggest_context: false,
3641 show_paths: false,
3642 constraints: false,
3643 summary: false,
3644 output_format: OutputFormat::Json,
3645 project_root: None,
3646 };
3647
3648 let mut all_leaks = Vec::new();
3649 let mut all_double_closes = Vec::new();
3650 let mut all_use_after_closes = Vec::new();
3651
3652 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3654 for (func_name, func_node) in functions {
3655 let (_resources, leaks, double_closes, use_after_closes) =
3656 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3657
3658 for leak in leaks {
3659 all_leaks.push((func_name.clone(), leak));
3660 }
3661 for dc in double_closes {
3662 all_double_closes.push((func_name.clone(), dc));
3663 }
3664 for uac in use_after_closes {
3665 all_use_after_closes.push((func_name.clone(), uac));
3666 }
3667 }
3668
3669 Ok(ResourceAnalysisResults {
3670 leaks: all_leaks,
3671 double_closes: all_double_closes,
3672 use_after_closes: all_use_after_closes,
3673 })
3674}
3675
3676#[cfg(test)]
3681mod tests {
3682 use super::*;
3683
3684 const TEST_LEAKY_FUNCTION: &str = r#"
3685def leaky_function(path):
3686 f = open(path)
3687 if some_condition():
3688 return None
3689 content = f.read()
3690 f.close()
3691 return content
3692"#;
3693
3694 const TEST_SAFE_WITH_CONTEXT: &str = r#"
3695def safe_with_context(path):
3696 with open(path) as f:
3697 return f.read()
3698"#;
3699
3700 const TEST_DOUBLE_CLOSE: &str = r#"
3701def double_close(path):
3702 f = open(path)
3703 content = f.read()
3704 f.close()
3705 f.close()
3706 return content
3707"#;
3708
3709 const TEST_USE_AFTER_CLOSE: &str = r#"
3710def use_after_close(path):
3711 f = open(path)
3712 f.close()
3713 content = f.read()
3714 return content
3715"#;
3716
3717 #[test]
3718 fn test_resource_creators_constant() {
3719 assert!(RESOURCE_CREATORS.contains(&"open"));
3720 assert!(RESOURCE_CREATORS.contains(&"socket"));
3721 assert!(RESOURCE_CREATORS.contains(&"connect"));
3722 assert!(RESOURCE_CREATORS.contains(&"cursor"));
3723 }
3724
3725 #[test]
3726 fn test_resource_closers_constant() {
3727 assert!(RESOURCE_CLOSERS.contains(&"close"));
3728 assert!(RESOURCE_CLOSERS.contains(&"shutdown"));
3729 assert!(RESOURCE_CLOSERS.contains(&"disconnect"));
3730 }
3731
3732 #[test]
3733 fn test_max_paths_constant() {
3734 assert_eq!(MAX_PATHS, 1000);
3735 }
3736
3737 #[test]
3738 fn test_resource_detector_finds_open() {
3739 let mut parser = get_python_parser().unwrap();
3740 let tree = parser.parse(TEST_LEAKY_FUNCTION, None).unwrap();
3741 let source = TEST_LEAKY_FUNCTION.as_bytes();
3742
3743 let func_node = find_function_node(&tree, "leaky_function", source).unwrap();
3744 let mut detector = ResourceDetector::new();
3745 let resources = detector.detect(func_node, source);
3746
3747 assert_eq!(resources.len(), 1);
3748 assert_eq!(resources[0].name, "f");
3749 assert_eq!(resources[0].resource_type, "file");
3750 assert!(!resources[0].closed);
3751 }
3752
3753 #[test]
3754 fn test_resource_detector_context_manager() {
3755 let mut parser = get_python_parser().unwrap();
3756 let tree = parser.parse(TEST_SAFE_WITH_CONTEXT, None).unwrap();
3757 let source = TEST_SAFE_WITH_CONTEXT.as_bytes();
3758
3759 let func_node = find_function_node(&tree, "safe_with_context", source).unwrap();
3760 let mut detector = ResourceDetector::new();
3761 let resources = detector.detect(func_node, source);
3762
3763 assert_eq!(resources.len(), 1);
3764 assert!(
3765 resources[0].closed,
3766 "Context manager resource should be marked as closed"
3767 );
3768 }
3769
3770 #[test]
3771 fn test_double_close_detector() {
3772 let mut parser = get_python_parser().unwrap();
3773 let tree = parser.parse(TEST_DOUBLE_CLOSE, None).unwrap();
3774 let source = TEST_DOUBLE_CLOSE.as_bytes();
3775
3776 let func_node = find_function_node(&tree, "double_close", source).unwrap();
3777 let detector = DoubleCloseDetector::new();
3778 let issues = detector.detect(func_node, source);
3779
3780 assert_eq!(issues.len(), 1);
3781 assert_eq!(issues[0].resource, "f");
3782 }
3783
3784 #[test]
3785 fn test_use_after_close_detector() {
3786 let mut parser = get_python_parser().unwrap();
3787 let tree = parser.parse(TEST_USE_AFTER_CLOSE, None).unwrap();
3788 let source = TEST_USE_AFTER_CLOSE.as_bytes();
3789
3790 let func_node = find_function_node(&tree, "use_after_close", source).unwrap();
3791 let detector = UseAfterCloseDetector::new();
3792 let issues = detector.detect(func_node, source);
3793
3794 assert!(!issues.is_empty());
3795 assert_eq!(issues[0].resource, "f");
3796 }
3797
3798 #[test]
3799 fn test_suggest_context_manager() {
3800 let resources = vec![ResourceInfo {
3801 name: "f".to_string(),
3802 resource_type: "file".to_string(),
3803 line: 2,
3804 closed: false,
3805 }];
3806
3807 let suggestions = suggest_context_manager(&resources);
3808 assert_eq!(suggestions.len(), 1);
3809 assert!(suggestions[0].suggestion.contains("with open"));
3810 }
3811
3812 #[test]
3813 fn test_generate_constraints_for_leak() {
3814 let resources = vec![ResourceInfo {
3815 name: "f".to_string(),
3816 resource_type: "file".to_string(),
3817 line: 2,
3818 closed: false,
3819 }];
3820 let leaks = vec![LeakInfo {
3821 resource: "f".to_string(),
3822 line: 2,
3823 paths: None,
3824 }];
3825
3826 let constraints =
3827 generate_constraints("test.py", Some("test_func"), &resources, &leaks, &[], &[]);
3828
3829 assert!(!constraints.is_empty());
3830 assert!(constraints[0].rule.contains("must be closed"));
3831 }
3832
3833 #[test]
3834 fn test_leak_detector_path_limit() {
3835 let detector = LeakDetector::new();
3836 assert_eq!(detector.max_paths, MAX_PATHS);
3837 }
3838
3839 #[test]
3840 fn test_cfg_builder_basic() {
3841 let mut parser = get_python_parser().unwrap();
3842 let source = r#"
3843def simple():
3844 x = 1
3845 return x
3846"#;
3847 let tree = parser.parse(source, None).unwrap();
3848 let func_node = find_function_node(&tree, "simple", source.as_bytes()).unwrap();
3849 let cfg = build_cfg(func_node, source.as_bytes());
3850
3851 assert!(!cfg.blocks.is_empty());
3852 assert!(!cfg.exit_blocks.is_empty());
3853 }
3854
3855 #[test]
3856 fn test_cfg_builder_with_if() {
3857 let mut parser = get_python_parser().unwrap();
3858 let source = r#"
3859def with_if(x):
3860 if x > 0:
3861 return x
3862 return -x
3863"#;
3864 let tree = parser.parse(source, None).unwrap();
3865 let func_node = find_function_node(&tree, "with_if", source.as_bytes()).unwrap();
3866 let cfg = build_cfg(func_node, source.as_bytes());
3867
3868 assert!(cfg.blocks.len() > 1);
3870 }
3871
3872 #[test]
3873 fn test_format_resources_text() {
3874 let report = ResourceReport {
3875 file: "test.py".to_string(),
3876 language: "python".to_string(),
3877 function: Some("test".to_string()),
3878 resources: vec![ResourceInfo {
3879 name: "f".to_string(),
3880 resource_type: "file".to_string(),
3881 line: 2,
3882 closed: false,
3883 }],
3884 leaks: vec![],
3885 double_closes: vec![],
3886 use_after_closes: vec![],
3887 suggestions: vec![],
3888 constraints: vec![],
3889 summary: ResourceSummary::default(),
3890 analysis_time_ms: 10,
3891 };
3892
3893 let text = format_resources_text(&report);
3894 assert!(text.contains("Resource Analysis: test.py"));
3895 assert!(text.contains("Function: test"));
3896 assert!(text.contains("file"));
3897 }
3898
3899 #[test]
3900 fn test_find_ts_arrow_function_resources() {
3901 let ts_source = r#"
3902const getDuration = (start: Date, end: Date): number => {
3903 const conn = createConnection();
3904 const result = end.getTime() - start.getTime();
3905 conn.close();
3906 return result;
3907};
3908
3909function regularFunc(x: number): number {
3910 return x * 2;
3911}
3912"#;
3913 let tree = tldr_core::ast::parser::parse(ts_source, Language::TypeScript).unwrap();
3914 let source_bytes = ts_source.as_bytes();
3915
3916 let regular =
3918 find_function_node_multilang(&tree, "regularFunc", source_bytes, Language::TypeScript);
3919 assert!(regular.is_some(), "Should find regular TS function");
3920
3921 let arrow =
3923 find_function_node_multilang(&tree, "getDuration", source_bytes, Language::TypeScript);
3924 assert!(
3925 arrow.is_some(),
3926 "Should find TS arrow function 'getDuration'"
3927 );
3928 }
3929
3930 #[test]
3931 fn test_resources_args_lang_flag() {
3932 let args = ResourcesArgs {
3934 file: PathBuf::from("src/db.go"),
3935 function: None,
3936 lang: Some(Language::Go),
3937 check_leaks: true,
3938 check_double_close: false,
3939 check_use_after_close: false,
3940 check_all: false,
3941 suggest_context: false,
3942 show_paths: false,
3943 constraints: false,
3944 summary: false,
3945 output_format: OutputFormat::Json,
3946 project_root: None,
3947 };
3948 assert_eq!(args.lang, Some(Language::Go));
3949
3950 let args_auto = ResourcesArgs {
3952 file: PathBuf::from("src/db.py"),
3953 function: None,
3954 lang: None,
3955 check_leaks: true,
3956 check_double_close: false,
3957 check_use_after_close: false,
3958 check_all: false,
3959 suggest_context: false,
3960 show_paths: false,
3961 constraints: false,
3962 summary: false,
3963 output_format: OutputFormat::Json,
3964 project_root: None,
3965 };
3966 assert_eq!(args_auto.lang, None);
3967 }
3968}