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
1176const TS_JS_AMBIGUOUS_NAMES: &[&str] = &["event", "request", "response", "data"];
1186
1187const TS_JS_CLEANUP_METHODS: &[&str] = &[
1190 "close",
1191 "destroy",
1192 "end",
1193 "abort",
1194 "disconnect",
1195 "release",
1196 "unref",
1197 "removeListener",
1198 "removeAllListeners",
1199 "removeEventListener",
1200 "unsubscribe",
1201 "cancel",
1202];
1203
1204fn collect_ts_js_cleanup_vars(func_node: Node, source: &[u8]) -> HashSet<String> {
1208 let mut out: HashSet<String> = HashSet::new();
1209 fn visit(node: Node, source: &[u8], out: &mut HashSet<String>) {
1210 if node.kind() == "call_expression" {
1213 if let Some(func) = node.child_by_field_name("function") {
1214 if func.kind() == "member_expression" {
1215 let object = func.child_by_field_name("object");
1216 let property = func.child_by_field_name("property");
1217 if let (Some(obj), Some(prop)) = (object, property) {
1218 if obj.kind() == "identifier" {
1219 let prop_text = node_text(prop, source);
1220 if TS_JS_CLEANUP_METHODS.contains(&prop_text) {
1221 out.insert(node_text(obj, source).to_string());
1222 }
1223 }
1224 }
1225 }
1226 }
1227 }
1228 let mut cursor = node.walk();
1229 for child in node.children(&mut cursor) {
1230 visit(child, source, out);
1231 }
1232 }
1233 visit(func_node, source, &mut out);
1234 out
1235}
1236
1237pub struct ResourceDetector {
1239 resources: Vec<DetectedResource>,
1240 context_manager_vars: HashSet<String>,
1241 ts_js_cleanup_vars: HashSet<String>,
1245 lang: Language,
1246}
1247
1248impl ResourceDetector {
1249 pub fn new() -> Self {
1250 Self {
1251 resources: Vec::new(),
1252 context_manager_vars: HashSet::new(),
1253 ts_js_cleanup_vars: HashSet::new(),
1254 lang: Language::Python,
1255 }
1256 }
1257
1258 pub fn with_language(lang: Language) -> Self {
1259 Self {
1260 resources: Vec::new(),
1261 context_manager_vars: HashSet::new(),
1262 ts_js_cleanup_vars: HashSet::new(),
1263 lang,
1264 }
1265 }
1266
1267 fn ts_js_should_skip_ambiguous(&self, var_name: &str) -> bool {
1271 if !matches!(self.lang, Language::TypeScript | Language::JavaScript) {
1272 return false;
1273 }
1274 if !TS_JS_AMBIGUOUS_NAMES.contains(&var_name) {
1275 return false;
1276 }
1277 !self.ts_js_cleanup_vars.contains(var_name)
1278 }
1279
1280 pub fn detect(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1282 self.resources.clear();
1283 self.context_manager_vars.clear();
1284 self.visit_node(func_node, source, false);
1285
1286 self.resources
1287 .iter()
1288 .map(|r| ResourceInfo {
1289 name: r.name.clone(),
1290 resource_type: r.resource_type.clone(),
1291 line: r.line,
1292 closed: r.in_context_manager,
1293 })
1294 .collect()
1295 }
1296
1297 pub fn detect_with_patterns(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1299 let patterns = get_resource_patterns(self.lang);
1300 self.resources.clear();
1301 self.context_manager_vars.clear();
1302 self.ts_js_cleanup_vars.clear();
1303 if matches!(self.lang, Language::TypeScript | Language::JavaScript) {
1306 self.ts_js_cleanup_vars = collect_ts_js_cleanup_vars(func_node, source);
1307 }
1308 self.visit_node_multilang(func_node, source, false, &patterns);
1309
1310 self.resources
1311 .iter()
1312 .map(|r| ResourceInfo {
1313 name: r.name.clone(),
1314 resource_type: r.resource_type.clone(),
1315 line: r.line,
1316 closed: r.in_context_manager,
1317 })
1318 .collect()
1319 }
1320
1321 fn visit_node(&mut self, node: Node, source: &[u8], in_with: bool) {
1322 match node.kind() {
1323 "with_statement" => {
1324 let mut cursor = node.walk();
1326 for child in node.children(&mut cursor) {
1327 if child.kind() == "with_item" {
1328 self.visit_with_item(child, source);
1329 } else if child.kind() == "with_clause" {
1330 let mut inner_cursor = child.walk();
1332 for item in child.children(&mut inner_cursor) {
1333 if item.kind() == "with_item" {
1334 self.visit_with_item(item, source);
1335 }
1336 }
1337 }
1338 }
1339 let mut cursor = node.walk();
1341 for child in node.children(&mut cursor) {
1342 self.visit_node(child, source, true);
1343 }
1344 }
1345 "assignment" => {
1346 self.check_assignment(node, source, in_with);
1347 }
1348 _ => {
1349 let mut cursor = node.walk();
1351 for child in node.children(&mut cursor) {
1352 self.visit_node(child, source, in_with);
1353 }
1354 }
1355 }
1356 }
1357
1358 fn visit_with_item(&mut self, node: Node, source: &[u8]) {
1359 let mut cursor = node.walk();
1372 for child in node.children(&mut cursor) {
1373 if child.kind() == "as_pattern" {
1374 let mut as_cursor = child.walk();
1375 let mut call_node: Option<Node> = None;
1376 let mut target_node: Option<Node> = None;
1377
1378 for as_child in child.children(&mut as_cursor) {
1379 if as_child.kind() == "call" {
1380 call_node = Some(as_child);
1381 } else if as_child.kind() == "as_pattern_target" {
1382 if let Some(ident) = as_child.child(0) {
1384 if ident.kind() == "identifier" {
1385 target_node = Some(ident);
1386 }
1387 }
1388 }
1389 }
1390
1391 if let (Some(call), Some(target)) = (call_node, target_node) {
1392 let var_name = node_text(target, source).to_string();
1393 self.context_manager_vars.insert(var_name.clone());
1394
1395 if let Some(resource_type) = self.get_resource_type_from_call(call, source) {
1396 self.resources.push(DetectedResource {
1397 name: var_name,
1398 resource_type,
1399 line: node.start_position().row as u32 + 1,
1400 in_context_manager: true,
1401 });
1402 }
1403 }
1404 }
1405 }
1406
1407 if let Some(target) = node.child_by_field_name("alias") {
1409 let var_name = node_text(target, source).to_string();
1410 if !self.context_manager_vars.contains(&var_name) {
1411 self.context_manager_vars.insert(var_name.clone());
1412
1413 if let Some(value) = node.child_by_field_name("value") {
1414 if let Some(resource_type) = self.get_resource_type_from_call(value, source) {
1415 self.resources.push(DetectedResource {
1416 name: var_name,
1417 resource_type,
1418 line: node.start_position().row as u32 + 1,
1419 in_context_manager: true,
1420 });
1421 }
1422 }
1423 }
1424 }
1425 }
1426
1427 fn check_assignment(&mut self, node: Node, source: &[u8], in_with: bool) {
1428 if let Some(left) = node.child_by_field_name("left") {
1430 if let Some(right) = node.child_by_field_name("right") {
1431 let var_name = node_text(left, source).to_string();
1432
1433 if let Some(resource_type) = self.get_resource_type_from_call(right, source) {
1434 let in_context = in_with || self.context_manager_vars.contains(&var_name);
1435 self.resources.push(DetectedResource {
1436 name: var_name,
1437 resource_type,
1438 line: node.start_position().row as u32 + 1,
1439 in_context_manager: in_context,
1440 });
1441 }
1442 }
1443 }
1444 }
1445
1446 fn get_resource_type_from_call(&self, node: Node, source: &[u8]) -> Option<String> {
1447 if node.kind() != "call" {
1448 return None;
1449 }
1450
1451 let func = node.child_by_field_name("function")?;
1453 let func_text = node_text(func, source);
1454
1455 let func_name = func_text.split('.').next_back().unwrap_or(func_text);
1457
1458 for &creator in RESOURCE_CREATORS {
1460 if func_name == creator {
1461 for &(name, rtype) in RESOURCE_TYPE_MAP {
1463 if func_name == name {
1464 return Some(rtype.to_string());
1465 }
1466 }
1467 return Some(func_name.to_string());
1469 }
1470 }
1471
1472 None
1473 }
1474
1475 fn visit_node_multilang(
1480 &mut self,
1481 node: Node,
1482 source: &[u8],
1483 in_cleanup: bool,
1484 patterns: &LangResourcePatterns,
1485 ) {
1486 let kind = node.kind();
1487
1488 if patterns.cleanup_block_kinds.contains(&kind) {
1490 match self.lang {
1491 Language::Python => {
1492 let mut cursor = node.walk();
1494 for child in node.children(&mut cursor) {
1495 if child.kind() == "with_item" {
1496 self.visit_with_item(child, source);
1497 } else if child.kind() == "with_clause" {
1498 let mut inner_cursor = child.walk();
1499 for item in child.children(&mut inner_cursor) {
1500 if item.kind() == "with_item" {
1501 self.visit_with_item(item, source);
1502 }
1503 }
1504 }
1505 }
1506 let mut cursor = node.walk();
1508 for child in node.children(&mut cursor) {
1509 self.visit_node_multilang(child, source, true, patterns);
1510 }
1511 return;
1512 }
1513 Language::Go => {
1514 let mut cursor = node.walk();
1517 for child in node.children(&mut cursor) {
1518 self.visit_node_multilang(child, source, true, patterns);
1519 }
1520 return;
1521 }
1522 Language::CSharp => {
1523 let mut cursor = node.walk();
1525 for child in node.children(&mut cursor) {
1526 self.visit_node_multilang(child, source, true, patterns);
1527 }
1528 return;
1529 }
1530 Language::Java => {
1531 let mut cursor = node.walk();
1533 for child in node.children(&mut cursor) {
1534 self.visit_node_multilang(child, source, true, patterns);
1535 }
1536 return;
1537 }
1538 _ => {}
1539 }
1540 }
1541
1542 if patterns.assignment_kinds.contains(&kind) {
1544 self.check_assignment_multilang(node, source, in_cleanup, patterns);
1545 }
1546
1547 let mut cursor = node.walk();
1549 for child in node.children(&mut cursor) {
1550 self.visit_node_multilang(child, source, in_cleanup, patterns);
1551 }
1552 }
1553
1554 fn check_assignment_multilang(
1555 &mut self,
1556 node: Node,
1557 source: &[u8],
1558 in_cleanup: bool,
1559 patterns: &LangResourcePatterns,
1560 ) {
1561 match self.lang {
1562 Language::Python => {
1563 if let Some(left) = node.child_by_field_name("left") {
1565 if let Some(right) = node.child_by_field_name("right") {
1566 let var_name = node_text(left, source).to_string();
1567 if let Some(resource_type) =
1568 self.get_resource_type_from_call_multilang(right, source, patterns)
1569 {
1570 let in_context =
1571 in_cleanup || self.context_manager_vars.contains(&var_name);
1572 self.resources.push(DetectedResource {
1573 name: var_name,
1574 resource_type,
1575 line: node.start_position().row as u32 + 1,
1576 in_context_manager: in_context,
1577 });
1578 }
1579 }
1580 }
1581 }
1582 Language::Go => {
1583 if let Some(left) = node.child_by_field_name("left") {
1587 if let Some(right) = node.child_by_field_name("right") {
1588 let var_name = if left.kind() == "expression_list" {
1590 left.child(0).map(|c| node_text(c, source).to_string())
1592 } else {
1593 Some(node_text(left, source).to_string())
1594 };
1595 if let Some(var_name) = var_name {
1596 if var_name != "_" && var_name != "err" {
1597 let call_node = if right.kind() == "expression_list" {
1599 right.child(0)
1600 } else {
1601 Some(right)
1602 };
1603 if let Some(call_node) = call_node {
1604 if let Some(resource_type) = self
1605 .get_resource_type_from_call_multilang(
1606 call_node, source, patterns,
1607 )
1608 {
1609 self.resources.push(DetectedResource {
1610 name: var_name,
1611 resource_type,
1612 line: node.start_position().row as u32 + 1,
1613 in_context_manager: in_cleanup,
1614 });
1615 }
1616 }
1617 }
1618 }
1619 }
1620 }
1621 }
1622 Language::Rust => {
1623 if let Some(pattern) = node.child_by_field_name("pattern") {
1626 if let Some(value) = node.child_by_field_name("value") {
1627 let var_name = node_text(pattern, source).to_string();
1628 if let Some(resource_type) =
1631 self.get_resource_type_from_call_multilang(value, source, patterns)
1632 {
1633 self.resources.push(DetectedResource {
1634 name: var_name,
1635 resource_type,
1636 line: node.start_position().row as u32 + 1,
1637 in_context_manager: true, });
1639 }
1640 }
1641 }
1642 }
1643 Language::Java | Language::CSharp => {
1644 let mut cursor = node.walk();
1647 for child in node.children(&mut cursor) {
1648 if child.kind() == "variable_declarator" {
1649 if let Some(name_node) = child.child_by_field_name("name") {
1650 if let Some(value) = child.child_by_field_name("value") {
1651 let var_name = node_text(name_node, source).to_string();
1652 if let Some(resource_type) = self
1653 .get_resource_type_from_call_multilang(value, source, patterns)
1654 {
1655 self.resources.push(DetectedResource {
1656 name: var_name,
1657 resource_type,
1658 line: node.start_position().row as u32 + 1,
1659 in_context_manager: in_cleanup,
1660 });
1661 }
1662 }
1663 }
1664 }
1665 }
1666 }
1667 Language::TypeScript | Language::JavaScript => {
1668 let mut cursor = node.walk();
1671 for child in node.children(&mut cursor) {
1672 if child.kind() == "variable_declarator" {
1673 if let Some(name_node) = child.child_by_field_name("name") {
1674 if let Some(value) = child.child_by_field_name("value") {
1675 let var_name = node_text(name_node, source).to_string();
1676 if let Some(resource_type) = self
1677 .get_resource_type_from_call_multilang(value, source, patterns)
1678 {
1679 if self.ts_js_should_skip_ambiguous(&var_name) {
1682 continue;
1683 }
1684 self.resources.push(DetectedResource {
1685 name: var_name,
1686 resource_type,
1687 line: node.start_position().row as u32 + 1,
1688 in_context_manager: in_cleanup,
1689 });
1690 }
1691 }
1692 }
1693 }
1694 }
1695 if node.kind() == "assignment_expression" {
1697 if let Some(left) = node.child_by_field_name("left") {
1698 if let Some(right) = node.child_by_field_name("right") {
1699 let var_name = node_text(left, source).to_string();
1700 if let Some(resource_type) =
1701 self.get_resource_type_from_call_multilang(right, source, patterns)
1702 {
1703 if self.ts_js_should_skip_ambiguous(&var_name) {
1706 return;
1707 }
1708 self.resources.push(DetectedResource {
1709 name: var_name,
1710 resource_type,
1711 line: node.start_position().row as u32 + 1,
1712 in_context_manager: in_cleanup,
1713 });
1714 }
1715 }
1716 }
1717 }
1718 }
1719 Language::C | Language::Cpp => {
1720 let mut cursor = node.walk();
1723 for child in node.children(&mut cursor) {
1724 if child.kind() == "init_declarator" {
1725 if let Some(declarator) = child.child_by_field_name("declarator") {
1726 if let Some(value) = child.child_by_field_name("value") {
1727 let var_name = extract_c_declarator_name(declarator, source);
1729 if let Some(var_name) = var_name {
1730 if let Some(resource_type) = self
1731 .get_resource_type_from_call_multilang(
1732 value, source, patterns,
1733 )
1734 {
1735 self.resources.push(DetectedResource {
1736 name: var_name,
1737 resource_type,
1738 line: node.start_position().row as u32 + 1,
1739 in_context_manager: in_cleanup,
1740 });
1741 }
1742 }
1743 }
1744 }
1745 }
1746 }
1747 if node.kind() == "assignment_expression" {
1749 if let Some(left) = node.child_by_field_name("left") {
1750 if let Some(right) = node.child_by_field_name("right") {
1751 let var_name = node_text(left, source).to_string();
1752 if let Some(resource_type) =
1753 self.get_resource_type_from_call_multilang(right, source, patterns)
1754 {
1755 self.resources.push(DetectedResource {
1756 name: var_name,
1757 resource_type,
1758 line: node.start_position().row as u32 + 1,
1759 in_context_manager: in_cleanup,
1760 });
1761 }
1762 }
1763 }
1764 }
1765 }
1766 Language::Kotlin => {
1767 if node.kind() == "property_declaration" {
1771 let mut cursor = node.walk();
1772 for child in node.children(&mut cursor) {
1773 if child.kind() == "variable_declaration" {
1774 if let Some(name_node) =
1775 child.child_by_field_name("name").or_else(|| child.child(0))
1776 {
1777 let var_name = node_text(name_node, source).to_string();
1778 let mut inner_cursor = node.walk();
1782 for sibling in node.children(&mut inner_cursor) {
1783 if let Some(resource_type) = self
1784 .get_resource_type_from_call_multilang(
1785 sibling, source, patterns,
1786 )
1787 {
1788 self.resources.push(DetectedResource {
1789 name: var_name.clone(),
1790 resource_type,
1791 line: node.start_position().row as u32 + 1,
1792 in_context_manager: in_cleanup,
1793 });
1794 break;
1795 }
1796 }
1797 }
1798 }
1799 }
1800 } else if node.kind() == "assignment" {
1801 if let Some(left) = node.child_by_field_name("left").or_else(|| node.child(0)) {
1802 if let Some(right) = node.child_by_field_name("right") {
1803 let var_name = node_text(left, source).to_string();
1804 if let Some(resource_type) =
1805 self.get_resource_type_from_call_multilang(right, source, patterns)
1806 {
1807 self.resources.push(DetectedResource {
1808 name: var_name,
1809 resource_type,
1810 line: node.start_position().row as u32 + 1,
1811 in_context_manager: in_cleanup,
1812 });
1813 }
1814 }
1815 }
1816 }
1817 }
1818 Language::Swift => {
1819 if node.kind() == "property_declaration"
1822 || node.kind() == "directly_assignable_expression"
1823 {
1824 if let Some(pattern) = node
1825 .child_by_field_name("pattern")
1826 .or_else(|| node.child_by_field_name("name"))
1827 {
1828 let var_name = node_text(pattern, source).to_string();
1829 let mut cursor = node.walk();
1831 for child in node.children(&mut cursor) {
1832 if let Some(resource_type) =
1833 self.get_resource_type_from_call_multilang(child, source, patterns)
1834 {
1835 self.resources.push(DetectedResource {
1836 name: var_name.clone(),
1837 resource_type,
1838 line: node.start_position().row as u32 + 1,
1839 in_context_manager: in_cleanup,
1840 });
1841 break;
1842 }
1843 }
1844 }
1845 }
1846 }
1847 Language::Ocaml => {
1848 if node.kind() == "let_binding" {
1851 if let Some(pattern) = node.child_by_field_name("pattern") {
1852 let var_name = node_text(pattern, source).to_string();
1853 if let Some(body) = node.child_by_field_name("body") {
1855 if let Some(resource_type) =
1856 self.get_resource_type_from_call_multilang(body, source, patterns)
1857 {
1858 self.resources.push(DetectedResource {
1859 name: var_name,
1860 resource_type,
1861 line: node.start_position().row as u32 + 1,
1862 in_context_manager: in_cleanup,
1863 });
1864 }
1865 }
1866 }
1867 }
1868 }
1869 Language::Lua | Language::Luau => {
1870 if let Some(right) = node
1874 .child_by_field_name("values")
1875 .or_else(|| node.child_by_field_name("right"))
1876 {
1877 if let Some(left) = node
1878 .child_by_field_name("variables")
1879 .or_else(|| node.child_by_field_name("left"))
1880 .or_else(|| node.child_by_field_name("name"))
1881 {
1882 let var_name =
1884 if left.kind() == "variable_list" || left.kind() == "identifier_list" {
1885 left.child(0).map(|c| node_text(c, source).to_string())
1886 } else {
1887 Some(node_text(left, source).to_string())
1888 };
1889 if let Some(var_name) = var_name {
1890 let call_node = if right.kind() == "expression_list" {
1892 right.child(0)
1893 } else {
1894 Some(right)
1895 };
1896 if let Some(call_node) = call_node {
1897 if let Some(resource_type) = self
1898 .get_resource_type_from_call_multilang(
1899 call_node, source, patterns,
1900 )
1901 {
1902 self.resources.push(DetectedResource {
1903 name: var_name,
1904 resource_type,
1905 line: node.start_position().row as u32 + 1,
1906 in_context_manager: in_cleanup,
1907 });
1908 }
1909 }
1910 }
1911 }
1912 }
1913 }
1914 _ => {
1915 if let Some(left) = node.child_by_field_name("left") {
1917 if let Some(right) = node.child_by_field_name("right") {
1918 let var_name = node_text(left, source).to_string();
1919 if let Some(resource_type) =
1920 self.get_resource_type_from_call_multilang(right, source, patterns)
1921 {
1922 self.resources.push(DetectedResource {
1923 name: var_name,
1924 resource_type,
1925 line: node.start_position().row as u32 + 1,
1926 in_context_manager: in_cleanup,
1927 });
1928 }
1929 }
1930 }
1931 }
1932 }
1933 }
1934
1935 fn get_resource_type_from_call_multilang(
1937 &self,
1938 node: Node,
1939 source: &[u8],
1940 patterns: &LangResourcePatterns,
1941 ) -> Option<String> {
1942 let func_name = extract_call_name(node, source)?;
1944
1945 for &(creator, rtype) in patterns.creators {
1947 if func_name == creator
1948 || func_name.ends_with(&format!("::{}", creator))
1949 || func_name.ends_with(&format!(".{}", creator))
1950 {
1951 return Some(rtype.to_string());
1952 }
1953 }
1954
1955 if matches!(self.lang, Language::C | Language::Cpp) {
1957 if node.kind() == "call_expression" {
1958 let text = node_text(node, source);
1959 for &(creator, rtype) in patterns.creators {
1960 if text.starts_with(creator) {
1961 return Some(rtype.to_string());
1962 }
1963 }
1964 }
1965 if node.kind() == "new_expression" {
1967 return Some("heap_object".to_string());
1968 }
1969 }
1970
1971 if matches!(self.lang, Language::Kotlin) {
1973 let text = node_text(node, source);
1975 for &(creator, rtype) in patterns.creators {
1976 if text.starts_with(creator) {
1977 return Some(rtype.to_string());
1978 }
1979 }
1980 }
1981
1982 if matches!(self.lang, Language::Swift) {
1984 let text = node_text(node, source);
1985 for &(creator, rtype) in patterns.creators {
1986 if text.starts_with(creator) {
1987 return Some(rtype.to_string());
1988 }
1989 }
1990 if node.kind() == "force_unwrap_expression" || node.kind() == "try_expression" {
1992 if let Some(child) = node.child(0) {
1993 return self.get_resource_type_from_call_multilang(child, source, patterns);
1994 }
1995 }
1996 }
1997
1998 if matches!(self.lang, Language::Ocaml) {
2000 if node.kind() == "application" {
2002 if let Some(func_node) = node
2003 .child_by_field_name("function")
2004 .or_else(|| node.child(0))
2005 {
2006 let func_text = node_text(func_node, source);
2007 for &(creator, rtype) in patterns.creators {
2008 if func_text == creator || func_text.ends_with(&format!(".{}", creator)) {
2009 return Some(rtype.to_string());
2010 }
2011 }
2012 }
2013 }
2014 let text = node_text(node, source);
2016 let first_word = text.split_whitespace().next().unwrap_or("");
2017 for &(creator, rtype) in patterns.creators {
2018 if first_word == creator {
2019 return Some(rtype.to_string());
2020 }
2021 }
2022 }
2023
2024 if matches!(self.lang, Language::Lua | Language::Luau) {
2026 let text = node_text(node, source);
2027 for &(creator, rtype) in patterns.creators {
2028 if text.starts_with(creator) {
2029 return Some(rtype.to_string());
2030 }
2031 }
2032 }
2033
2034 if matches!(self.lang, Language::Java | Language::CSharp)
2036 && node.kind() == "object_creation_expression"
2037 {
2038 if let Some(type_node) = node.child_by_field_name("type") {
2040 let type_name = node_text(type_node, source);
2041 for &(creator, rtype) in patterns.creators {
2042 if type_name == creator || type_name.contains(creator) {
2043 return Some(rtype.to_string());
2044 }
2045 }
2046 }
2047 }
2048
2049 None
2050 }
2051}
2052
2053impl Default for ResourceDetector {
2054 fn default() -> Self {
2055 Self::new()
2056 }
2057}
2058
2059pub struct LeakDetector {
2065 max_paths: usize,
2067 paths_enumerated: usize,
2069 hit_limit: bool,
2071}
2072
2073impl LeakDetector {
2074 pub fn new() -> Self {
2075 Self {
2076 max_paths: MAX_PATHS,
2077 paths_enumerated: 0,
2078 hit_limit: false,
2079 }
2080 }
2081
2082 pub fn detect(
2084 &mut self,
2085 cfg: &SimpleCfg,
2086 resources: &[ResourceInfo],
2087 source: &[u8],
2088 show_paths: bool,
2089 ) -> Vec<LeakInfo> {
2090 let mut leaks = Vec::new();
2091 self.paths_enumerated = 0;
2092 self.hit_limit = false;
2093
2094 for resource in resources {
2095 if resource.closed {
2097 continue;
2098 }
2099
2100 let paths = self.enumerate_paths(cfg, resource, source);
2102
2103 for path in &paths {
2105 if !self.path_has_close(path, &resource.name) {
2106 leaks.push(LeakInfo {
2107 resource: resource.name.clone(),
2108 line: resource.line,
2109 paths: if show_paths {
2110 Some(vec![self.format_path(path)])
2111 } else {
2112 None
2113 },
2114 });
2115 break; }
2117 }
2118 }
2119
2120 leaks
2121 }
2122
2123 pub fn detect_multilang(
2126 &mut self,
2127 cfg: &SimpleCfg,
2128 resources: &[ResourceInfo],
2129 source: &[u8],
2130 show_paths: bool,
2131 ) -> Vec<LeakInfo> {
2132 self.detect(cfg, resources, source, show_paths)
2133 }
2134
2135 fn enumerate_paths(
2137 &mut self,
2138 cfg: &SimpleCfg,
2139 resource: &ResourceInfo,
2140 _source: &[u8],
2141 ) -> Vec<Vec<usize>> {
2142 let mut paths = Vec::new();
2143
2144 let start_block = self.find_block_with_line(cfg, resource.line);
2146 if start_block.is_none() {
2147 return paths;
2148 }
2149 let start = start_block.unwrap();
2150
2151 for &exit_id in &cfg.exit_blocks {
2153 if self.hit_limit {
2154 break;
2155 }
2156 self.find_paths_dfs(cfg, start, exit_id, &mut Vec::new(), &mut paths);
2157 }
2158
2159 paths
2160 }
2161
2162 fn find_block_with_line(&self, cfg: &SimpleCfg, line: u32) -> Option<usize> {
2163 for (id, block) in &cfg.blocks {
2164 if block.lines.contains(&line) {
2165 return Some(*id);
2166 }
2167 }
2168 Some(cfg.entry_block)
2170 }
2171
2172 fn find_paths_dfs(
2173 &mut self,
2174 cfg: &SimpleCfg,
2175 current: usize,
2176 target: usize,
2177 current_path: &mut Vec<usize>,
2178 paths: &mut Vec<Vec<usize>>,
2179 ) {
2180 if self.paths_enumerated >= self.max_paths {
2182 self.hit_limit = true;
2183 return;
2184 }
2185
2186 if current_path.contains(¤t) {
2188 return;
2189 }
2190
2191 current_path.push(current);
2192
2193 if current == target {
2194 paths.push(current_path.clone());
2195 self.paths_enumerated += 1;
2196 } else if let Some(block) = cfg.blocks.get(¤t) {
2197 for &succ in &block.succs {
2198 self.find_paths_dfs(cfg, succ, target, current_path, paths);
2199 if self.hit_limit {
2200 break;
2201 }
2202 }
2203 }
2204
2205 current_path.pop();
2206 }
2207
2208 fn path_has_close(&self, path: &[usize], resource_name: &str) -> bool {
2209 let _ = (path, resource_name);
2214 false
2215 }
2216
2217 fn format_path(&self, path: &[usize]) -> String {
2218 path.iter()
2219 .map(|id| id.to_string())
2220 .collect::<Vec<_>>()
2221 .join(" -> ")
2222 }
2223}
2224
2225impl Default for LeakDetector {
2226 fn default() -> Self {
2227 Self::new()
2228 }
2229}
2230
2231pub struct DoubleCloseDetector {
2237 lang: Language,
2238}
2239
2240impl DoubleCloseDetector {
2241 pub fn new() -> Self {
2242 Self {
2243 lang: Language::Python,
2244 }
2245 }
2246
2247 pub fn with_language(lang: Language) -> Self {
2248 Self { lang }
2249 }
2250
2251 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2253 let mut issues = Vec::new();
2254 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2255
2256 self.find_closes(func_node, source, &mut close_sites);
2257
2258 for (resource, lines) in close_sites {
2259 if lines.len() > 1 {
2260 issues.push(DoubleCloseInfo {
2261 resource,
2262 first_close: lines[0],
2263 second_close: lines[1],
2264 });
2265 }
2266 }
2267
2268 issues
2269 }
2270
2271 pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2273 let mut issues = Vec::new();
2274 let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2275 let patterns = get_resource_patterns(self.lang);
2276
2277 self.find_closes_multilang(func_node, source, &mut close_sites, &patterns);
2278
2279 for (resource, lines) in close_sites {
2280 if lines.len() > 1 {
2281 issues.push(DoubleCloseInfo {
2282 resource,
2283 first_close: lines[0],
2284 second_close: lines[1],
2285 });
2286 }
2287 }
2288
2289 issues
2290 }
2291
2292 fn find_closes(&self, node: Node, source: &[u8], closes: &mut HashMap<String, Vec<u32>>) {
2293 if node.kind() == "call" {
2294 if let Some(func) = node.child_by_field_name("function") {
2295 if func.kind() == "attribute" {
2296 if let Some(attr) = func.child_by_field_name("attribute") {
2297 let method = node_text(attr, source);
2298 if RESOURCE_CLOSERS.contains(&method) {
2299 if let Some(obj) = func.child_by_field_name("object") {
2300 let var_name = node_text(obj, source).to_string();
2301 let line = node.start_position().row as u32 + 1;
2302 closes.entry(var_name).or_default().push(line);
2303 }
2304 }
2305 }
2306 }
2307 }
2308 }
2309
2310 let mut cursor = node.walk();
2311 for child in node.children(&mut cursor) {
2312 self.find_closes(child, source, closes);
2313 }
2314 }
2315
2316 fn find_closes_multilang(
2317 &self,
2318 node: Node,
2319 source: &[u8],
2320 closes: &mut HashMap<String, Vec<u32>>,
2321 patterns: &LangResourcePatterns,
2322 ) {
2323 let kind = node.kind();
2324 if kind == "call"
2326 || kind == "call_expression"
2327 || kind == "method_invocation"
2328 || kind == "invocation_expression"
2329 {
2330 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2331 if patterns.closers.contains(&method.as_str()) {
2332 let line = node.start_position().row as u32 + 1;
2333 closes.entry(var_name).or_default().push(line);
2334 }
2335 }
2336 }
2337
2338 let mut cursor = node.walk();
2339 for child in node.children(&mut cursor) {
2340 self.find_closes_multilang(child, source, closes, patterns);
2341 }
2342 }
2343}
2344
2345impl Default for DoubleCloseDetector {
2346 fn default() -> Self {
2347 Self::new()
2348 }
2349}
2350
2351pub struct UseAfterCloseDetector {
2357 lang: Language,
2358}
2359
2360impl UseAfterCloseDetector {
2361 pub fn new() -> Self {
2362 Self {
2363 lang: Language::Python,
2364 }
2365 }
2366
2367 pub fn with_language(lang: Language) -> Self {
2368 Self { lang }
2369 }
2370
2371 pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2373 let mut issues = Vec::new();
2374 let mut close_lines: HashMap<String, u32> = HashMap::new();
2375 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2376
2377 self.analyze(func_node, source, &mut close_lines, &mut uses_after_close);
2378
2379 for (resource, close_line, use_line) in uses_after_close {
2380 issues.push(UseAfterCloseInfo {
2381 resource,
2382 close_line,
2383 use_line,
2384 });
2385 }
2386
2387 issues
2388 }
2389
2390 pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2392 let mut issues = Vec::new();
2393 let mut close_lines: HashMap<String, u32> = HashMap::new();
2394 let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2395 let patterns = get_resource_patterns(self.lang);
2396
2397 self.analyze_multilang(
2398 func_node,
2399 source,
2400 &mut close_lines,
2401 &mut uses_after_close,
2402 &patterns,
2403 );
2404
2405 for (resource, close_line, use_line) in uses_after_close {
2406 issues.push(UseAfterCloseInfo {
2407 resource,
2408 close_line,
2409 use_line,
2410 });
2411 }
2412
2413 issues
2414 }
2415
2416 fn analyze(
2417 &self,
2418 node: Node,
2419 source: &[u8],
2420 close_lines: &mut HashMap<String, u32>,
2421 uses_after: &mut Vec<(String, u32, u32)>,
2422 ) {
2423 let line = node.start_position().row as u32 + 1;
2424
2425 if node.kind() == "call" {
2426 if let Some(func) = node.child_by_field_name("function") {
2427 if func.kind() == "attribute" {
2428 if let Some(attr) = func.child_by_field_name("attribute") {
2429 let method = node_text(attr, source);
2430 if RESOURCE_CLOSERS.contains(&method) {
2431 if let Some(obj) = func.child_by_field_name("object") {
2432 let var_name = node_text(obj, source).to_string();
2433 close_lines.insert(var_name, line);
2434 }
2435 } else if let Some(obj) = func.child_by_field_name("object") {
2436 let var_name = node_text(obj, source).to_string();
2437 if let Some(&close_line) = close_lines.get(&var_name) {
2438 if line > close_line {
2439 uses_after.push((var_name, close_line, line));
2440 }
2441 }
2442 }
2443 }
2444 }
2445 }
2446 }
2447
2448 if node.kind() == "attribute" {
2449 if let Some(obj) = node.child_by_field_name("object") {
2450 if obj.kind() == "identifier" {
2451 let var_name = node_text(obj, source).to_string();
2452 if let Some(&close_line) = close_lines.get(&var_name) {
2453 if line > close_line {
2454 uses_after.push((var_name, close_line, line));
2455 }
2456 }
2457 }
2458 }
2459 }
2460
2461 let mut cursor = node.walk();
2462 for child in node.children(&mut cursor) {
2463 self.analyze(child, source, close_lines, uses_after);
2464 }
2465 }
2466
2467 fn analyze_multilang(
2468 &self,
2469 node: Node,
2470 source: &[u8],
2471 close_lines: &mut HashMap<String, u32>,
2472 uses_after: &mut Vec<(String, u32, u32)>,
2473 patterns: &LangResourcePatterns,
2474 ) {
2475 let line = node.start_position().row as u32 + 1;
2476 let kind = node.kind();
2477
2478 if kind == "call"
2480 || kind == "call_expression"
2481 || kind == "method_invocation"
2482 || kind == "invocation_expression"
2483 {
2484 if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2485 if patterns.closers.contains(&method.as_str()) {
2486 close_lines.insert(var_name, line);
2487 } else {
2488 if let Some((obj_name, _)) = extract_close_call(node, source, self.lang) {
2491 if let Some(&close_line) = close_lines.get(&obj_name) {
2492 if line > close_line {
2493 uses_after.push((obj_name, close_line, line));
2494 }
2495 }
2496 }
2497 }
2498 }
2499 }
2500
2501 if kind == "attribute"
2503 || kind == "member_expression"
2504 || kind == "field_expression"
2505 || kind == "selector_expression"
2506 {
2507 if let Some(obj) = node
2508 .child_by_field_name("object")
2509 .or_else(|| node.child_by_field_name("operand"))
2510 .or_else(|| node.child(0))
2511 {
2512 if obj.kind() == "identifier" {
2513 let var_name = node_text(obj, source).to_string();
2514 if let Some(&close_line) = close_lines.get(&var_name) {
2515 if line > close_line {
2516 uses_after.push((var_name, close_line, line));
2517 }
2518 }
2519 }
2520 }
2521 }
2522
2523 let mut cursor = node.walk();
2524 for child in node.children(&mut cursor) {
2525 self.analyze_multilang(child, source, close_lines, uses_after, patterns);
2526 }
2527 }
2528}
2529
2530impl Default for UseAfterCloseDetector {
2531 fn default() -> Self {
2532 Self::new()
2533 }
2534}
2535
2536pub fn suggest_context_manager(resources: &[ResourceInfo]) -> Vec<ContextSuggestion> {
2542 resources
2543 .iter()
2544 .filter(|r| !r.closed) .map(|r| {
2546 let suggestion = match r.resource_type.as_str() {
2547 "file" => format!("with open(...) as {}:", r.name),
2548 "connection" => format!("with connect(...) as {}:", r.name),
2549 "cursor" => format!("with connection.cursor() as {}:", r.name),
2550 "socket" => format!("with socket.socket(...) as {}:", r.name),
2551 _ => format!("with {} as {}:", r.resource_type, r.name),
2552 };
2553 ContextSuggestion {
2554 resource: r.name.clone(),
2555 suggestion,
2556 }
2557 })
2558 .collect()
2559}
2560
2561pub fn suggest_context_manager_multilang(
2563 resources: &[ResourceInfo],
2564 lang: Language,
2565) -> Vec<ContextSuggestion> {
2566 resources
2567 .iter()
2568 .filter(|r| !r.closed)
2569 .map(|r| {
2570 let suggestion = match lang {
2571 Language::Python => match r.resource_type.as_str() {
2572 "file" => format!("with open(...) as {}:", r.name),
2573 "connection" => format!("with connect(...) as {}:", r.name),
2574 "cursor" => format!("with connection.cursor() as {}:", r.name),
2575 "socket" => format!("with socket.socket(...) as {}:", r.name),
2576 _ => format!("with {} as {}:", r.resource_type, r.name),
2577 },
2578 Language::Go => format!("defer {}.Close()", r.name),
2579 Language::Rust => format!("// {}: Drop trait handles cleanup automatically. Consider wrapping in a scope block.", r.name),
2580 Language::Java => match r.resource_type.as_str() {
2581 "file_stream" | "reader" | "writer" | "scanner" | "stream" =>
2582 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2583 "connection" | "statement" =>
2584 format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2585 _ => format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2586 },
2587 Language::CSharp => format!("using (var {} = ...) {{ ... }}", r.name),
2588 Language::TypeScript | Language::JavaScript =>
2589 format!("try {{ ... }} finally {{ {}.close(); }}", r.name),
2590 Language::C => match r.resource_type.as_str() {
2591 "file" => format!("// Ensure fclose({}) on all paths", r.name),
2592 "memory" => format!("// Ensure free({}) on all paths", r.name),
2593 _ => format!("// Ensure cleanup of {} on all paths", r.name),
2594 },
2595 Language::Cpp => match r.resource_type.as_str() {
2596 "heap_object" => format!("// Use std::unique_ptr or std::shared_ptr instead of raw new for {}", r.name),
2597 "memory" => format!("// Use RAII wrapper or smart pointer for {}", r.name),
2598 _ => format!("// Consider RAII wrapper for {}", r.name),
2599 },
2600 Language::Ruby => format!("File.open(...) do |{}| ... end", r.name),
2601 Language::Php => format!("// Ensure {}() cleanup in finally block", r.name),
2602 Language::Kotlin => format!("{}.use {{ {} -> ... }}", r.name, r.name),
2603 Language::Swift => format!("defer {{ {}.closeFile() }}", r.name),
2604 Language::Ocaml => format!("Fun.protect ~finally:(fun () -> close_in {}) (fun () -> ...)", r.name),
2605 Language::Lua | Language::Luau => format!("// Ensure {}:close() is called, consider pcall for cleanup", r.name),
2606 _ => format!("// Ensure {} is properly closed/released", r.name),
2607 };
2608 ContextSuggestion {
2609 resource: r.name.clone(),
2610 suggestion,
2611 }
2612 })
2613 .collect()
2614}
2615
2616pub fn generate_constraints(
2622 file: &str,
2623 function: Option<&str>,
2624 resources: &[ResourceInfo],
2625 leaks: &[LeakInfo],
2626 double_closes: &[DoubleCloseInfo],
2627 use_after_closes: &[UseAfterCloseInfo],
2628) -> Vec<ResourceConstraint> {
2629 let mut constraints = Vec::new();
2630 let context = function.unwrap_or("module").to_string();
2631
2632 for leak in leaks {
2634 constraints.push(ResourceConstraint {
2635 rule: format!(
2636 "Resource '{}' opened at line {} must be closed on all control flow paths",
2637 leak.resource, leak.line
2638 ),
2639 context: format!("{} in {}", context, file),
2640 confidence: 0.9,
2641 });
2642 }
2643
2644 for dc in double_closes {
2646 constraints.push(ResourceConstraint {
2647 rule: format!(
2648 "Resource '{}' must not be closed twice (lines {} and {})",
2649 dc.resource, dc.first_close, dc.second_close
2650 ),
2651 context: format!("{} in {}", context, file),
2652 confidence: 0.95,
2653 });
2654 }
2655
2656 for uac in use_after_closes {
2658 constraints.push(ResourceConstraint {
2659 rule: format!(
2660 "Resource '{}' must not be used at line {} after being closed at line {}",
2661 uac.resource, uac.use_line, uac.close_line
2662 ),
2663 context: format!("{} in {}", context, file),
2664 confidence: 0.95,
2665 });
2666 }
2667
2668 for resource in resources {
2670 if !resource.closed {
2671 constraints.push(ResourceConstraint {
2672 rule: format!(
2673 "Resource '{}' ({}) should use context manager pattern (with statement)",
2674 resource.name, resource.resource_type
2675 ),
2676 context: format!("{} in {}", context, file),
2677 confidence: 0.85,
2678 });
2679 }
2680 }
2681
2682 constraints
2683}
2684
2685pub fn format_resources_text(report: &ResourceReport) -> String {
2691 let mut lines = Vec::new();
2692
2693 lines.push(format!("Resource Analysis: {}", report.file));
2694 lines.push(format!("Language: {}", report.language));
2695 if let Some(ref func) = report.function {
2696 lines.push(format!("Function: {}", func));
2697 }
2698 lines.push(String::new());
2699
2700 lines.push(format!("Resources detected: {}", report.resources.len()));
2702 for r in &report.resources {
2703 let status = if r.closed { "closed" } else { "open" };
2704 lines.push(format!(
2705 " - {}: {} at line {} [{}]",
2706 r.name, r.resource_type, r.line, status
2707 ));
2708 }
2709 lines.push(String::new());
2710
2711 if !report.leaks.is_empty() {
2713 lines.push(format!("Leaks found: {}", report.leaks.len()));
2714 for leak in &report.leaks {
2715 lines.push(format!(" - {} at line {}", leak.resource, leak.line));
2716 if let Some(ref paths) = leak.paths {
2717 for path in paths {
2718 lines.push(format!(" Path: {}", path));
2719 }
2720 }
2721 }
2722 } else {
2723 lines.push("Leaks found: 0".to_string());
2724 }
2725
2726 if !report.double_closes.is_empty() {
2728 lines.push(String::new());
2729 lines.push(format!(
2730 "Double-close errors: {}",
2731 report.double_closes.len()
2732 ));
2733 for dc in &report.double_closes {
2734 lines.push(format!(
2735 " - {}: first close at {}, second close at {}",
2736 dc.resource, dc.first_close, dc.second_close
2737 ));
2738 }
2739 }
2740
2741 if !report.use_after_closes.is_empty() {
2743 lines.push(String::new());
2744 lines.push(format!(
2745 "Use-after-close errors: {}",
2746 report.use_after_closes.len()
2747 ));
2748 for uac in &report.use_after_closes {
2749 lines.push(format!(
2750 " - {}: closed at {}, used at {}",
2751 uac.resource, uac.close_line, uac.use_line
2752 ));
2753 }
2754 }
2755
2756 if !report.suggestions.is_empty() {
2758 lines.push(String::new());
2759 lines.push(format!("Suggestions: {}", report.suggestions.len()));
2760 for s in &report.suggestions {
2761 lines.push(format!(" - {}: {}", s.resource, s.suggestion));
2762 }
2763 }
2764
2765 if !report.constraints.is_empty() {
2767 lines.push(String::new());
2768 lines.push(format!("Constraints: {}", report.constraints.len()));
2769 for c in &report.constraints {
2770 lines.push(format!(" - {} (confidence: {:.2})", c.rule, c.confidence));
2771 }
2772 }
2773
2774 lines.push(String::new());
2776 lines.push("Summary:".to_string());
2777 lines.push(format!(
2778 " resources_detected: {}",
2779 report.summary.resources_detected
2780 ));
2781 lines.push(format!(" leaks_found: {}", report.summary.leaks_found));
2782 lines.push(format!(
2783 " double_closes_found: {}",
2784 report.summary.double_closes_found
2785 ));
2786 lines.push(format!(
2787 " use_after_closes_found: {}",
2788 report.summary.use_after_closes_found
2789 ));
2790 lines.push(String::new());
2791 lines.push(format!(
2792 "Analysis completed in {}ms",
2793 report.analysis_time_ms
2794 ));
2795
2796 lines.join("\n")
2797}
2798
2799fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2804 std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
2805}
2806
2807fn extract_call_name(node: Node, source: &[u8]) -> Option<String> {
2810 match node.kind() {
2812 "call" | "call_expression" | "method_invocation" | "invocation_expression" => {
2814 if let Some(func) = node
2815 .child_by_field_name("function")
2816 .or_else(|| node.child_by_field_name("name"))
2817 .or_else(|| node.child_by_field_name("method"))
2818 {
2819 let func_text = node_text(func, source);
2820 let func_name = func_text
2822 .split('.')
2823 .next_back()
2824 .unwrap_or(func_text)
2825 .rsplit("::")
2826 .next()
2827 .unwrap_or(func_text);
2828 return Some(func_name.to_string());
2829 }
2830 if let Some(first_child) = node.child(0) {
2832 let text = node_text(first_child, source);
2833 let name = text
2834 .split('.')
2835 .next_back()
2836 .unwrap_or(text)
2837 .rsplit("::")
2838 .next()
2839 .unwrap_or(text);
2840 return Some(name.to_string());
2841 }
2842 }
2843 "composite_literal" => {
2845 }
2847 _ => {}
2848 }
2849
2850 let text = node_text(node, source);
2852 if text.contains('(') {
2853 let name_part = text.split('(').next()?;
2854 let func_name = name_part
2855 .split('.')
2856 .next_back()
2857 .unwrap_or(name_part)
2858 .rsplit("::")
2859 .next()
2860 .unwrap_or(name_part)
2861 .trim();
2862 if !func_name.is_empty() {
2863 return Some(func_name.to_string());
2864 }
2865 }
2866
2867 None
2868}
2869
2870fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
2872 match declarator.kind() {
2873 "identifier" => Some(node_text(declarator, source).to_string()),
2874 "pointer_declarator" => {
2875 let mut cursor = declarator.walk();
2877 for child in declarator.children(&mut cursor) {
2878 if child.kind() == "identifier" {
2879 return Some(node_text(child, source).to_string());
2880 }
2881 if child.kind() == "pointer_declarator" {
2882 return extract_c_declarator_name(child, source);
2883 }
2884 }
2885 None
2886 }
2887 _ => Some(node_text(declarator, source).to_string()),
2888 }
2889}
2890
2891fn extract_close_call(node: Node, source: &[u8], lang: Language) -> Option<(String, String)> {
2893 match lang {
2894 Language::Python
2895 | Language::Ruby
2896 | Language::Java
2897 | Language::CSharp
2898 | Language::TypeScript
2899 | Language::JavaScript
2900 | Language::Scala
2901 | Language::Kotlin
2902 | Language::Swift => {
2903 if let Some(func) = node
2905 .child_by_field_name("function")
2906 .or_else(|| node.child_by_field_name("method"))
2907 .or_else(|| node.child_by_field_name("name"))
2908 {
2909 if func.kind() == "attribute"
2911 || func.kind() == "member_expression"
2912 || func.kind() == "selector_expression"
2913 || func.kind() == "field_access"
2914 {
2915 let obj = func.child_by_field_name("object").or_else(|| func.child(0));
2916 let attr = func
2917 .child_by_field_name("attribute")
2918 .or_else(|| func.child_by_field_name("field"))
2919 .or_else(|| func.child_by_field_name("name"));
2920
2921 if let (Some(obj), Some(attr)) = (obj, attr) {
2922 let var_name = node_text(obj, source).to_string();
2923 let method = node_text(attr, source).to_string();
2924 return Some((var_name, method));
2925 }
2926 }
2927 }
2928 None
2929 }
2930 Language::Go => {
2931 if let Some(func) = node.child_by_field_name("function") {
2933 if func.kind() == "selector_expression" {
2934 if let Some(operand) = func.child_by_field_name("operand") {
2935 if let Some(field) = func.child_by_field_name("field") {
2936 let var_name = node_text(operand, source).to_string();
2937 let method = node_text(field, source).to_string();
2938 return Some((var_name, method));
2939 }
2940 }
2941 }
2942 }
2943 None
2944 }
2945 Language::C | Language::Cpp => {
2946 if let Some(func) = node
2948 .child_by_field_name("function")
2949 .or_else(|| node.child(0))
2950 {
2951 let func_name = node_text(func, source).to_string();
2952 if let Some(args) = node.child_by_field_name("arguments") {
2954 if let Some(first_arg) = args.child(1) {
2955 let var_name = node_text(first_arg, source).to_string();
2957 return Some((var_name, func_name));
2958 }
2959 }
2960 }
2961 None
2962 }
2963 _ => {
2964 if let Some(func) = node.child_by_field_name("function") {
2966 if let Some(obj) = func.child_by_field_name("object").or_else(|| func.child(0)) {
2967 if let Some(attr) = func.child_by_field_name("attribute") {
2968 let var_name = node_text(obj, source).to_string();
2969 let method = node_text(attr, source).to_string();
2970 return Some((var_name, method));
2971 }
2972 }
2973 }
2974 None
2975 }
2976 }
2977}
2978
2979pub fn build_cfg_multilang(func_node: Node, source: &[u8], lang: Language) -> SimpleCfg {
2985 let patterns = get_resource_patterns(lang);
2986 let mut cfg = SimpleCfg::new();
2987 let entry_id = cfg.new_block();
2988 cfg.entry_block = entry_id;
2989
2990 if let Some(block) = cfg.blocks.get_mut(&entry_id) {
2991 block.is_entry = true;
2992 }
2993
2994 let body = func_node
2996 .children(&mut func_node.walk())
2997 .find(|n| patterns.body_kinds.contains(&n.kind()));
2998
2999 if let Some(body_node) = body {
3000 let exit_id =
3001 process_statements_multilang(&mut cfg, body_node, source, entry_id, &patterns);
3002 if let Some(exit) = exit_id {
3003 if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
3004 cfg.mark_exit(exit);
3005 }
3006 }
3007 } else {
3008 cfg.mark_exit(entry_id);
3010 }
3011
3012 cfg
3013}
3014
3015fn process_statements_multilang(
3016 cfg: &mut SimpleCfg,
3017 node: Node,
3018 source: &[u8],
3019 mut current: usize,
3020 patterns: &LangResourcePatterns,
3021) -> Option<usize> {
3022 let mut cursor = node.walk();
3023 for child in node.children(&mut cursor) {
3024 let kind = child.kind();
3025
3026 if patterns.return_kinds.contains(&kind) {
3027 let text = node_text(child, source).to_string();
3029 let line = child.start_position().row as u32 + 1;
3030 if let Some(block) = cfg.blocks.get_mut(¤t) {
3031 block
3032 .stmts
3033 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
3034 block.lines.push(line);
3035 }
3036 cfg.mark_exit(current);
3037 return None;
3038 } else if patterns.if_kinds.contains(&kind) {
3039 current = process_if_multilang(cfg, child, source, current, patterns)?;
3041 } else if patterns.loop_kinds.contains(&kind) {
3042 current = process_loop_multilang(cfg, child, source, current, patterns)?;
3044 } else if patterns.try_kinds.contains(&kind) {
3045 current = process_try_multilang(cfg, child, source, current, patterns)?;
3047 } else if patterns.cleanup_block_kinds.contains(&kind) {
3048 current = process_cleanup_block_multilang(cfg, child, source, current, patterns)?;
3050 } else {
3051 let text = node_text(child, source).to_string();
3053 let line = child.start_position().row as u32 + 1;
3054 if let Some(block) = cfg.blocks.get_mut(¤t) {
3055 block
3056 .stmts
3057 .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
3058 block.lines.push(line);
3059 }
3060 }
3061 }
3062 Some(current)
3063}
3064
3065fn process_if_multilang(
3066 cfg: &mut SimpleCfg,
3067 node: Node,
3068 source: &[u8],
3069 current: usize,
3070 patterns: &LangResourcePatterns,
3071) -> Option<usize> {
3072 if let Some(cond) = node.child_by_field_name("condition") {
3074 let text = node_text(cond, source).to_string();
3075 let line = cond.start_position().row as u32 + 1;
3076 if let Some(block) = cfg.blocks.get_mut(¤t) {
3077 block.stmts.push((
3078 cond.start_byte(),
3079 cond.end_byte(),
3080 "condition".to_string(),
3081 text,
3082 ));
3083 block.lines.push(line);
3084 }
3085 }
3086
3087 let true_block = cfg.new_block();
3088 cfg.add_edge(current, true_block);
3089
3090 let mut cursor = node.walk();
3092 let consequence = node
3093 .children(&mut cursor)
3094 .find(|n| patterns.body_kinds.contains(&n.kind()));
3095 let true_exit = if let Some(body) = consequence {
3096 process_statements_multilang(cfg, body, source, true_block, patterns)
3097 } else {
3098 Some(true_block)
3099 };
3100
3101 let mut cursor = node.walk();
3103 let alternative = node
3104 .children(&mut cursor)
3105 .find(|n| n.kind() == "else_clause" || n.kind() == "elif_clause" || n.kind() == "else");
3106
3107 let false_exit = if let Some(alt) = alternative {
3108 let false_block = cfg.new_block();
3109 cfg.add_edge(current, false_block);
3110 let alt_body = alt
3111 .children(&mut alt.walk())
3112 .find(|n| patterns.body_kinds.contains(&n.kind()));
3113 if let Some(alt_body) = alt_body {
3114 process_statements_multilang(cfg, alt_body, source, false_block, patterns)
3115 } else {
3116 Some(false_block)
3117 }
3118 } else {
3119 None
3120 };
3121
3122 let merge = cfg.new_block();
3123 if let Some(te) = true_exit {
3124 cfg.add_edge(te, merge);
3125 }
3126 if let Some(fe) = false_exit {
3127 cfg.add_edge(fe, merge);
3128 }
3129 if alternative.is_none() {
3130 cfg.add_edge(current, merge);
3131 }
3132
3133 Some(merge)
3134}
3135
3136fn process_loop_multilang(
3137 cfg: &mut SimpleCfg,
3138 node: Node,
3139 source: &[u8],
3140 current: usize,
3141 patterns: &LangResourcePatterns,
3142) -> Option<usize> {
3143 let header = cfg.new_block();
3144 cfg.add_edge(current, header);
3145
3146 if let Some(cond) = node.child_by_field_name("condition") {
3147 let text = node_text(cond, source).to_string();
3148 let line = cond.start_position().row as u32 + 1;
3149 if let Some(block) = cfg.blocks.get_mut(&header) {
3150 block.stmts.push((
3151 cond.start_byte(),
3152 cond.end_byte(),
3153 "loop_condition".to_string(),
3154 text,
3155 ));
3156 block.lines.push(line);
3157 }
3158 }
3159
3160 let body_block = cfg.new_block();
3161 cfg.add_edge(header, body_block);
3162
3163 let body = node
3164 .children(&mut node.walk())
3165 .find(|n| patterns.body_kinds.contains(&n.kind()));
3166 let body_exit = if let Some(body_node) = body {
3167 process_statements_multilang(cfg, body_node, source, body_block, patterns)
3168 } else {
3169 Some(body_block)
3170 };
3171
3172 if let Some(be) = body_exit {
3173 cfg.add_edge(be, header);
3174 }
3175
3176 let exit = cfg.new_block();
3177 cfg.add_edge(header, exit);
3178 Some(exit)
3179}
3180
3181fn process_try_multilang(
3182 cfg: &mut SimpleCfg,
3183 node: Node,
3184 source: &[u8],
3185 current: usize,
3186 patterns: &LangResourcePatterns,
3187) -> Option<usize> {
3188 let try_block = cfg.new_block();
3189 cfg.add_edge(current, try_block);
3190
3191 let try_body = node
3192 .children(&mut node.walk())
3193 .find(|n| patterns.body_kinds.contains(&n.kind()));
3194 let try_exit = if let Some(body) = try_body {
3195 process_statements_multilang(cfg, body, source, try_block, patterns)
3196 } else {
3197 Some(try_block)
3198 };
3199
3200 let mut cursor = node.walk();
3201 let mut handler_exits = Vec::new();
3202 for child in node.children(&mut cursor) {
3203 let ck = child.kind();
3204 if ck == "except_clause" || ck == "catch_clause" || ck == "rescue" {
3205 let handler_block = cfg.new_block();
3206 cfg.add_edge(try_block, handler_block);
3207 if let Some(block) = cfg.blocks.get_mut(&try_block) {
3208 block.exception_handlers.push(handler_block);
3209 }
3210 let handler_body = child
3211 .children(&mut child.walk())
3212 .find(|n| patterns.body_kinds.contains(&n.kind()));
3213 if let Some(hb) = handler_body {
3214 if let Some(exit) =
3215 process_statements_multilang(cfg, hb, source, handler_block, patterns)
3216 {
3217 handler_exits.push(exit);
3218 }
3219 } else {
3220 handler_exits.push(handler_block);
3221 }
3222 }
3223 }
3224
3225 let finally_clause = node
3226 .children(&mut node.walk())
3227 .find(|n| n.kind() == "finally_clause" || n.kind() == "finally");
3228
3229 let merge = cfg.new_block();
3230 if let Some(te) = try_exit {
3231 if let Some(finally) = finally_clause {
3232 let finally_block = cfg.new_block();
3233 cfg.add_edge(te, finally_block);
3234 let finally_body = finally
3235 .children(&mut finally.walk())
3236 .find(|n| patterns.body_kinds.contains(&n.kind()));
3237 if let Some(fb) = finally_body {
3238 if let Some(exit) =
3239 process_statements_multilang(cfg, fb, source, finally_block, patterns)
3240 {
3241 cfg.add_edge(exit, merge);
3242 }
3243 } else {
3244 cfg.add_edge(finally_block, merge);
3245 }
3246 } else {
3247 cfg.add_edge(te, merge);
3248 }
3249 }
3250 for he in handler_exits {
3251 cfg.add_edge(he, merge);
3252 }
3253
3254 Some(merge)
3255}
3256
3257fn process_cleanup_block_multilang(
3258 cfg: &mut SimpleCfg,
3259 node: Node,
3260 source: &[u8],
3261 current: usize,
3262 patterns: &LangResourcePatterns,
3263) -> Option<usize> {
3264 let text = node_text(node, source).to_string();
3265 let line = node.start_position().row as u32 + 1;
3266 if let Some(block) = cfg.blocks.get_mut(¤t) {
3267 block.stmts.push((
3268 node.start_byte(),
3269 node.end_byte(),
3270 node.kind().to_string(),
3271 text,
3272 ));
3273 block.lines.push(line);
3274 }
3275
3276 let body = node
3277 .children(&mut node.walk())
3278 .find(|n| patterns.body_kinds.contains(&n.kind()));
3279 if let Some(body_node) = body {
3280 process_statements_multilang(cfg, body_node, source, current, patterns)
3281 } else {
3282 Some(current)
3283 }
3284}
3285
3286#[cfg(test)]
3287fn get_python_parser() -> PatternsResult<Parser> {
3288 get_parser_for_language(Language::Python)
3289}
3290
3291fn get_parser_for_language(lang: Language) -> PatternsResult<Parser> {
3293 let mut parser = Parser::new();
3294 let ts_lang =
3295 ParserPool::get_ts_language(lang).ok_or_else(|| PatternsError::UnsupportedLanguage {
3296 language: lang.as_str().to_string(),
3297 })?;
3298 parser
3299 .set_language(&ts_lang)
3300 .map_err(|e| PatternsError::ParseError {
3301 file: PathBuf::from("<internal>"),
3302 message: format!("Failed to set {} language: {}", lang.as_str(), e),
3303 })?;
3304 Ok(parser)
3305}
3306
3307fn get_function_name_from_node(
3311 node: Node,
3312 source: &[u8],
3313 patterns: &LangResourcePatterns,
3314) -> Option<String> {
3315 if node.kind() == "value_definition" {
3317 let mut cursor = node.walk();
3318 for child in node.children(&mut cursor) {
3319 if child.kind() == "let_binding" {
3320 if let Some(pattern) = child.child_by_field_name("pattern") {
3321 return Some(node_text(pattern, source).to_string());
3322 }
3323 }
3324 }
3325 return None;
3326 }
3327
3328 if let Some(name_node) = node.child_by_field_name(patterns.name_field) {
3330 if name_node.kind() == "function_declarator" {
3333 if let Some(inner) = name_node.child_by_field_name("declarator") {
3334 return Some(node_text(inner, source).to_string());
3335 }
3336 }
3337 if name_node.kind() == "pointer_declarator" {
3339 let mut cursor = name_node.walk();
3340 for child in name_node.children(&mut cursor) {
3341 if child.kind() == "function_declarator" {
3342 if let Some(inner) = child.child_by_field_name("declarator") {
3343 return Some(node_text(inner, source).to_string());
3344 }
3345 }
3346 }
3347 }
3348 return Some(node_text(name_node, source).to_string());
3349 }
3350 None
3351}
3352
3353#[cfg(test)]
3354fn find_function_node<'a>(
3355 tree: &'a tree_sitter::Tree,
3356 function_name: &str,
3357 source: &[u8],
3358) -> Option<Node<'a>> {
3359 let root = tree.root_node();
3360 let patterns = get_resource_patterns(Language::Python);
3362 find_function_recursive(root, function_name, source, &patterns)
3363}
3364
3365fn find_function_node_multilang<'a>(
3366 tree: &'a tree_sitter::Tree,
3367 function_name: &str,
3368 source: &[u8],
3369 lang: Language,
3370) -> Option<Node<'a>> {
3371 let root = tree.root_node();
3372 let patterns = get_resource_patterns(lang);
3373 find_function_recursive(root, function_name, source, &patterns)
3374}
3375
3376fn find_function_recursive<'a>(
3377 node: Node<'a>,
3378 function_name: &str,
3379 source: &[u8],
3380 patterns: &LangResourcePatterns,
3381) -> Option<Node<'a>> {
3382 let kind = node.kind();
3383 if patterns.function_kinds.contains(&kind) {
3384 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3385 if name == function_name {
3386 return Some(node);
3387 }
3388 }
3389 }
3390
3391 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3394 let mut decl_cursor = node.walk();
3395 for child in node.children(&mut decl_cursor) {
3396 if child.kind() == "variable_declarator" {
3397 if let Some(name_node) = child.child_by_field_name("name") {
3398 let var_name = name_node.utf8_text(source).unwrap_or("");
3399 if var_name == function_name {
3400 if let Some(value_node) = child.child_by_field_name("value") {
3401 if matches!(
3402 value_node.kind(),
3403 "arrow_function"
3404 | "function"
3405 | "function_expression"
3406 | "generator_function"
3407 ) {
3408 return Some(value_node);
3409 }
3410 }
3411 }
3412 }
3413 }
3414 }
3415 }
3416
3417 if kind == "assignment_expression" {
3426 if let (Some(left), Some(right)) = (
3427 node.child_by_field_name("left"),
3428 node.child_by_field_name("right"),
3429 ) {
3430 let target_name = match left.kind() {
3431 "identifier" => Some(left.utf8_text(source).unwrap_or("").to_string()),
3432 "member_expression" => left
3433 .child_by_field_name("property")
3434 .map(|p| p.utf8_text(source).unwrap_or("").to_string()),
3435 _ => None,
3436 };
3437 if let Some(name) = target_name {
3438 if name == function_name
3439 && matches!(
3440 right.kind(),
3441 "arrow_function"
3442 | "function"
3443 | "function_expression"
3444 | "generator_function"
3445 )
3446 {
3447 return Some(right);
3448 }
3449 }
3450 }
3451 }
3452
3453 if kind == "pair" {
3458 if let (Some(key), Some(value)) = (
3459 node.child_by_field_name("key"),
3460 node.child_by_field_name("value"),
3461 ) {
3462 let key_name = match key.kind() {
3463 "property_identifier" | "identifier" => {
3464 key.utf8_text(source).unwrap_or("").to_string()
3465 }
3466 "string" => key
3467 .utf8_text(source)
3468 .unwrap_or("")
3469 .trim_matches(|c| c == '"' || c == '\'' || c == '`')
3470 .to_string(),
3471 _ => String::new(),
3472 };
3473 if key_name == function_name
3474 && matches!(
3475 value.kind(),
3476 "arrow_function"
3477 | "function"
3478 | "function_expression"
3479 | "generator_function"
3480 )
3481 {
3482 return Some(value);
3483 }
3484 }
3485 }
3486
3487 let mut cursor = node.walk();
3488 for child in node.children(&mut cursor) {
3489 if let Some(found) = find_function_recursive(child, function_name, source, patterns) {
3490 return Some(found);
3491 }
3492 }
3493
3494 None
3495}
3496
3497fn find_all_functions_multilang<'a>(
3498 tree: &'a tree_sitter::Tree,
3499 source: &[u8],
3500 lang: Language,
3501) -> Vec<(String, Node<'a>)> {
3502 let mut functions = Vec::new();
3503 let patterns = get_resource_patterns(lang);
3504 collect_functions(tree.root_node(), source, &mut functions, &patterns);
3505 functions
3506}
3507
3508fn collect_functions<'a>(
3509 node: Node<'a>,
3510 source: &[u8],
3511 functions: &mut Vec<(String, Node<'a>)>,
3512 patterns: &LangResourcePatterns,
3513) {
3514 let kind = node.kind();
3515 if patterns.function_kinds.contains(&kind) {
3516 if let Some(name) = get_function_name_from_node(node, source, patterns) {
3517 functions.push((name, node));
3518 }
3519 }
3520
3521 if matches!(kind, "lexical_declaration" | "variable_declaration") {
3524 let mut decl_cursor = node.walk();
3525 for child in node.children(&mut decl_cursor) {
3526 if child.kind() == "variable_declarator" {
3527 if let Some(name_node) = child.child_by_field_name("name") {
3528 if let Some(value_node) = child.child_by_field_name("value") {
3529 if matches!(
3530 value_node.kind(),
3531 "arrow_function"
3532 | "function"
3533 | "function_expression"
3534 | "generator_function"
3535 ) {
3536 let var_name = name_node.utf8_text(source).unwrap_or("").to_string();
3537 functions.push((var_name, value_node));
3538 }
3539 }
3540 }
3541 }
3542 }
3543 }
3544
3545 if kind == "assignment_expression" {
3548 if let (Some(left), Some(right)) = (
3549 node.child_by_field_name("left"),
3550 node.child_by_field_name("right"),
3551 ) {
3552 if matches!(
3553 right.kind(),
3554 "arrow_function" | "function" | "function_expression" | "generator_function"
3555 ) {
3556 let target_name = match left.kind() {
3557 "identifier" => Some(left.utf8_text(source).unwrap_or("").to_string()),
3558 "member_expression" => left
3559 .child_by_field_name("property")
3560 .map(|p| p.utf8_text(source).unwrap_or("").to_string()),
3561 _ => None,
3562 };
3563 if let Some(name) = target_name {
3564 if !name.is_empty() {
3565 functions.push((name, right));
3566 }
3567 }
3568 }
3569 }
3570 }
3571
3572 if kind == "pair" {
3575 if let (Some(key), Some(value)) = (
3576 node.child_by_field_name("key"),
3577 node.child_by_field_name("value"),
3578 ) {
3579 if matches!(
3580 value.kind(),
3581 "arrow_function" | "function" | "function_expression" | "generator_function"
3582 ) {
3583 let key_name = match key.kind() {
3584 "property_identifier" | "identifier" => {
3585 key.utf8_text(source).unwrap_or("").to_string()
3586 }
3587 "string" => key
3588 .utf8_text(source)
3589 .unwrap_or("")
3590 .trim_matches(|c| c == '"' || c == '\'' || c == '`')
3591 .to_string(),
3592 _ => String::new(),
3593 };
3594 if !key_name.is_empty() {
3595 functions.push((key_name, value));
3596 }
3597 }
3598 }
3599 }
3600
3601 let mut cursor = node.walk();
3602 for child in node.children(&mut cursor) {
3603 collect_functions(child, source, functions, patterns);
3604 }
3605}
3606
3607fn analyze_function_with_lang(
3612 func_node: Node,
3613 source: &[u8],
3614 args: &ResourcesArgs,
3615 lang: Language,
3616) -> (
3617 Vec<ResourceInfo>,
3618 Vec<LeakInfo>,
3619 Vec<DoubleCloseInfo>,
3620 Vec<UseAfterCloseInfo>,
3621) {
3622 let check_leaks = args.check_leaks || args.check_all;
3623 let check_double_close = args.check_double_close || args.check_all;
3624 let check_use_after_close = args.check_use_after_close || args.check_all;
3625 let mut detector = ResourceDetector::with_language(lang);
3627 let resources = detector.detect_with_patterns(func_node, source);
3628
3629 let leaks = if check_leaks {
3631 let cfg = build_cfg_multilang(func_node, source, lang);
3632 let mut leak_detector = LeakDetector::new();
3633 leak_detector.detect_multilang(&cfg, &resources, source, args.show_paths)
3634 } else {
3635 Vec::new()
3636 };
3637
3638 let double_closes = if check_double_close {
3640 let detector = DoubleCloseDetector::with_language(lang);
3641 detector.detect_multilang(func_node, source)
3642 } else {
3643 Vec::new()
3644 };
3645
3646 let use_after_closes = if check_use_after_close {
3648 let detector = UseAfterCloseDetector::with_language(lang);
3649 detector.detect_multilang(func_node, source)
3650 } else {
3651 Vec::new()
3652 };
3653
3654 (resources, leaks, double_closes, use_after_closes)
3655}
3656
3657pub fn run(args: ResourcesArgs, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
3663 let start_time = Instant::now();
3664
3665 let path = if let Some(ref root) = args.project_root {
3673 validate_file_path_in_project(&args.file, root)?
3674 } else {
3675 validate_file_path(&args.file)?
3676 };
3677
3678 let source = read_file_safe(&path)?;
3680 let source_bytes = source.as_bytes();
3681
3682 let lang: Language = match args.lang {
3684 Some(l) => l,
3685 None => Language::from_path(&path).ok_or_else(|| {
3686 let ext = path
3687 .extension()
3688 .and_then(|e| e.to_str())
3689 .unwrap_or("unknown")
3690 .to_string();
3691 PatternsError::UnsupportedLanguage { language: ext }
3692 })?,
3693 };
3694
3695 let mut parser = get_parser_for_language(lang)?;
3697 let tree = parser
3698 .parse(&source, None)
3699 .ok_or_else(|| PatternsError::ParseError {
3700 file: path.clone(),
3701 message: format!("Failed to parse {} file", lang.as_str()),
3702 })?;
3703
3704 let mut all_resources = Vec::new();
3706 let mut all_leaks = Vec::new();
3707 let mut all_double_closes = Vec::new();
3708 let mut all_use_after_closes = Vec::new();
3709
3710 if let Some(ref func_name) = args.function {
3711 if let Some(func_node) = find_function_node_multilang(&tree, func_name, source_bytes, lang)
3713 {
3714 let (resources, leaks, double_closes, use_after_closes) =
3715 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3716 all_resources = resources;
3717 all_leaks = leaks;
3718 all_double_closes = double_closes;
3719 all_use_after_closes = use_after_closes;
3720 } else {
3721 return Err(PatternsError::FunctionNotFound {
3722 function: func_name.clone(),
3723 file: path.clone(),
3724 }
3725 .into());
3726 }
3727 } else {
3728 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3730 for (_name, func_node) in functions {
3731 let (resources, leaks, double_closes, use_after_closes) =
3732 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3733 all_resources.extend(resources);
3734 all_leaks.extend(leaks);
3735 all_double_closes.extend(double_closes);
3736 all_use_after_closes.extend(use_after_closes);
3737 }
3738 }
3739
3740 let suggestions = if args.suggest_context {
3742 suggest_context_manager_multilang(&all_resources, lang)
3743 } else {
3744 Vec::new()
3745 };
3746
3747 let constraints = if args.constraints {
3749 generate_constraints(
3750 path.to_str().unwrap_or(""),
3751 args.function.as_deref(),
3752 &all_resources,
3753 &all_leaks,
3754 &all_double_closes,
3755 &all_use_after_closes,
3756 )
3757 } else {
3758 Vec::new()
3759 };
3760
3761 let summary = ResourceSummary {
3763 resources_detected: all_resources.len() as u32,
3764 leaks_found: all_leaks.len() as u32,
3765 double_closes_found: all_double_closes.len() as u32,
3766 use_after_closes_found: all_use_after_closes.len() as u32,
3767 };
3768
3769 let elapsed_ms = start_time.elapsed().as_millis() as u64;
3770
3771 let report = ResourceReport {
3777 file: args.file.to_string_lossy().to_string(),
3778 language: lang.as_str().to_string(),
3779 function: args.function.clone(),
3780 resources: all_resources,
3781 leaks: all_leaks,
3782 double_closes: all_double_closes,
3783 use_after_closes: all_use_after_closes,
3784 suggestions,
3785 constraints,
3786 summary,
3787 analysis_time_ms: elapsed_ms,
3788 };
3789
3790 let use_text = matches!(global_format, GlobalOutputFormat::Text)
3792 || matches!(args.output_format, OutputFormat::Text);
3793 let output = if use_text {
3794 format_resources_text(&report)
3795 } else {
3796 serde_json::to_string_pretty(&report)?
3797 };
3798
3799 println!("{}", output);
3800
3801 let has_issues = report.summary.leaks_found > 0
3803 || report.summary.double_closes_found > 0
3804 || report.summary.use_after_closes_found > 0;
3805
3806 if has_issues {
3807 std::process::exit(3);
3808 }
3809
3810 Ok(())
3811}
3812
3813pub struct ResourceAnalysisResults {
3822 pub leaks: Vec<(String, LeakInfo)>,
3824 pub double_closes: Vec<(String, DoubleCloseInfo)>,
3826 pub use_after_closes: Vec<(String, UseAfterCloseInfo)>,
3828}
3829
3830pub fn analyze_source_for_resource_issues(
3846 source: &str,
3847 lang: Language,
3848) -> PatternsResult<ResourceAnalysisResults> {
3849 let source_bytes = source.as_bytes();
3850
3851 let mut parser = get_parser_for_language(lang)?;
3853 let tree = parser
3854 .parse(source, None)
3855 .ok_or_else(|| PatternsError::ParseError {
3856 file: PathBuf::from("<in-memory>"),
3857 message: format!(
3858 "Failed to parse {} source for resource analysis",
3859 lang.as_str()
3860 ),
3861 })?;
3862
3863 let args = ResourcesArgs {
3865 file: PathBuf::from("<in-memory>"),
3866 function: None,
3867 lang: Some(lang),
3868 check_leaks: true,
3869 check_double_close: true,
3870 check_use_after_close: true,
3871 check_all: true,
3872 suggest_context: false,
3873 show_paths: false,
3874 constraints: false,
3875 summary: false,
3876 output_format: OutputFormat::Json,
3877 project_root: None,
3878 };
3879
3880 let mut all_leaks = Vec::new();
3881 let mut all_double_closes = Vec::new();
3882 let mut all_use_after_closes = Vec::new();
3883
3884 let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3886 for (func_name, func_node) in functions {
3887 let (_resources, leaks, double_closes, use_after_closes) =
3888 analyze_function_with_lang(func_node, source_bytes, &args, lang);
3889
3890 for leak in leaks {
3891 all_leaks.push((func_name.clone(), leak));
3892 }
3893 for dc in double_closes {
3894 all_double_closes.push((func_name.clone(), dc));
3895 }
3896 for uac in use_after_closes {
3897 all_use_after_closes.push((func_name.clone(), uac));
3898 }
3899 }
3900
3901 Ok(ResourceAnalysisResults {
3902 leaks: all_leaks,
3903 double_closes: all_double_closes,
3904 use_after_closes: all_use_after_closes,
3905 })
3906}
3907
3908#[cfg(test)]
3913mod tests {
3914 use super::*;
3915
3916 const TEST_LEAKY_FUNCTION: &str = r#"
3917def leaky_function(path):
3918 f = open(path)
3919 if some_condition():
3920 return None
3921 content = f.read()
3922 f.close()
3923 return content
3924"#;
3925
3926 const TEST_SAFE_WITH_CONTEXT: &str = r#"
3927def safe_with_context(path):
3928 with open(path) as f:
3929 return f.read()
3930"#;
3931
3932 const TEST_DOUBLE_CLOSE: &str = r#"
3933def double_close(path):
3934 f = open(path)
3935 content = f.read()
3936 f.close()
3937 f.close()
3938 return content
3939"#;
3940
3941 const TEST_USE_AFTER_CLOSE: &str = r#"
3942def use_after_close(path):
3943 f = open(path)
3944 f.close()
3945 content = f.read()
3946 return content
3947"#;
3948
3949 #[test]
3950 fn test_resource_creators_constant() {
3951 assert!(RESOURCE_CREATORS.contains(&"open"));
3952 assert!(RESOURCE_CREATORS.contains(&"socket"));
3953 assert!(RESOURCE_CREATORS.contains(&"connect"));
3954 assert!(RESOURCE_CREATORS.contains(&"cursor"));
3955 }
3956
3957 #[test]
3958 fn test_resource_closers_constant() {
3959 assert!(RESOURCE_CLOSERS.contains(&"close"));
3960 assert!(RESOURCE_CLOSERS.contains(&"shutdown"));
3961 assert!(RESOURCE_CLOSERS.contains(&"disconnect"));
3962 }
3963
3964 #[test]
3965 fn test_max_paths_constant() {
3966 assert_eq!(MAX_PATHS, 1000);
3967 }
3968
3969 #[test]
3970 fn test_resource_detector_finds_open() {
3971 let mut parser = get_python_parser().unwrap();
3972 let tree = parser.parse(TEST_LEAKY_FUNCTION, None).unwrap();
3973 let source = TEST_LEAKY_FUNCTION.as_bytes();
3974
3975 let func_node = find_function_node(&tree, "leaky_function", source).unwrap();
3976 let mut detector = ResourceDetector::new();
3977 let resources = detector.detect(func_node, source);
3978
3979 assert_eq!(resources.len(), 1);
3980 assert_eq!(resources[0].name, "f");
3981 assert_eq!(resources[0].resource_type, "file");
3982 assert!(!resources[0].closed);
3983 }
3984
3985 #[test]
3986 fn test_resource_detector_context_manager() {
3987 let mut parser = get_python_parser().unwrap();
3988 let tree = parser.parse(TEST_SAFE_WITH_CONTEXT, None).unwrap();
3989 let source = TEST_SAFE_WITH_CONTEXT.as_bytes();
3990
3991 let func_node = find_function_node(&tree, "safe_with_context", source).unwrap();
3992 let mut detector = ResourceDetector::new();
3993 let resources = detector.detect(func_node, source);
3994
3995 assert_eq!(resources.len(), 1);
3996 assert!(
3997 resources[0].closed,
3998 "Context manager resource should be marked as closed"
3999 );
4000 }
4001
4002 #[test]
4003 fn test_double_close_detector() {
4004 let mut parser = get_python_parser().unwrap();
4005 let tree = parser.parse(TEST_DOUBLE_CLOSE, None).unwrap();
4006 let source = TEST_DOUBLE_CLOSE.as_bytes();
4007
4008 let func_node = find_function_node(&tree, "double_close", source).unwrap();
4009 let detector = DoubleCloseDetector::new();
4010 let issues = detector.detect(func_node, source);
4011
4012 assert_eq!(issues.len(), 1);
4013 assert_eq!(issues[0].resource, "f");
4014 }
4015
4016 #[test]
4017 fn test_use_after_close_detector() {
4018 let mut parser = get_python_parser().unwrap();
4019 let tree = parser.parse(TEST_USE_AFTER_CLOSE, None).unwrap();
4020 let source = TEST_USE_AFTER_CLOSE.as_bytes();
4021
4022 let func_node = find_function_node(&tree, "use_after_close", source).unwrap();
4023 let detector = UseAfterCloseDetector::new();
4024 let issues = detector.detect(func_node, source);
4025
4026 assert!(!issues.is_empty());
4027 assert_eq!(issues[0].resource, "f");
4028 }
4029
4030 #[test]
4031 fn test_suggest_context_manager() {
4032 let resources = vec![ResourceInfo {
4033 name: "f".to_string(),
4034 resource_type: "file".to_string(),
4035 line: 2,
4036 closed: false,
4037 }];
4038
4039 let suggestions = suggest_context_manager(&resources);
4040 assert_eq!(suggestions.len(), 1);
4041 assert!(suggestions[0].suggestion.contains("with open"));
4042 }
4043
4044 #[test]
4045 fn test_generate_constraints_for_leak() {
4046 let resources = vec![ResourceInfo {
4047 name: "f".to_string(),
4048 resource_type: "file".to_string(),
4049 line: 2,
4050 closed: false,
4051 }];
4052 let leaks = vec![LeakInfo {
4053 resource: "f".to_string(),
4054 line: 2,
4055 paths: None,
4056 }];
4057
4058 let constraints =
4059 generate_constraints("test.py", Some("test_func"), &resources, &leaks, &[], &[]);
4060
4061 assert!(!constraints.is_empty());
4062 assert!(constraints[0].rule.contains("must be closed"));
4063 }
4064
4065 #[test]
4066 fn test_leak_detector_path_limit() {
4067 let detector = LeakDetector::new();
4068 assert_eq!(detector.max_paths, MAX_PATHS);
4069 }
4070
4071 #[test]
4072 fn test_cfg_builder_basic() {
4073 let mut parser = get_python_parser().unwrap();
4074 let source = r#"
4075def simple():
4076 x = 1
4077 return x
4078"#;
4079 let tree = parser.parse(source, None).unwrap();
4080 let func_node = find_function_node(&tree, "simple", source.as_bytes()).unwrap();
4081 let cfg = build_cfg(func_node, source.as_bytes());
4082
4083 assert!(!cfg.blocks.is_empty());
4084 assert!(!cfg.exit_blocks.is_empty());
4085 }
4086
4087 #[test]
4088 fn test_cfg_builder_with_if() {
4089 let mut parser = get_python_parser().unwrap();
4090 let source = r#"
4091def with_if(x):
4092 if x > 0:
4093 return x
4094 return -x
4095"#;
4096 let tree = parser.parse(source, None).unwrap();
4097 let func_node = find_function_node(&tree, "with_if", source.as_bytes()).unwrap();
4098 let cfg = build_cfg(func_node, source.as_bytes());
4099
4100 assert!(cfg.blocks.len() > 1);
4102 }
4103
4104 #[test]
4105 fn test_format_resources_text() {
4106 let report = ResourceReport {
4107 file: "test.py".to_string(),
4108 language: "python".to_string(),
4109 function: Some("test".to_string()),
4110 resources: vec![ResourceInfo {
4111 name: "f".to_string(),
4112 resource_type: "file".to_string(),
4113 line: 2,
4114 closed: false,
4115 }],
4116 leaks: vec![],
4117 double_closes: vec![],
4118 use_after_closes: vec![],
4119 suggestions: vec![],
4120 constraints: vec![],
4121 summary: ResourceSummary::default(),
4122 analysis_time_ms: 10,
4123 };
4124
4125 let text = format_resources_text(&report);
4126 assert!(text.contains("Resource Analysis: test.py"));
4127 assert!(text.contains("Function: test"));
4128 assert!(text.contains("file"));
4129 }
4130
4131 #[test]
4132 fn test_find_ts_arrow_function_resources() {
4133 let ts_source = r#"
4134const getDuration = (start: Date, end: Date): number => {
4135 const conn = createConnection();
4136 const result = end.getTime() - start.getTime();
4137 conn.close();
4138 return result;
4139};
4140
4141function regularFunc(x: number): number {
4142 return x * 2;
4143}
4144"#;
4145 let tree = tldr_core::ast::parser::parse(ts_source, Language::TypeScript).unwrap();
4146 let source_bytes = ts_source.as_bytes();
4147
4148 let regular =
4150 find_function_node_multilang(&tree, "regularFunc", source_bytes, Language::TypeScript);
4151 assert!(regular.is_some(), "Should find regular TS function");
4152
4153 let arrow =
4155 find_function_node_multilang(&tree, "getDuration", source_bytes, Language::TypeScript);
4156 assert!(
4157 arrow.is_some(),
4158 "Should find TS arrow function 'getDuration'"
4159 );
4160 }
4161
4162 #[test]
4163 fn test_resources_args_lang_flag() {
4164 let args = ResourcesArgs {
4166 file: PathBuf::from("src/db.go"),
4167 function: None,
4168 lang: Some(Language::Go),
4169 check_leaks: true,
4170 check_double_close: false,
4171 check_use_after_close: false,
4172 check_all: false,
4173 suggest_context: false,
4174 show_paths: false,
4175 constraints: false,
4176 summary: false,
4177 output_format: OutputFormat::Json,
4178 project_root: None,
4179 };
4180 assert_eq!(args.lang, Some(Language::Go));
4181
4182 let args_auto = ResourcesArgs {
4184 file: PathBuf::from("src/db.py"),
4185 function: None,
4186 lang: None,
4187 check_leaks: true,
4188 check_double_close: false,
4189 check_use_after_close: false,
4190 check_all: false,
4191 suggest_context: false,
4192 show_paths: false,
4193 constraints: false,
4194 summary: false,
4195 output_format: OutputFormat::Json,
4196 project_root: None,
4197 };
4198 assert_eq!(args_auto.lang, None);
4199 }
4200}