Skip to main content

tldr_cli/commands/patterns/
resources.rs

1//! Resources Command - Resource Lifecycle Analysis
2//!
3//! Analyzes resource lifecycle to detect leaks, double-close, and use-after-close issues.
4//!
5//! # Analysis Types
6//!
7//! - R1: Resource detection - Identify resources requiring close
8//! - R2: Close verification - All-paths leak detection
9//! - R3: Double-close detection - Closing resources twice
10//! - R4: Use-after-close - Using closed resources
11//! - R6: Context manager suggestions - Suggest `with` statement
12//! - R7: Leak path enumeration - Detailed paths to leaks
13//! - R9: Constraint generation - LLM-ready constraints
14//!
15//! # TIGER Mitigations
16//!
17//! - T04: MAX_PATHS=1000 with early termination for path enumeration
18//!
19//! # Example
20//!
21//! ```bash
22//! # Analyze a single file
23//! tldr resources src/db.py
24//!
25//! # Analyze specific function
26//! tldr resources src/db.py query
27//!
28//! # Check all issues
29//! tldr resources src/db.py --check-all
30//!
31//! # Show leak paths
32//! tldr resources src/db.py --show-paths
33//! ```
34
35use 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
53// =============================================================================
54// TIGER-04: Path Enumeration Limit
55// =============================================================================
56
57/// Maximum paths to enumerate before early termination (TIGER-04).
58pub const MAX_PATHS: usize = 1000;
59
60// =============================================================================
61// Resource Detection Constants (Multi-Language)
62// =============================================================================
63
64/// Resource pattern for a specific language: (creator_function, resource_type, closer_functions)
65struct LangResourcePatterns {
66    /// Functions that create resources requiring cleanup
67    creators: &'static [(&'static str, &'static str)], // (func_name, resource_type)
68    /// Methods/functions that release resources
69    closers: &'static [&'static str],
70    /// Function node kinds for this language in tree-sitter
71    function_kinds: &'static [&'static str],
72    /// Name field for function nodes (usually "name")
73    name_field: &'static str,
74    /// Body node kind ("block" for Python, "statement_block" for TS, etc.)
75    body_kinds: &'static [&'static str],
76    /// Assignment node kinds
77    assignment_kinds: &'static [&'static str],
78    /// Return statement kinds
79    return_kinds: &'static [&'static str],
80    /// If statement kinds
81    if_kinds: &'static [&'static str],
82    /// Loop statement kinds
83    loop_kinds: &'static [&'static str],
84    /// Try statement kinds
85    try_kinds: &'static [&'static str],
86    /// Context manager / RAII / defer kinds
87    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"], // Elixir uses `def` as a macro call
468            name_field: "target",
469            body_kinds: &["do_block"],
470            assignment_kinds: &["binary_operator"], // = operator
471            return_kinds: &[],
472            if_kinds: &["call"],   // if is a macro
473            loop_kinds: &["call"], // for/Enum.each are calls
474            try_kinds: &["call"],  // try is a macro
475            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"], // .use { } block
526        },
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"], // do { } catch { }
554            cleanup_block_kinds: &[],     // defer detected differently
555        },
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
609/// Legacy constant for backward compatibility with tests
610pub 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
628/// Legacy constant for backward compatibility with tests
629pub const RESOURCE_CLOSERS: &[&str] = &[
630    "close",
631    "shutdown",
632    "disconnect",
633    "release",
634    "dispose",
635    "cleanup",
636    "terminate",
637    "__exit__",
638];
639
640/// Legacy resource type map for backward compatibility with Python detection
641const 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// =============================================================================
659// CLI Arguments
660// =============================================================================
661
662/// Analyze resource lifecycle to detect leaks, double-close, and use-after-close.
663#[derive(Debug, Args, Clone)]
664pub struct ResourcesArgs {
665    /// Source file to analyze
666    pub file: PathBuf,
667
668    /// Function to analyze (optional; analyze all if omitted)
669    pub function: Option<String>,
670
671    /// Language filter (auto-detected if omitted)
672    #[arg(long, short = 'l')]
673    pub lang: Option<Language>,
674
675    /// Run leak detection (R2) - enabled by default
676    #[arg(long, default_value = "true")]
677    pub check_leaks: bool,
678
679    /// Run double-close detection (R3)
680    #[arg(long)]
681    pub check_double_close: bool,
682
683    /// Run use-after-close detection (R4)
684    #[arg(long)]
685    pub check_use_after_close: bool,
686
687    /// Run all checks (R2, R3, R4)
688    #[arg(long)]
689    pub check_all: bool,
690
691    /// Suggest context manager usage (R6)
692    #[arg(long)]
693    pub suggest_context: bool,
694
695    /// Show detailed leak paths (R7)
696    #[arg(long)]
697    pub show_paths: bool,
698
699    /// Generate LLM constraints (R9)
700    #[arg(long)]
701    pub constraints: bool,
702
703    /// Output summary statistics only
704    #[arg(long)]
705    pub summary: bool,
706
707    /// Output format (json or text). Prefer global --format/-f flag.
708    #[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    /// Project root for path validation (optional)
718    #[arg(long)]
719    pub project_root: Option<PathBuf>,
720}
721
722impl ResourcesArgs {
723    /// Run the resources analysis command
724    pub fn run(&self, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
725        run(self.clone(), global_format)
726    }
727}
728
729// =============================================================================
730// Basic Block and Simplified CFG
731// =============================================================================
732
733/// A basic block in the simplified control flow graph.
734#[derive(Debug, Clone)]
735pub struct BasicBlock {
736    /// Unique block identifier
737    pub id: usize,
738    /// Statement nodes in this block (start_byte, end_byte, kind, text)
739    pub stmts: Vec<(usize, usize, String, String)>,
740    /// Line numbers of statements
741    pub lines: Vec<u32>,
742    /// Predecessor block IDs
743    pub preds: Vec<usize>,
744    /// Successor block IDs
745    pub succs: Vec<usize>,
746    /// Whether this is an entry block
747    pub is_entry: bool,
748    /// Whether this is an exit block (return/raise/implicit)
749    pub is_exit: bool,
750    /// Exception handler block IDs (for try blocks)
751    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/// Simplified control flow graph for resource analysis.
770#[derive(Debug)]
771pub struct SimpleCfg {
772    /// Mapping from block ID to BasicBlock
773    pub blocks: HashMap<usize, BasicBlock>,
774    /// ID of the entry block
775    pub entry_block: usize,
776    /// IDs of all exit blocks
777    pub exit_blocks: Vec<usize>,
778    /// Next available block ID
779    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
822// =============================================================================
823// CFG Builder
824// =============================================================================
825
826/// Build a simplified CFG from a function AST.
827pub 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    // Find the function body
837    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            // Mark implicit exit if we have a non-exit block at the end
845            if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
846                cfg.mark_exit(exit);
847            }
848        }
849    } else {
850        // Empty function
851        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            // Simple statements - add to current block
867            "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(&current) {
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                // Handle exit statements
894                if child.kind() == "return_statement" || child.kind() == "raise_statement" {
895                    cfg.mark_exit(current);
896                    return None; // No more statements can be executed
897                }
898            }
899
900            // If statement - creates branches
901            "if_statement" => {
902                current = process_if_statement(cfg, child, source, current)?;
903            }
904
905            // For/while loops
906            "for_statement" | "while_statement" => {
907                current = process_loop(cfg, child, source, current)?;
908            }
909
910            // Try statement
911            "try_statement" => {
912                current = process_try(cfg, child, source, current)?;
913            }
914
915            // With statement (context manager)
916            "with_statement" => {
917                current = process_with(cfg, child, source, current)?;
918            }
919
920            _ => {
921                // Unknown or compound statement - add as is
922                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(&current) {
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    // Add the condition to current block
947    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(&current) {
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    // Create blocks for true branch
962    let true_block = cfg.new_block();
963    cfg.add_edge(current, true_block);
964
965    // Find consequence block
966    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    // Find alternative (else/elif)
975    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        // Find the block within else/elif
985        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        // No else clause - false branch goes to next block
992        None
993    };
994
995    // Create merge block
996    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        // If no else, false path goes directly from current to merge
1006        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    // Create header block
1014    let header = cfg.new_block();
1015    cfg.add_edge(current, header);
1016
1017    // Add loop condition to header
1018    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    // Create body block
1033    let body_block = cfg.new_block();
1034    cfg.add_edge(header, body_block);
1035
1036    // Process body
1037    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    // Back edge from body to header
1047    if let Some(be) = body_exit {
1048        cfg.add_edge(be, header);
1049    }
1050
1051    // Exit block
1052    let exit = cfg.new_block();
1053    cfg.add_edge(header, exit); // Loop can exit when condition is false
1054
1055    Some(exit)
1056}
1057
1058fn process_try(cfg: &mut SimpleCfg, node: Node, source: &[u8], current: usize) -> Option<usize> {
1059    // Create try block
1060    let try_block = cfg.new_block();
1061    cfg.add_edge(current, try_block);
1062
1063    // Find and process try body
1064    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    // Find except handlers
1074    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            // Exception edge from try block
1080            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            // Process handler body
1086            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    // Find finally clause
1100    let finally_clause = node
1101        .children(&mut node.walk())
1102        .find(|n| n.kind() == "finally_clause");
1103
1104    // Create merge block
1105    let merge = cfg.new_block();
1106
1107    if let Some(te) = try_exit {
1108        if let Some(finally) = finally_clause {
1109            // Process finally
1110            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    // Add with statement to current block (marks context manager entry)
1136    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(&current) {
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    // Process the with body
1149    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// =============================================================================
1160// Resource Detection
1161// =============================================================================
1162
1163/// Detected resource information during analysis.
1164#[derive(Debug, Clone)]
1165struct DetectedResource {
1166    /// Variable name holding the resource
1167    name: String,
1168    /// Type of resource
1169    resource_type: String,
1170    /// Line where resource was created
1171    line: u32,
1172    /// Whether it's inside a context manager (with statement)
1173    in_context_manager: bool,
1174}
1175
1176/// Resource detector for finding must-close resources.
1177pub struct ResourceDetector {
1178    resources: Vec<DetectedResource>,
1179    context_manager_vars: HashSet<String>,
1180    lang: Language,
1181}
1182
1183impl ResourceDetector {
1184    pub fn new() -> Self {
1185        Self {
1186            resources: Vec::new(),
1187            context_manager_vars: HashSet::new(),
1188            lang: Language::Python,
1189        }
1190    }
1191
1192    pub fn with_language(lang: Language) -> Self {
1193        Self {
1194            resources: Vec::new(),
1195            context_manager_vars: HashSet::new(),
1196            lang,
1197        }
1198    }
1199
1200    /// Detect resources in a function (legacy Python-only).
1201    pub fn detect(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1202        self.resources.clear();
1203        self.context_manager_vars.clear();
1204        self.visit_node(func_node, source, false);
1205
1206        self.resources
1207            .iter()
1208            .map(|r| ResourceInfo {
1209                name: r.name.clone(),
1210                resource_type: r.resource_type.clone(),
1211                line: r.line,
1212                closed: r.in_context_manager,
1213            })
1214            .collect()
1215    }
1216
1217    /// Detect resources using language-specific patterns.
1218    pub fn detect_with_patterns(&mut self, func_node: Node, source: &[u8]) -> Vec<ResourceInfo> {
1219        let patterns = get_resource_patterns(self.lang);
1220        self.resources.clear();
1221        self.context_manager_vars.clear();
1222        self.visit_node_multilang(func_node, source, false, &patterns);
1223
1224        self.resources
1225            .iter()
1226            .map(|r| ResourceInfo {
1227                name: r.name.clone(),
1228                resource_type: r.resource_type.clone(),
1229                line: r.line,
1230                closed: r.in_context_manager,
1231            })
1232            .collect()
1233    }
1234
1235    fn visit_node(&mut self, node: Node, source: &[u8], in_with: bool) {
1236        match node.kind() {
1237            "with_statement" => {
1238                // Process with_items - they're direct children of with_statement
1239                let mut cursor = node.walk();
1240                for child in node.children(&mut cursor) {
1241                    if child.kind() == "with_item" {
1242                        self.visit_with_item(child, source);
1243                    } else if child.kind() == "with_clause" {
1244                        // Some Python versions use with_clause wrapper
1245                        let mut inner_cursor = child.walk();
1246                        for item in child.children(&mut inner_cursor) {
1247                            if item.kind() == "with_item" {
1248                                self.visit_with_item(item, source);
1249                            }
1250                        }
1251                    }
1252                }
1253                // Recurse into body with context manager flag
1254                let mut cursor = node.walk();
1255                for child in node.children(&mut cursor) {
1256                    self.visit_node(child, source, true);
1257                }
1258            }
1259            "assignment" => {
1260                self.check_assignment(node, source, in_with);
1261            }
1262            _ => {
1263                // Recurse
1264                let mut cursor = node.walk();
1265                for child in node.children(&mut cursor) {
1266                    self.visit_node(child, source, in_with);
1267                }
1268            }
1269        }
1270    }
1271
1272    fn visit_with_item(&mut self, node: Node, source: &[u8]) {
1273        // with_item structure in tree-sitter-python:
1274        //   with_item
1275        //     as_pattern
1276        //       call (the expression, e.g., open(path))
1277        //       as_pattern_target
1278        //         identifier (the variable name, e.g., f)
1279        //
1280        // OR (for with expression without 'as'):
1281        //   with_item
1282        //     call (the expression only)
1283
1284        // First check for as_pattern (with ... as var)
1285        let mut cursor = node.walk();
1286        for child in node.children(&mut cursor) {
1287            if child.kind() == "as_pattern" {
1288                let mut as_cursor = child.walk();
1289                let mut call_node: Option<Node> = None;
1290                let mut target_node: Option<Node> = None;
1291
1292                for as_child in child.children(&mut as_cursor) {
1293                    if as_child.kind() == "call" {
1294                        call_node = Some(as_child);
1295                    } else if as_child.kind() == "as_pattern_target" {
1296                        // as_pattern_target contains the identifier
1297                        if let Some(ident) = as_child.child(0) {
1298                            if ident.kind() == "identifier" {
1299                                target_node = Some(ident);
1300                            }
1301                        }
1302                    }
1303                }
1304
1305                if let (Some(call), Some(target)) = (call_node, target_node) {
1306                    let var_name = node_text(target, source).to_string();
1307                    self.context_manager_vars.insert(var_name.clone());
1308
1309                    if let Some(resource_type) = self.get_resource_type_from_call(call, source) {
1310                        self.resources.push(DetectedResource {
1311                            name: var_name,
1312                            resource_type,
1313                            line: node.start_position().row as u32 + 1,
1314                            in_context_manager: true,
1315                        });
1316                    }
1317                }
1318            }
1319        }
1320
1321        // Also try field names for older tree-sitter versions
1322        if let Some(target) = node.child_by_field_name("alias") {
1323            let var_name = node_text(target, source).to_string();
1324            if !self.context_manager_vars.contains(&var_name) {
1325                self.context_manager_vars.insert(var_name.clone());
1326
1327                if let Some(value) = node.child_by_field_name("value") {
1328                    if let Some(resource_type) = self.get_resource_type_from_call(value, source) {
1329                        self.resources.push(DetectedResource {
1330                            name: var_name,
1331                            resource_type,
1332                            line: node.start_position().row as u32 + 1,
1333                            in_context_manager: true,
1334                        });
1335                    }
1336                }
1337            }
1338        }
1339    }
1340
1341    fn check_assignment(&mut self, node: Node, source: &[u8], in_with: bool) {
1342        // f = open(...)
1343        if let Some(left) = node.child_by_field_name("left") {
1344            if let Some(right) = node.child_by_field_name("right") {
1345                let var_name = node_text(left, source).to_string();
1346
1347                if let Some(resource_type) = self.get_resource_type_from_call(right, source) {
1348                    let in_context = in_with || self.context_manager_vars.contains(&var_name);
1349                    self.resources.push(DetectedResource {
1350                        name: var_name,
1351                        resource_type,
1352                        line: node.start_position().row as u32 + 1,
1353                        in_context_manager: in_context,
1354                    });
1355                }
1356            }
1357        }
1358    }
1359
1360    fn get_resource_type_from_call(&self, node: Node, source: &[u8]) -> Option<String> {
1361        if node.kind() != "call" {
1362            return None;
1363        }
1364
1365        // Get function name
1366        let func = node.child_by_field_name("function")?;
1367        let func_text = node_text(func, source);
1368
1369        // Extract just the function name from attribute access (e.g., "sqlite3.connect" -> "connect")
1370        let func_name = func_text.split('.').next_back().unwrap_or(func_text);
1371
1372        // Check if it's a resource creator
1373        for &creator in RESOURCE_CREATORS {
1374            if func_name == creator {
1375                // Find the resource type from the type map
1376                for &(name, rtype) in RESOURCE_TYPE_MAP {
1377                    if func_name == name {
1378                        return Some(rtype.to_string());
1379                    }
1380                }
1381                // Default to the function name as type
1382                return Some(func_name.to_string());
1383            }
1384        }
1385
1386        None
1387    }
1388
1389    // =========================================================================
1390    // Multi-language methods
1391    // =========================================================================
1392
1393    fn visit_node_multilang(
1394        &mut self,
1395        node: Node,
1396        source: &[u8],
1397        in_cleanup: bool,
1398        patterns: &LangResourcePatterns,
1399    ) {
1400        let kind = node.kind();
1401
1402        // Check for cleanup block kinds (with, defer, using, try-with-resources)
1403        if patterns.cleanup_block_kinds.contains(&kind) {
1404            match self.lang {
1405                Language::Python => {
1406                    // Python with_statement: check for with_item children
1407                    let mut cursor = node.walk();
1408                    for child in node.children(&mut cursor) {
1409                        if child.kind() == "with_item" {
1410                            self.visit_with_item(child, source);
1411                        } else if child.kind() == "with_clause" {
1412                            let mut inner_cursor = child.walk();
1413                            for item in child.children(&mut inner_cursor) {
1414                                if item.kind() == "with_item" {
1415                                    self.visit_with_item(item, source);
1416                                }
1417                            }
1418                        }
1419                    }
1420                    // Recurse into body with cleanup flag
1421                    let mut cursor = node.walk();
1422                    for child in node.children(&mut cursor) {
1423                        self.visit_node_multilang(child, source, true, patterns);
1424                    }
1425                    return;
1426                }
1427                Language::Go => {
1428                    // Go defer: mark any resource in the defer as cleanup-managed
1429                    // We just recurse with in_cleanup=true
1430                    let mut cursor = node.walk();
1431                    for child in node.children(&mut cursor) {
1432                        self.visit_node_multilang(child, source, true, patterns);
1433                    }
1434                    return;
1435                }
1436                Language::CSharp => {
1437                    // C# using statement: resources are auto-disposed
1438                    let mut cursor = node.walk();
1439                    for child in node.children(&mut cursor) {
1440                        self.visit_node_multilang(child, source, true, patterns);
1441                    }
1442                    return;
1443                }
1444                Language::Java => {
1445                    // Java try-with-resources: resources in the resource spec are auto-closed
1446                    let mut cursor = node.walk();
1447                    for child in node.children(&mut cursor) {
1448                        self.visit_node_multilang(child, source, true, patterns);
1449                    }
1450                    return;
1451                }
1452                _ => {}
1453            }
1454        }
1455
1456        // Check for assignment kinds
1457        if patterns.assignment_kinds.contains(&kind) {
1458            self.check_assignment_multilang(node, source, in_cleanup, patterns);
1459        }
1460
1461        // Recurse
1462        let mut cursor = node.walk();
1463        for child in node.children(&mut cursor) {
1464            self.visit_node_multilang(child, source, in_cleanup, patterns);
1465        }
1466    }
1467
1468    fn check_assignment_multilang(
1469        &mut self,
1470        node: Node,
1471        source: &[u8],
1472        in_cleanup: bool,
1473        patterns: &LangResourcePatterns,
1474    ) {
1475        match self.lang {
1476            Language::Python => {
1477                // f = open(...)
1478                if let Some(left) = node.child_by_field_name("left") {
1479                    if let Some(right) = node.child_by_field_name("right") {
1480                        let var_name = node_text(left, source).to_string();
1481                        if let Some(resource_type) =
1482                            self.get_resource_type_from_call_multilang(right, source, patterns)
1483                        {
1484                            let in_context =
1485                                in_cleanup || self.context_manager_vars.contains(&var_name);
1486                            self.resources.push(DetectedResource {
1487                                name: var_name,
1488                                resource_type,
1489                                line: node.start_position().row as u32 + 1,
1490                                in_context_manager: in_context,
1491                            });
1492                        }
1493                    }
1494                }
1495            }
1496            Language::Go => {
1497                // Go: f, err := os.Open(...) or f := os.Open(...)
1498                // short_var_declaration has left and right fields
1499                // assignment_statement has left and right fields
1500                if let Some(left) = node.child_by_field_name("left") {
1501                    if let Some(right) = node.child_by_field_name("right") {
1502                        // left might be an expression_list with multiple identifiers
1503                        let var_name = if left.kind() == "expression_list" {
1504                            // Take first identifier
1505                            left.child(0).map(|c| node_text(c, source).to_string())
1506                        } else {
1507                            Some(node_text(left, source).to_string())
1508                        };
1509                        if let Some(var_name) = var_name {
1510                            if var_name != "_" && var_name != "err" {
1511                                // Check right side - may be expression_list too
1512                                let call_node = if right.kind() == "expression_list" {
1513                                    right.child(0)
1514                                } else {
1515                                    Some(right)
1516                                };
1517                                if let Some(call_node) = call_node {
1518                                    if let Some(resource_type) = self
1519                                        .get_resource_type_from_call_multilang(
1520                                            call_node, source, patterns,
1521                                        )
1522                                    {
1523                                        self.resources.push(DetectedResource {
1524                                            name: var_name,
1525                                            resource_type,
1526                                            line: node.start_position().row as u32 + 1,
1527                                            in_context_manager: in_cleanup,
1528                                        });
1529                                    }
1530                                }
1531                            }
1532                        }
1533                    }
1534                }
1535            }
1536            Language::Rust => {
1537                // let f = File::open(...)?;
1538                // let_declaration has pattern and value fields
1539                if let Some(pattern) = node.child_by_field_name("pattern") {
1540                    if let Some(value) = node.child_by_field_name("value") {
1541                        let var_name = node_text(pattern, source).to_string();
1542                        // Rust uses RAII, so most resources are auto-cleaned.
1543                        // We detect them but mark as closed (RAII)
1544                        if let Some(resource_type) =
1545                            self.get_resource_type_from_call_multilang(value, source, patterns)
1546                        {
1547                            self.resources.push(DetectedResource {
1548                                name: var_name,
1549                                resource_type,
1550                                line: node.start_position().row as u32 + 1,
1551                                in_context_manager: true, // RAII: auto-cleaned on drop
1552                            });
1553                        }
1554                    }
1555                }
1556            }
1557            Language::Java | Language::CSharp => {
1558                // Type var = new Resource(...);
1559                // local_variable_declaration contains declarator children
1560                let mut cursor = node.walk();
1561                for child in node.children(&mut cursor) {
1562                    if child.kind() == "variable_declarator" {
1563                        if let Some(name_node) = child.child_by_field_name("name") {
1564                            if let Some(value) = child.child_by_field_name("value") {
1565                                let var_name = node_text(name_node, source).to_string();
1566                                if let Some(resource_type) = self
1567                                    .get_resource_type_from_call_multilang(value, source, patterns)
1568                                {
1569                                    self.resources.push(DetectedResource {
1570                                        name: var_name,
1571                                        resource_type,
1572                                        line: node.start_position().row as u32 + 1,
1573                                        in_context_manager: in_cleanup,
1574                                    });
1575                                }
1576                            }
1577                        }
1578                    }
1579                }
1580            }
1581            Language::TypeScript | Language::JavaScript => {
1582                // const f = fs.open(...); or let f = ...
1583                // variable_declaration / lexical_declaration contain variable_declarator children
1584                let mut cursor = node.walk();
1585                for child in node.children(&mut cursor) {
1586                    if child.kind() == "variable_declarator" {
1587                        if let Some(name_node) = child.child_by_field_name("name") {
1588                            if let Some(value) = child.child_by_field_name("value") {
1589                                let var_name = node_text(name_node, source).to_string();
1590                                if let Some(resource_type) = self
1591                                    .get_resource_type_from_call_multilang(value, source, patterns)
1592                                {
1593                                    self.resources.push(DetectedResource {
1594                                        name: var_name,
1595                                        resource_type,
1596                                        line: node.start_position().row as u32 + 1,
1597                                        in_context_manager: in_cleanup,
1598                                    });
1599                                }
1600                            }
1601                        }
1602                    }
1603                }
1604                // Also handle assignment_expression: f = open(...)
1605                if node.kind() == "assignment_expression" {
1606                    if let Some(left) = node.child_by_field_name("left") {
1607                        if let Some(right) = node.child_by_field_name("right") {
1608                            let var_name = node_text(left, source).to_string();
1609                            if let Some(resource_type) =
1610                                self.get_resource_type_from_call_multilang(right, source, patterns)
1611                            {
1612                                self.resources.push(DetectedResource {
1613                                    name: var_name,
1614                                    resource_type,
1615                                    line: node.start_position().row as u32 + 1,
1616                                    in_context_manager: in_cleanup,
1617                                });
1618                            }
1619                        }
1620                    }
1621                }
1622            }
1623            Language::C | Language::Cpp => {
1624                // FILE *f = fopen(...); or void *p = malloc(...);
1625                // declaration contains init_declarator children
1626                let mut cursor = node.walk();
1627                for child in node.children(&mut cursor) {
1628                    if child.kind() == "init_declarator" {
1629                        if let Some(declarator) = child.child_by_field_name("declarator") {
1630                            if let Some(value) = child.child_by_field_name("value") {
1631                                // declarator might be a pointer_declarator wrapping an identifier
1632                                let var_name = extract_c_declarator_name(declarator, source);
1633                                if let Some(var_name) = var_name {
1634                                    if let Some(resource_type) = self
1635                                        .get_resource_type_from_call_multilang(
1636                                            value, source, patterns,
1637                                        )
1638                                    {
1639                                        self.resources.push(DetectedResource {
1640                                            name: var_name,
1641                                            resource_type,
1642                                            line: node.start_position().row as u32 + 1,
1643                                            in_context_manager: in_cleanup,
1644                                        });
1645                                    }
1646                                }
1647                            }
1648                        }
1649                    }
1650                }
1651                // Also handle assignment_expression
1652                if node.kind() == "assignment_expression" {
1653                    if let Some(left) = node.child_by_field_name("left") {
1654                        if let Some(right) = node.child_by_field_name("right") {
1655                            let var_name = node_text(left, source).to_string();
1656                            if let Some(resource_type) =
1657                                self.get_resource_type_from_call_multilang(right, source, patterns)
1658                            {
1659                                self.resources.push(DetectedResource {
1660                                    name: var_name,
1661                                    resource_type,
1662                                    line: node.start_position().row as u32 + 1,
1663                                    in_context_manager: in_cleanup,
1664                                });
1665                            }
1666                        }
1667                    }
1668                }
1669            }
1670            Language::Kotlin => {
1671                // Kotlin: val reader = BufferedReader(FileReader(path))
1672                // property_declaration has variable_declaration children with name/value
1673                // or assignment has left/right
1674                if node.kind() == "property_declaration" {
1675                    let mut cursor = node.walk();
1676                    for child in node.children(&mut cursor) {
1677                        if child.kind() == "variable_declaration" {
1678                            if let Some(name_node) =
1679                                child.child_by_field_name("name").or_else(|| child.child(0))
1680                            {
1681                                let var_name = node_text(name_node, source).to_string();
1682                                // The initializer/value is a sibling after the '='
1683                                // In Kotlin tree-sitter, the value/expression follows the property_declaration's delegation_specifier or directly
1684                                // Check remaining children for call expressions
1685                                let mut inner_cursor = node.walk();
1686                                for sibling in node.children(&mut inner_cursor) {
1687                                    if let Some(resource_type) = self
1688                                        .get_resource_type_from_call_multilang(
1689                                            sibling, source, patterns,
1690                                        )
1691                                    {
1692                                        self.resources.push(DetectedResource {
1693                                            name: var_name.clone(),
1694                                            resource_type,
1695                                            line: node.start_position().row as u32 + 1,
1696                                            in_context_manager: in_cleanup,
1697                                        });
1698                                        break;
1699                                    }
1700                                }
1701                            }
1702                        }
1703                    }
1704                } else if node.kind() == "assignment" {
1705                    if let Some(left) = node.child_by_field_name("left").or_else(|| node.child(0)) {
1706                        if let Some(right) = node.child_by_field_name("right") {
1707                            let var_name = node_text(left, source).to_string();
1708                            if let Some(resource_type) =
1709                                self.get_resource_type_from_call_multilang(right, source, patterns)
1710                            {
1711                                self.resources.push(DetectedResource {
1712                                    name: var_name,
1713                                    resource_type,
1714                                    line: node.start_position().row as u32 + 1,
1715                                    in_context_manager: in_cleanup,
1716                                });
1717                            }
1718                        }
1719                    }
1720                }
1721            }
1722            Language::Swift => {
1723                // Swift: let handle = FileHandle(forReadingAtPath: path)!
1724                // property_declaration has pattern (name) and value (expression)
1725                if node.kind() == "property_declaration"
1726                    || node.kind() == "directly_assignable_expression"
1727                {
1728                    if let Some(pattern) = node
1729                        .child_by_field_name("pattern")
1730                        .or_else(|| node.child_by_field_name("name"))
1731                    {
1732                        let var_name = node_text(pattern, source).to_string();
1733                        // Check all children for call expressions (value may be force-unwrapped, etc.)
1734                        let mut cursor = node.walk();
1735                        for child in node.children(&mut cursor) {
1736                            if let Some(resource_type) =
1737                                self.get_resource_type_from_call_multilang(child, source, patterns)
1738                            {
1739                                self.resources.push(DetectedResource {
1740                                    name: var_name.clone(),
1741                                    resource_type,
1742                                    line: node.start_position().row as u32 + 1,
1743                                    in_context_manager: in_cleanup,
1744                                });
1745                                break;
1746                            }
1747                        }
1748                    }
1749                }
1750            }
1751            Language::Ocaml => {
1752                // OCaml: let ic = open_in path in ...
1753                // let_binding has pattern (value_name) and body (application / expression)
1754                if node.kind() == "let_binding" {
1755                    if let Some(pattern) = node.child_by_field_name("pattern") {
1756                        let var_name = node_text(pattern, source).to_string();
1757                        // Check body for resource creation
1758                        if let Some(body) = node.child_by_field_name("body") {
1759                            if let Some(resource_type) =
1760                                self.get_resource_type_from_call_multilang(body, source, patterns)
1761                            {
1762                                self.resources.push(DetectedResource {
1763                                    name: var_name,
1764                                    resource_type,
1765                                    line: node.start_position().row as u32 + 1,
1766                                    in_context_manager: in_cleanup,
1767                                });
1768                            }
1769                        }
1770                    }
1771                }
1772            }
1773            Language::Lua | Language::Luau => {
1774                // Lua/Luau: local f = io.open(path, "r")
1775                // assignment_statement has variable_list and expression_list
1776                // variable_declaration has assignment with variable_list and expression_list
1777                if let Some(right) = node
1778                    .child_by_field_name("values")
1779                    .or_else(|| node.child_by_field_name("right"))
1780                {
1781                    if let Some(left) = node
1782                        .child_by_field_name("variables")
1783                        .or_else(|| node.child_by_field_name("left"))
1784                        .or_else(|| node.child_by_field_name("name"))
1785                    {
1786                        // left is usually a variable_list containing identifier(s)
1787                        let var_name =
1788                            if left.kind() == "variable_list" || left.kind() == "identifier_list" {
1789                                left.child(0).map(|c| node_text(c, source).to_string())
1790                            } else {
1791                                Some(node_text(left, source).to_string())
1792                            };
1793                        if let Some(var_name) = var_name {
1794                            // right is usually an expression_list
1795                            let call_node = if right.kind() == "expression_list" {
1796                                right.child(0)
1797                            } else {
1798                                Some(right)
1799                            };
1800                            if let Some(call_node) = call_node {
1801                                if let Some(resource_type) = self
1802                                    .get_resource_type_from_call_multilang(
1803                                        call_node, source, patterns,
1804                                    )
1805                                {
1806                                    self.resources.push(DetectedResource {
1807                                        name: var_name,
1808                                        resource_type,
1809                                        line: node.start_position().row as u32 + 1,
1810                                        in_context_manager: in_cleanup,
1811                                    });
1812                                }
1813                            }
1814                        }
1815                    }
1816                }
1817            }
1818            _ => {
1819                // Generic fallback: try left/right fields
1820                if let Some(left) = node.child_by_field_name("left") {
1821                    if let Some(right) = node.child_by_field_name("right") {
1822                        let var_name = node_text(left, source).to_string();
1823                        if let Some(resource_type) =
1824                            self.get_resource_type_from_call_multilang(right, source, patterns)
1825                        {
1826                            self.resources.push(DetectedResource {
1827                                name: var_name,
1828                                resource_type,
1829                                line: node.start_position().row as u32 + 1,
1830                                in_context_manager: in_cleanup,
1831                            });
1832                        }
1833                    }
1834                }
1835            }
1836        }
1837    }
1838
1839    /// Multi-language resource type detection from call expressions.
1840    fn get_resource_type_from_call_multilang(
1841        &self,
1842        node: Node,
1843        source: &[u8],
1844        patterns: &LangResourcePatterns,
1845    ) -> Option<String> {
1846        // Extract the function/method name from the call
1847        let func_name = extract_call_name(node, source)?;
1848
1849        // Check against language-specific creator patterns
1850        for &(creator, rtype) in patterns.creators {
1851            if func_name == creator
1852                || func_name.ends_with(&format!("::{}", creator))
1853                || func_name.ends_with(&format!(".{}", creator))
1854            {
1855                return Some(rtype.to_string());
1856            }
1857        }
1858
1859        // For C/C++: also check for new/malloc at the call level
1860        if matches!(self.lang, Language::C | Language::Cpp) {
1861            if node.kind() == "call_expression" {
1862                let text = node_text(node, source);
1863                for &(creator, rtype) in patterns.creators {
1864                    if text.starts_with(creator) {
1865                        return Some(rtype.to_string());
1866                    }
1867                }
1868            }
1869            // Check for `new` expressions in C++
1870            if node.kind() == "new_expression" {
1871                return Some("heap_object".to_string());
1872            }
1873        }
1874
1875        // For Kotlin: check for constructor calls like BufferedReader(FileReader(path))
1876        if matches!(self.lang, Language::Kotlin) {
1877            // Kotlin constructors look like function calls in tree-sitter
1878            let text = node_text(node, source);
1879            for &(creator, rtype) in patterns.creators {
1880                if text.starts_with(creator) {
1881                    return Some(rtype.to_string());
1882                }
1883            }
1884        }
1885
1886        // For Swift: check for constructor calls like FileHandle(forReadingAtPath: path)
1887        if matches!(self.lang, Language::Swift) {
1888            let text = node_text(node, source);
1889            for &(creator, rtype) in patterns.creators {
1890                if text.starts_with(creator) {
1891                    return Some(rtype.to_string());
1892                }
1893            }
1894            // Also check force-unwrap: FileHandle(...)!
1895            if node.kind() == "force_unwrap_expression" || node.kind() == "try_expression" {
1896                if let Some(child) = node.child(0) {
1897                    return self.get_resource_type_from_call_multilang(child, source, patterns);
1898                }
1899            }
1900        }
1901
1902        // For OCaml: check for function application like `open_in path`
1903        if matches!(self.lang, Language::Ocaml) {
1904            // OCaml uses application nodes: (application function: (value_name) argument: ...)
1905            if node.kind() == "application" {
1906                if let Some(func_node) = node
1907                    .child_by_field_name("function")
1908                    .or_else(|| node.child(0))
1909                {
1910                    let func_text = node_text(func_node, source);
1911                    for &(creator, rtype) in patterns.creators {
1912                        if func_text == creator || func_text.ends_with(&format!(".{}", creator)) {
1913                            return Some(rtype.to_string());
1914                        }
1915                    }
1916                }
1917            }
1918            // Also check the raw text for patterns like `open_in`
1919            let text = node_text(node, source);
1920            let first_word = text.split_whitespace().next().unwrap_or("");
1921            for &(creator, rtype) in patterns.creators {
1922                if first_word == creator {
1923                    return Some(rtype.to_string());
1924                }
1925            }
1926        }
1927
1928        // For Lua/Luau: check for method calls like io.open(path, "r")
1929        if matches!(self.lang, Language::Lua | Language::Luau) {
1930            let text = node_text(node, source);
1931            for &(creator, rtype) in patterns.creators {
1932                if text.starts_with(creator) {
1933                    return Some(rtype.to_string());
1934                }
1935            }
1936        }
1937
1938        // For Java/C#: check for `new ClassName(...)` constructor calls
1939        if matches!(self.lang, Language::Java | Language::CSharp)
1940            && node.kind() == "object_creation_expression"
1941        {
1942            // Get the type name
1943            if let Some(type_node) = node.child_by_field_name("type") {
1944                let type_name = node_text(type_node, source);
1945                for &(creator, rtype) in patterns.creators {
1946                    if type_name == creator || type_name.contains(creator) {
1947                        return Some(rtype.to_string());
1948                    }
1949                }
1950            }
1951        }
1952
1953        None
1954    }
1955}
1956
1957impl Default for ResourceDetector {
1958    fn default() -> Self {
1959        Self::new()
1960    }
1961}
1962
1963// =============================================================================
1964// Leak Detection (TIGER-04)
1965// =============================================================================
1966
1967/// Leak detector using CFG path analysis.
1968pub struct LeakDetector {
1969    /// Maximum paths to enumerate (TIGER-04)
1970    max_paths: usize,
1971    /// Paths enumerated so far
1972    paths_enumerated: usize,
1973    /// Whether we hit the limit
1974    hit_limit: bool,
1975}
1976
1977impl LeakDetector {
1978    pub fn new() -> Self {
1979        Self {
1980            max_paths: MAX_PATHS,
1981            paths_enumerated: 0,
1982            hit_limit: false,
1983        }
1984    }
1985
1986    /// Detect potential leaks using CFG path analysis.
1987    pub fn detect(
1988        &mut self,
1989        cfg: &SimpleCfg,
1990        resources: &[ResourceInfo],
1991        source: &[u8],
1992        show_paths: bool,
1993    ) -> Vec<LeakInfo> {
1994        let mut leaks = Vec::new();
1995        self.paths_enumerated = 0;
1996        self.hit_limit = false;
1997
1998        for resource in resources {
1999            // Skip resources in context managers
2000            if resource.closed {
2001                continue;
2002            }
2003
2004            // Find all paths from resource creation to exits
2005            let paths = self.enumerate_paths(cfg, resource, source);
2006
2007            // Check if any path lacks a close
2008            for path in &paths {
2009                if !self.path_has_close(path, &resource.name) {
2010                    leaks.push(LeakInfo {
2011                        resource: resource.name.clone(),
2012                        line: resource.line,
2013                        paths: if show_paths {
2014                            Some(vec![self.format_path(path)])
2015                        } else {
2016                            None
2017                        },
2018                    });
2019                    break; // One leak path is enough per resource
2020                }
2021            }
2022        }
2023
2024        leaks
2025    }
2026
2027    /// Detect potential leaks using CFG path analysis (multi-language).
2028    /// Same logic as `detect` since the CFG is already language-aware.
2029    pub fn detect_multilang(
2030        &mut self,
2031        cfg: &SimpleCfg,
2032        resources: &[ResourceInfo],
2033        source: &[u8],
2034        show_paths: bool,
2035    ) -> Vec<LeakInfo> {
2036        self.detect(cfg, resources, source, show_paths)
2037    }
2038
2039    /// Enumerate paths from resource creation to exits (TIGER-04: with limit).
2040    fn enumerate_paths(
2041        &mut self,
2042        cfg: &SimpleCfg,
2043        resource: &ResourceInfo,
2044        _source: &[u8],
2045    ) -> Vec<Vec<usize>> {
2046        let mut paths = Vec::new();
2047
2048        // Find which block contains the resource creation
2049        let start_block = self.find_block_with_line(cfg, resource.line);
2050        if start_block.is_none() {
2051            return paths;
2052        }
2053        let start = start_block.unwrap();
2054
2055        // DFS to find all paths to exit blocks
2056        for &exit_id in &cfg.exit_blocks {
2057            if self.hit_limit {
2058                break;
2059            }
2060            self.find_paths_dfs(cfg, start, exit_id, &mut Vec::new(), &mut paths);
2061        }
2062
2063        paths
2064    }
2065
2066    fn find_block_with_line(&self, cfg: &SimpleCfg, line: u32) -> Option<usize> {
2067        for (id, block) in &cfg.blocks {
2068            if block.lines.contains(&line) {
2069                return Some(*id);
2070            }
2071        }
2072        // Default to entry block if not found
2073        Some(cfg.entry_block)
2074    }
2075
2076    fn find_paths_dfs(
2077        &mut self,
2078        cfg: &SimpleCfg,
2079        current: usize,
2080        target: usize,
2081        current_path: &mut Vec<usize>,
2082        paths: &mut Vec<Vec<usize>>,
2083    ) {
2084        // TIGER-04: Check path limit
2085        if self.paths_enumerated >= self.max_paths {
2086            self.hit_limit = true;
2087            return;
2088        }
2089
2090        // Cycle detection
2091        if current_path.contains(&current) {
2092            return;
2093        }
2094
2095        current_path.push(current);
2096
2097        if current == target {
2098            paths.push(current_path.clone());
2099            self.paths_enumerated += 1;
2100        } else if let Some(block) = cfg.blocks.get(&current) {
2101            for &succ in &block.succs {
2102                self.find_paths_dfs(cfg, succ, target, current_path, paths);
2103                if self.hit_limit {
2104                    break;
2105                }
2106            }
2107        }
2108
2109        current_path.pop();
2110    }
2111
2112    fn path_has_close(&self, path: &[usize], resource_name: &str) -> bool {
2113        // This is a simplified check - a real implementation would track
2114        // the resource state through the CFG
2115        // For now, we assume the path doesn't have a close
2116        // (proper implementation would look for close calls in each block)
2117        let _ = (path, resource_name);
2118        false
2119    }
2120
2121    fn format_path(&self, path: &[usize]) -> String {
2122        path.iter()
2123            .map(|id| id.to_string())
2124            .collect::<Vec<_>>()
2125            .join(" -> ")
2126    }
2127}
2128
2129impl Default for LeakDetector {
2130    fn default() -> Self {
2131        Self::new()
2132    }
2133}
2134
2135// =============================================================================
2136// Double-Close Detection
2137// =============================================================================
2138
2139/// Double-close detector.
2140pub struct DoubleCloseDetector {
2141    lang: Language,
2142}
2143
2144impl DoubleCloseDetector {
2145    pub fn new() -> Self {
2146        Self {
2147            lang: Language::Python,
2148        }
2149    }
2150
2151    pub fn with_language(lang: Language) -> Self {
2152        Self { lang }
2153    }
2154
2155    /// Detect double-close issues (legacy Python).
2156    pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2157        let mut issues = Vec::new();
2158        let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2159
2160        self.find_closes(func_node, source, &mut close_sites);
2161
2162        for (resource, lines) in close_sites {
2163            if lines.len() > 1 {
2164                issues.push(DoubleCloseInfo {
2165                    resource,
2166                    first_close: lines[0],
2167                    second_close: lines[1],
2168                });
2169            }
2170        }
2171
2172        issues
2173    }
2174
2175    /// Detect double-close issues with multi-language support.
2176    pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<DoubleCloseInfo> {
2177        let mut issues = Vec::new();
2178        let mut close_sites: HashMap<String, Vec<u32>> = HashMap::new();
2179        let patterns = get_resource_patterns(self.lang);
2180
2181        self.find_closes_multilang(func_node, source, &mut close_sites, &patterns);
2182
2183        for (resource, lines) in close_sites {
2184            if lines.len() > 1 {
2185                issues.push(DoubleCloseInfo {
2186                    resource,
2187                    first_close: lines[0],
2188                    second_close: lines[1],
2189                });
2190            }
2191        }
2192
2193        issues
2194    }
2195
2196    fn find_closes(&self, node: Node, source: &[u8], closes: &mut HashMap<String, Vec<u32>>) {
2197        if node.kind() == "call" {
2198            if let Some(func) = node.child_by_field_name("function") {
2199                if func.kind() == "attribute" {
2200                    if let Some(attr) = func.child_by_field_name("attribute") {
2201                        let method = node_text(attr, source);
2202                        if RESOURCE_CLOSERS.contains(&method) {
2203                            if let Some(obj) = func.child_by_field_name("object") {
2204                                let var_name = node_text(obj, source).to_string();
2205                                let line = node.start_position().row as u32 + 1;
2206                                closes.entry(var_name).or_default().push(line);
2207                            }
2208                        }
2209                    }
2210                }
2211            }
2212        }
2213
2214        let mut cursor = node.walk();
2215        for child in node.children(&mut cursor) {
2216            self.find_closes(child, source, closes);
2217        }
2218    }
2219
2220    fn find_closes_multilang(
2221        &self,
2222        node: Node,
2223        source: &[u8],
2224        closes: &mut HashMap<String, Vec<u32>>,
2225        patterns: &LangResourcePatterns,
2226    ) {
2227        let kind = node.kind();
2228        // Check for method call patterns: obj.close(), obj.Close(), fclose(obj), etc.
2229        if kind == "call"
2230            || kind == "call_expression"
2231            || kind == "method_invocation"
2232            || kind == "invocation_expression"
2233        {
2234            if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2235                if patterns.closers.contains(&method.as_str()) {
2236                    let line = node.start_position().row as u32 + 1;
2237                    closes.entry(var_name).or_default().push(line);
2238                }
2239            }
2240        }
2241
2242        let mut cursor = node.walk();
2243        for child in node.children(&mut cursor) {
2244            self.find_closes_multilang(child, source, closes, patterns);
2245        }
2246    }
2247}
2248
2249impl Default for DoubleCloseDetector {
2250    fn default() -> Self {
2251        Self::new()
2252    }
2253}
2254
2255// =============================================================================
2256// Use-After-Close Detection
2257// =============================================================================
2258
2259/// Use-after-close detector.
2260pub struct UseAfterCloseDetector {
2261    lang: Language,
2262}
2263
2264impl UseAfterCloseDetector {
2265    pub fn new() -> Self {
2266        Self {
2267            lang: Language::Python,
2268        }
2269    }
2270
2271    pub fn with_language(lang: Language) -> Self {
2272        Self { lang }
2273    }
2274
2275    /// Detect use-after-close issues (legacy Python).
2276    pub fn detect(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2277        let mut issues = Vec::new();
2278        let mut close_lines: HashMap<String, u32> = HashMap::new();
2279        let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2280
2281        self.analyze(func_node, source, &mut close_lines, &mut uses_after_close);
2282
2283        for (resource, close_line, use_line) in uses_after_close {
2284            issues.push(UseAfterCloseInfo {
2285                resource,
2286                close_line,
2287                use_line,
2288            });
2289        }
2290
2291        issues
2292    }
2293
2294    /// Detect use-after-close issues with multi-language support.
2295    pub fn detect_multilang(&self, func_node: Node, source: &[u8]) -> Vec<UseAfterCloseInfo> {
2296        let mut issues = Vec::new();
2297        let mut close_lines: HashMap<String, u32> = HashMap::new();
2298        let mut uses_after_close: Vec<(String, u32, u32)> = Vec::new();
2299        let patterns = get_resource_patterns(self.lang);
2300
2301        self.analyze_multilang(
2302            func_node,
2303            source,
2304            &mut close_lines,
2305            &mut uses_after_close,
2306            &patterns,
2307        );
2308
2309        for (resource, close_line, use_line) in uses_after_close {
2310            issues.push(UseAfterCloseInfo {
2311                resource,
2312                close_line,
2313                use_line,
2314            });
2315        }
2316
2317        issues
2318    }
2319
2320    fn analyze(
2321        &self,
2322        node: Node,
2323        source: &[u8],
2324        close_lines: &mut HashMap<String, u32>,
2325        uses_after: &mut Vec<(String, u32, u32)>,
2326    ) {
2327        let line = node.start_position().row as u32 + 1;
2328
2329        if node.kind() == "call" {
2330            if let Some(func) = node.child_by_field_name("function") {
2331                if func.kind() == "attribute" {
2332                    if let Some(attr) = func.child_by_field_name("attribute") {
2333                        let method = node_text(attr, source);
2334                        if RESOURCE_CLOSERS.contains(&method) {
2335                            if let Some(obj) = func.child_by_field_name("object") {
2336                                let var_name = node_text(obj, source).to_string();
2337                                close_lines.insert(var_name, line);
2338                            }
2339                        } else if let Some(obj) = func.child_by_field_name("object") {
2340                            let var_name = node_text(obj, source).to_string();
2341                            if let Some(&close_line) = close_lines.get(&var_name) {
2342                                if line > close_line {
2343                                    uses_after.push((var_name, close_line, line));
2344                                }
2345                            }
2346                        }
2347                    }
2348                }
2349            }
2350        }
2351
2352        if node.kind() == "attribute" {
2353            if let Some(obj) = node.child_by_field_name("object") {
2354                if obj.kind() == "identifier" {
2355                    let var_name = node_text(obj, source).to_string();
2356                    if let Some(&close_line) = close_lines.get(&var_name) {
2357                        if line > close_line {
2358                            uses_after.push((var_name, close_line, line));
2359                        }
2360                    }
2361                }
2362            }
2363        }
2364
2365        let mut cursor = node.walk();
2366        for child in node.children(&mut cursor) {
2367            self.analyze(child, source, close_lines, uses_after);
2368        }
2369    }
2370
2371    fn analyze_multilang(
2372        &self,
2373        node: Node,
2374        source: &[u8],
2375        close_lines: &mut HashMap<String, u32>,
2376        uses_after: &mut Vec<(String, u32, u32)>,
2377        patterns: &LangResourcePatterns,
2378    ) {
2379        let line = node.start_position().row as u32 + 1;
2380        let kind = node.kind();
2381
2382        // Check for close calls
2383        if kind == "call"
2384            || kind == "call_expression"
2385            || kind == "method_invocation"
2386            || kind == "invocation_expression"
2387        {
2388            if let Some((var_name, method)) = extract_close_call(node, source, self.lang) {
2389                if patterns.closers.contains(&method.as_str()) {
2390                    close_lines.insert(var_name, line);
2391                } else {
2392                    // Non-close method call on a variable - check if it's been closed
2393                    // Try to extract the object name
2394                    if let Some((obj_name, _)) = extract_close_call(node, source, self.lang) {
2395                        if let Some(&close_line) = close_lines.get(&obj_name) {
2396                            if line > close_line {
2397                                uses_after.push((obj_name, close_line, line));
2398                            }
2399                        }
2400                    }
2401                }
2402            }
2403        }
2404
2405        // Check for member access on closed resources
2406        if kind == "attribute"
2407            || kind == "member_expression"
2408            || kind == "field_expression"
2409            || kind == "selector_expression"
2410        {
2411            if let Some(obj) = node
2412                .child_by_field_name("object")
2413                .or_else(|| node.child_by_field_name("operand"))
2414                .or_else(|| node.child(0))
2415            {
2416                if obj.kind() == "identifier" {
2417                    let var_name = node_text(obj, source).to_string();
2418                    if let Some(&close_line) = close_lines.get(&var_name) {
2419                        if line > close_line {
2420                            uses_after.push((var_name, close_line, line));
2421                        }
2422                    }
2423                }
2424            }
2425        }
2426
2427        let mut cursor = node.walk();
2428        for child in node.children(&mut cursor) {
2429            self.analyze_multilang(child, source, close_lines, uses_after, patterns);
2430        }
2431    }
2432}
2433
2434impl Default for UseAfterCloseDetector {
2435    fn default() -> Self {
2436        Self::new()
2437    }
2438}
2439
2440// =============================================================================
2441// Context Manager Suggestions
2442// =============================================================================
2443
2444/// Suggest context manager usage for resources.
2445pub fn suggest_context_manager(resources: &[ResourceInfo]) -> Vec<ContextSuggestion> {
2446    resources
2447        .iter()
2448        .filter(|r| !r.closed) // Only suggest for non-context-managed resources
2449        .map(|r| {
2450            let suggestion = match r.resource_type.as_str() {
2451                "file" => format!("with open(...) as {}:", r.name),
2452                "connection" => format!("with connect(...) as {}:", r.name),
2453                "cursor" => format!("with connection.cursor() as {}:", r.name),
2454                "socket" => format!("with socket.socket(...) as {}:", r.name),
2455                _ => format!("with {} as {}:", r.resource_type, r.name),
2456            };
2457            ContextSuggestion {
2458                resource: r.name.clone(),
2459                suggestion,
2460            }
2461        })
2462        .collect()
2463}
2464
2465/// Suggest cleanup patterns using language-appropriate idioms.
2466pub fn suggest_context_manager_multilang(
2467    resources: &[ResourceInfo],
2468    lang: Language,
2469) -> Vec<ContextSuggestion> {
2470    resources
2471        .iter()
2472        .filter(|r| !r.closed)
2473        .map(|r| {
2474            let suggestion = match lang {
2475                Language::Python => match r.resource_type.as_str() {
2476                    "file" => format!("with open(...) as {}:", r.name),
2477                    "connection" => format!("with connect(...) as {}:", r.name),
2478                    "cursor" => format!("with connection.cursor() as {}:", r.name),
2479                    "socket" => format!("with socket.socket(...) as {}:", r.name),
2480                    _ => format!("with {} as {}:", r.resource_type, r.name),
2481                },
2482                Language::Go => format!("defer {}.Close()", r.name),
2483                Language::Rust => format!("// {}: Drop trait handles cleanup automatically. Consider wrapping in a scope block.", r.name),
2484                Language::Java => match r.resource_type.as_str() {
2485                    "file_stream" | "reader" | "writer" | "scanner" | "stream" =>
2486                        format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2487                    "connection" | "statement" =>
2488                        format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2489                    _ => format!("try ({} {} = ...) {{ ... }}", r.resource_type, r.name),
2490                },
2491                Language::CSharp => format!("using (var {} = ...) {{ ... }}", r.name),
2492                Language::TypeScript | Language::JavaScript =>
2493                    format!("try {{ ... }} finally {{ {}.close(); }}", r.name),
2494                Language::C => match r.resource_type.as_str() {
2495                    "file" => format!("// Ensure fclose({}) on all paths", r.name),
2496                    "memory" => format!("// Ensure free({}) on all paths", r.name),
2497                    _ => format!("// Ensure cleanup of {} on all paths", r.name),
2498                },
2499                Language::Cpp => match r.resource_type.as_str() {
2500                    "heap_object" => format!("// Use std::unique_ptr or std::shared_ptr instead of raw new for {}", r.name),
2501                    "memory" => format!("// Use RAII wrapper or smart pointer for {}", r.name),
2502                    _ => format!("// Consider RAII wrapper for {}", r.name),
2503                },
2504                Language::Ruby => format!("File.open(...) do |{}| ... end", r.name),
2505                Language::Php => format!("// Ensure {}() cleanup in finally block", r.name),
2506                Language::Kotlin => format!("{}.use {{ {} -> ... }}", r.name, r.name),
2507                Language::Swift => format!("defer {{ {}.closeFile() }}", r.name),
2508                Language::Ocaml => format!("Fun.protect ~finally:(fun () -> close_in {}) (fun () -> ...)", r.name),
2509                Language::Lua | Language::Luau => format!("// Ensure {}:close() is called, consider pcall for cleanup", r.name),
2510                _ => format!("// Ensure {} is properly closed/released", r.name),
2511            };
2512            ContextSuggestion {
2513                resource: r.name.clone(),
2514                suggestion,
2515            }
2516        })
2517        .collect()
2518}
2519
2520// =============================================================================
2521// Constraint Generation
2522// =============================================================================
2523
2524/// Generate LLM-ready constraints from resource analysis.
2525pub fn generate_constraints(
2526    file: &str,
2527    function: Option<&str>,
2528    resources: &[ResourceInfo],
2529    leaks: &[LeakInfo],
2530    double_closes: &[DoubleCloseInfo],
2531    use_after_closes: &[UseAfterCloseInfo],
2532) -> Vec<ResourceConstraint> {
2533    let mut constraints = Vec::new();
2534    let context = function.unwrap_or("module").to_string();
2535
2536    // Generate constraints for leaks
2537    for leak in leaks {
2538        constraints.push(ResourceConstraint {
2539            rule: format!(
2540                "Resource '{}' opened at line {} must be closed on all control flow paths",
2541                leak.resource, leak.line
2542            ),
2543            context: format!("{} in {}", context, file),
2544            confidence: 0.9,
2545        });
2546    }
2547
2548    // Generate constraints for double-closes
2549    for dc in double_closes {
2550        constraints.push(ResourceConstraint {
2551            rule: format!(
2552                "Resource '{}' must not be closed twice (lines {} and {})",
2553                dc.resource, dc.first_close, dc.second_close
2554            ),
2555            context: format!("{} in {}", context, file),
2556            confidence: 0.95,
2557        });
2558    }
2559
2560    // Generate constraints for use-after-close
2561    for uac in use_after_closes {
2562        constraints.push(ResourceConstraint {
2563            rule: format!(
2564                "Resource '{}' must not be used at line {} after being closed at line {}",
2565                uac.resource, uac.use_line, uac.close_line
2566            ),
2567            context: format!("{} in {}", context, file),
2568            confidence: 0.95,
2569        });
2570    }
2571
2572    // General resource usage patterns
2573    for resource in resources {
2574        if !resource.closed {
2575            constraints.push(ResourceConstraint {
2576                rule: format!(
2577                    "Resource '{}' ({}) should use context manager pattern (with statement)",
2578                    resource.name, resource.resource_type
2579                ),
2580                context: format!("{} in {}", context, file),
2581                confidence: 0.85,
2582            });
2583        }
2584    }
2585
2586    constraints
2587}
2588
2589// =============================================================================
2590// Output Formatting
2591// =============================================================================
2592
2593/// Format resources report as human-readable text.
2594pub fn format_resources_text(report: &ResourceReport) -> String {
2595    let mut lines = Vec::new();
2596
2597    lines.push(format!("Resource Analysis: {}", report.file));
2598    lines.push(format!("Language: {}", report.language));
2599    if let Some(ref func) = report.function {
2600        lines.push(format!("Function: {}", func));
2601    }
2602    lines.push(String::new());
2603
2604    // Resources
2605    lines.push(format!("Resources detected: {}", report.resources.len()));
2606    for r in &report.resources {
2607        let status = if r.closed { "closed" } else { "open" };
2608        lines.push(format!(
2609            "  - {}: {} at line {} [{}]",
2610            r.name, r.resource_type, r.line, status
2611        ));
2612    }
2613    lines.push(String::new());
2614
2615    // Leaks
2616    if !report.leaks.is_empty() {
2617        lines.push(format!("Leaks found: {}", report.leaks.len()));
2618        for leak in &report.leaks {
2619            lines.push(format!("  - {} at line {}", leak.resource, leak.line));
2620            if let Some(ref paths) = leak.paths {
2621                for path in paths {
2622                    lines.push(format!("    Path: {}", path));
2623                }
2624            }
2625        }
2626    } else {
2627        lines.push("Leaks found: 0".to_string());
2628    }
2629
2630    // Double closes
2631    if !report.double_closes.is_empty() {
2632        lines.push(String::new());
2633        lines.push(format!(
2634            "Double-close errors: {}",
2635            report.double_closes.len()
2636        ));
2637        for dc in &report.double_closes {
2638            lines.push(format!(
2639                "  - {}: first close at {}, second close at {}",
2640                dc.resource, dc.first_close, dc.second_close
2641            ));
2642        }
2643    }
2644
2645    // Use after close
2646    if !report.use_after_closes.is_empty() {
2647        lines.push(String::new());
2648        lines.push(format!(
2649            "Use-after-close errors: {}",
2650            report.use_after_closes.len()
2651        ));
2652        for uac in &report.use_after_closes {
2653            lines.push(format!(
2654                "  - {}: closed at {}, used at {}",
2655                uac.resource, uac.close_line, uac.use_line
2656            ));
2657        }
2658    }
2659
2660    // Suggestions
2661    if !report.suggestions.is_empty() {
2662        lines.push(String::new());
2663        lines.push(format!("Suggestions: {}", report.suggestions.len()));
2664        for s in &report.suggestions {
2665            lines.push(format!("  - {}: {}", s.resource, s.suggestion));
2666        }
2667    }
2668
2669    // Constraints
2670    if !report.constraints.is_empty() {
2671        lines.push(String::new());
2672        lines.push(format!("Constraints: {}", report.constraints.len()));
2673        for c in &report.constraints {
2674            lines.push(format!("  - {} (confidence: {:.2})", c.rule, c.confidence));
2675        }
2676    }
2677
2678    // Summary
2679    lines.push(String::new());
2680    lines.push("Summary:".to_string());
2681    lines.push(format!(
2682        "  resources_detected: {}",
2683        report.summary.resources_detected
2684    ));
2685    lines.push(format!("  leaks_found: {}", report.summary.leaks_found));
2686    lines.push(format!(
2687        "  double_closes_found: {}",
2688        report.summary.double_closes_found
2689    ));
2690    lines.push(format!(
2691        "  use_after_closes_found: {}",
2692        report.summary.use_after_closes_found
2693    ));
2694    lines.push(String::new());
2695    lines.push(format!(
2696        "Analysis completed in {}ms",
2697        report.analysis_time_ms
2698    ));
2699
2700    lines.join("\n")
2701}
2702
2703// =============================================================================
2704// Helper Functions
2705// =============================================================================
2706
2707fn node_text<'a>(node: Node, source: &'a [u8]) -> &'a str {
2708    std::str::from_utf8(&source[node.start_byte()..node.end_byte()]).unwrap_or("")
2709}
2710
2711/// Extract the function/method name from a call expression node.
2712/// Works across languages by checking various call node structures.
2713fn extract_call_name(node: Node, source: &[u8]) -> Option<String> {
2714    // Handle different call expression kinds across languages
2715    match node.kind() {
2716        // Python, JS/TS, Java, C#, Ruby, PHP
2717        "call" | "call_expression" | "method_invocation" | "invocation_expression" => {
2718            if let Some(func) = node
2719                .child_by_field_name("function")
2720                .or_else(|| node.child_by_field_name("name"))
2721                .or_else(|| node.child_by_field_name("method"))
2722            {
2723                let func_text = node_text(func, source);
2724                // Extract just the function name from attribute/member access
2725                let func_name = func_text
2726                    .split('.')
2727                    .next_back()
2728                    .unwrap_or(func_text)
2729                    .rsplit("::")
2730                    .next()
2731                    .unwrap_or(func_text);
2732                return Some(func_name.to_string());
2733            }
2734            // For C/C++ call_expression, first child is the function
2735            if let Some(first_child) = node.child(0) {
2736                let text = node_text(first_child, source);
2737                let name = text
2738                    .split('.')
2739                    .next_back()
2740                    .unwrap_or(text)
2741                    .rsplit("::")
2742                    .next()
2743                    .unwrap_or(text);
2744                return Some(name.to_string());
2745            }
2746        }
2747        // Go: selector_expression.arguments
2748        "composite_literal" => {
2749            // Go: Type{} literal
2750        }
2751        _ => {}
2752    }
2753
2754    // Fallback: check the whole node text for common patterns
2755    let text = node_text(node, source);
2756    if text.contains('(') {
2757        let name_part = text.split('(').next()?;
2758        let func_name = name_part
2759            .split('.')
2760            .next_back()
2761            .unwrap_or(name_part)
2762            .rsplit("::")
2763            .next()
2764            .unwrap_or(name_part)
2765            .trim();
2766        if !func_name.is_empty() {
2767            return Some(func_name.to_string());
2768        }
2769    }
2770
2771    None
2772}
2773
2774/// Extract the variable name from a C/C++ declarator (handles pointer_declarator, etc.)
2775fn extract_c_declarator_name(declarator: Node, source: &[u8]) -> Option<String> {
2776    match declarator.kind() {
2777        "identifier" => Some(node_text(declarator, source).to_string()),
2778        "pointer_declarator" => {
2779            // *foo -> get the identifier inside
2780            let mut cursor = declarator.walk();
2781            for child in declarator.children(&mut cursor) {
2782                if child.kind() == "identifier" {
2783                    return Some(node_text(child, source).to_string());
2784                }
2785                if child.kind() == "pointer_declarator" {
2786                    return extract_c_declarator_name(child, source);
2787                }
2788            }
2789            None
2790        }
2791        _ => Some(node_text(declarator, source).to_string()),
2792    }
2793}
2794
2795/// Extract (object_name, method_name) from a close call like `f.close()` or `fclose(fp)`.
2796fn extract_close_call(node: Node, source: &[u8], lang: Language) -> Option<(String, String)> {
2797    match lang {
2798        Language::Python
2799        | Language::Ruby
2800        | Language::Java
2801        | Language::CSharp
2802        | Language::TypeScript
2803        | Language::JavaScript
2804        | Language::Scala
2805        | Language::Kotlin
2806        | Language::Swift => {
2807            // obj.method() pattern
2808            if let Some(func) = node
2809                .child_by_field_name("function")
2810                .or_else(|| node.child_by_field_name("method"))
2811                .or_else(|| node.child_by_field_name("name"))
2812            {
2813                // Check for attribute/member access: obj.close()
2814                if func.kind() == "attribute"
2815                    || func.kind() == "member_expression"
2816                    || func.kind() == "selector_expression"
2817                    || func.kind() == "field_access"
2818                {
2819                    let obj = func.child_by_field_name("object").or_else(|| func.child(0));
2820                    let attr = func
2821                        .child_by_field_name("attribute")
2822                        .or_else(|| func.child_by_field_name("field"))
2823                        .or_else(|| func.child_by_field_name("name"));
2824
2825                    if let (Some(obj), Some(attr)) = (obj, attr) {
2826                        let var_name = node_text(obj, source).to_string();
2827                        let method = node_text(attr, source).to_string();
2828                        return Some((var_name, method));
2829                    }
2830                }
2831            }
2832            None
2833        }
2834        Language::Go => {
2835            // Go: obj.Close() - selector_expression
2836            if let Some(func) = node.child_by_field_name("function") {
2837                if func.kind() == "selector_expression" {
2838                    if let Some(operand) = func.child_by_field_name("operand") {
2839                        if let Some(field) = func.child_by_field_name("field") {
2840                            let var_name = node_text(operand, source).to_string();
2841                            let method = node_text(field, source).to_string();
2842                            return Some((var_name, method));
2843                        }
2844                    }
2845                }
2846            }
2847            None
2848        }
2849        Language::C | Language::Cpp => {
2850            // C: fclose(fp) - the variable is the first argument
2851            if let Some(func) = node
2852                .child_by_field_name("function")
2853                .or_else(|| node.child(0))
2854            {
2855                let func_name = node_text(func, source).to_string();
2856                // Get first argument
2857                if let Some(args) = node.child_by_field_name("arguments") {
2858                    if let Some(first_arg) = args.child(1) {
2859                        // child(0) is usually '('
2860                        let var_name = node_text(first_arg, source).to_string();
2861                        return Some((var_name, func_name));
2862                    }
2863                }
2864            }
2865            None
2866        }
2867        _ => {
2868            // Generic: try obj.method() pattern
2869            if let Some(func) = node.child_by_field_name("function") {
2870                if let Some(obj) = func.child_by_field_name("object").or_else(|| func.child(0)) {
2871                    if let Some(attr) = func.child_by_field_name("attribute") {
2872                        let var_name = node_text(obj, source).to_string();
2873                        let method = node_text(attr, source).to_string();
2874                        return Some((var_name, method));
2875                    }
2876                }
2877            }
2878            None
2879        }
2880    }
2881}
2882
2883// =============================================================================
2884// Multi-language CFG Builder
2885// =============================================================================
2886
2887/// Build a simplified CFG from a function AST, using language-specific patterns.
2888pub fn build_cfg_multilang(func_node: Node, source: &[u8], lang: Language) -> SimpleCfg {
2889    let patterns = get_resource_patterns(lang);
2890    let mut cfg = SimpleCfg::new();
2891    let entry_id = cfg.new_block();
2892    cfg.entry_block = entry_id;
2893
2894    if let Some(block) = cfg.blocks.get_mut(&entry_id) {
2895        block.is_entry = true;
2896    }
2897
2898    // Find the function body - try all known body kinds
2899    let body = func_node
2900        .children(&mut func_node.walk())
2901        .find(|n| patterns.body_kinds.contains(&n.kind()));
2902
2903    if let Some(body_node) = body {
2904        let exit_id =
2905            process_statements_multilang(&mut cfg, body_node, source, entry_id, &patterns);
2906        if let Some(exit) = exit_id {
2907            if !cfg.blocks.get(&exit).is_none_or(|b| b.is_exit) {
2908                cfg.mark_exit(exit);
2909            }
2910        }
2911    } else {
2912        // Empty function or body not found - try processing children directly
2913        cfg.mark_exit(entry_id);
2914    }
2915
2916    cfg
2917}
2918
2919fn process_statements_multilang(
2920    cfg: &mut SimpleCfg,
2921    node: Node,
2922    source: &[u8],
2923    mut current: usize,
2924    patterns: &LangResourcePatterns,
2925) -> Option<usize> {
2926    let mut cursor = node.walk();
2927    for child in node.children(&mut cursor) {
2928        let kind = child.kind();
2929
2930        if patterns.return_kinds.contains(&kind) {
2931            // Return/raise/throw statement
2932            let text = node_text(child, source).to_string();
2933            let line = child.start_position().row as u32 + 1;
2934            if let Some(block) = cfg.blocks.get_mut(&current) {
2935                block
2936                    .stmts
2937                    .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2938                block.lines.push(line);
2939            }
2940            cfg.mark_exit(current);
2941            return None;
2942        } else if patterns.if_kinds.contains(&kind) {
2943            // If statement - creates branches
2944            current = process_if_multilang(cfg, child, source, current, patterns)?;
2945        } else if patterns.loop_kinds.contains(&kind) {
2946            // Loop statement
2947            current = process_loop_multilang(cfg, child, source, current, patterns)?;
2948        } else if patterns.try_kinds.contains(&kind) {
2949            // Try/catch statement
2950            current = process_try_multilang(cfg, child, source, current, patterns)?;
2951        } else if patterns.cleanup_block_kinds.contains(&kind) {
2952            // Context manager / defer / using
2953            current = process_cleanup_block_multilang(cfg, child, source, current, patterns)?;
2954        } else {
2955            // Regular statement
2956            let text = node_text(child, source).to_string();
2957            let line = child.start_position().row as u32 + 1;
2958            if let Some(block) = cfg.blocks.get_mut(&current) {
2959                block
2960                    .stmts
2961                    .push((child.start_byte(), child.end_byte(), kind.to_string(), text));
2962                block.lines.push(line);
2963            }
2964        }
2965    }
2966    Some(current)
2967}
2968
2969fn process_if_multilang(
2970    cfg: &mut SimpleCfg,
2971    node: Node,
2972    source: &[u8],
2973    current: usize,
2974    patterns: &LangResourcePatterns,
2975) -> Option<usize> {
2976    // Add condition to current block
2977    if let Some(cond) = node.child_by_field_name("condition") {
2978        let text = node_text(cond, source).to_string();
2979        let line = cond.start_position().row as u32 + 1;
2980        if let Some(block) = cfg.blocks.get_mut(&current) {
2981            block.stmts.push((
2982                cond.start_byte(),
2983                cond.end_byte(),
2984                "condition".to_string(),
2985                text,
2986            ));
2987            block.lines.push(line);
2988        }
2989    }
2990
2991    let true_block = cfg.new_block();
2992    cfg.add_edge(current, true_block);
2993
2994    // Find the body block
2995    let mut cursor = node.walk();
2996    let consequence = node
2997        .children(&mut cursor)
2998        .find(|n| patterns.body_kinds.contains(&n.kind()));
2999    let true_exit = if let Some(body) = consequence {
3000        process_statements_multilang(cfg, body, source, true_block, patterns)
3001    } else {
3002        Some(true_block)
3003    };
3004
3005    // Find alternative (else/elif)
3006    let mut cursor = node.walk();
3007    let alternative = node
3008        .children(&mut cursor)
3009        .find(|n| n.kind() == "else_clause" || n.kind() == "elif_clause" || n.kind() == "else");
3010
3011    let false_exit = if let Some(alt) = alternative {
3012        let false_block = cfg.new_block();
3013        cfg.add_edge(current, false_block);
3014        let alt_body = alt
3015            .children(&mut alt.walk())
3016            .find(|n| patterns.body_kinds.contains(&n.kind()));
3017        if let Some(alt_body) = alt_body {
3018            process_statements_multilang(cfg, alt_body, source, false_block, patterns)
3019        } else {
3020            Some(false_block)
3021        }
3022    } else {
3023        None
3024    };
3025
3026    let merge = cfg.new_block();
3027    if let Some(te) = true_exit {
3028        cfg.add_edge(te, merge);
3029    }
3030    if let Some(fe) = false_exit {
3031        cfg.add_edge(fe, merge);
3032    }
3033    if alternative.is_none() {
3034        cfg.add_edge(current, merge);
3035    }
3036
3037    Some(merge)
3038}
3039
3040fn process_loop_multilang(
3041    cfg: &mut SimpleCfg,
3042    node: Node,
3043    source: &[u8],
3044    current: usize,
3045    patterns: &LangResourcePatterns,
3046) -> Option<usize> {
3047    let header = cfg.new_block();
3048    cfg.add_edge(current, header);
3049
3050    if let Some(cond) = node.child_by_field_name("condition") {
3051        let text = node_text(cond, source).to_string();
3052        let line = cond.start_position().row as u32 + 1;
3053        if let Some(block) = cfg.blocks.get_mut(&header) {
3054            block.stmts.push((
3055                cond.start_byte(),
3056                cond.end_byte(),
3057                "loop_condition".to_string(),
3058                text,
3059            ));
3060            block.lines.push(line);
3061        }
3062    }
3063
3064    let body_block = cfg.new_block();
3065    cfg.add_edge(header, body_block);
3066
3067    let body = node
3068        .children(&mut node.walk())
3069        .find(|n| patterns.body_kinds.contains(&n.kind()));
3070    let body_exit = if let Some(body_node) = body {
3071        process_statements_multilang(cfg, body_node, source, body_block, patterns)
3072    } else {
3073        Some(body_block)
3074    };
3075
3076    if let Some(be) = body_exit {
3077        cfg.add_edge(be, header);
3078    }
3079
3080    let exit = cfg.new_block();
3081    cfg.add_edge(header, exit);
3082    Some(exit)
3083}
3084
3085fn process_try_multilang(
3086    cfg: &mut SimpleCfg,
3087    node: Node,
3088    source: &[u8],
3089    current: usize,
3090    patterns: &LangResourcePatterns,
3091) -> Option<usize> {
3092    let try_block = cfg.new_block();
3093    cfg.add_edge(current, try_block);
3094
3095    let try_body = node
3096        .children(&mut node.walk())
3097        .find(|n| patterns.body_kinds.contains(&n.kind()));
3098    let try_exit = if let Some(body) = try_body {
3099        process_statements_multilang(cfg, body, source, try_block, patterns)
3100    } else {
3101        Some(try_block)
3102    };
3103
3104    let mut cursor = node.walk();
3105    let mut handler_exits = Vec::new();
3106    for child in node.children(&mut cursor) {
3107        let ck = child.kind();
3108        if ck == "except_clause" || ck == "catch_clause" || ck == "rescue" {
3109            let handler_block = cfg.new_block();
3110            cfg.add_edge(try_block, handler_block);
3111            if let Some(block) = cfg.blocks.get_mut(&try_block) {
3112                block.exception_handlers.push(handler_block);
3113            }
3114            let handler_body = child
3115                .children(&mut child.walk())
3116                .find(|n| patterns.body_kinds.contains(&n.kind()));
3117            if let Some(hb) = handler_body {
3118                if let Some(exit) =
3119                    process_statements_multilang(cfg, hb, source, handler_block, patterns)
3120                {
3121                    handler_exits.push(exit);
3122                }
3123            } else {
3124                handler_exits.push(handler_block);
3125            }
3126        }
3127    }
3128
3129    let finally_clause = node
3130        .children(&mut node.walk())
3131        .find(|n| n.kind() == "finally_clause" || n.kind() == "finally");
3132
3133    let merge = cfg.new_block();
3134    if let Some(te) = try_exit {
3135        if let Some(finally) = finally_clause {
3136            let finally_block = cfg.new_block();
3137            cfg.add_edge(te, finally_block);
3138            let finally_body = finally
3139                .children(&mut finally.walk())
3140                .find(|n| patterns.body_kinds.contains(&n.kind()));
3141            if let Some(fb) = finally_body {
3142                if let Some(exit) =
3143                    process_statements_multilang(cfg, fb, source, finally_block, patterns)
3144                {
3145                    cfg.add_edge(exit, merge);
3146                }
3147            } else {
3148                cfg.add_edge(finally_block, merge);
3149            }
3150        } else {
3151            cfg.add_edge(te, merge);
3152        }
3153    }
3154    for he in handler_exits {
3155        cfg.add_edge(he, merge);
3156    }
3157
3158    Some(merge)
3159}
3160
3161fn process_cleanup_block_multilang(
3162    cfg: &mut SimpleCfg,
3163    node: Node,
3164    source: &[u8],
3165    current: usize,
3166    patterns: &LangResourcePatterns,
3167) -> Option<usize> {
3168    let text = node_text(node, source).to_string();
3169    let line = node.start_position().row as u32 + 1;
3170    if let Some(block) = cfg.blocks.get_mut(&current) {
3171        block.stmts.push((
3172            node.start_byte(),
3173            node.end_byte(),
3174            node.kind().to_string(),
3175            text,
3176        ));
3177        block.lines.push(line);
3178    }
3179
3180    let body = node
3181        .children(&mut node.walk())
3182        .find(|n| patterns.body_kinds.contains(&n.kind()));
3183    if let Some(body_node) = body {
3184        process_statements_multilang(cfg, body_node, source, current, patterns)
3185    } else {
3186        Some(current)
3187    }
3188}
3189
3190#[cfg(test)]
3191fn get_python_parser() -> PatternsResult<Parser> {
3192    get_parser_for_language(Language::Python)
3193}
3194
3195/// Create a tree-sitter parser for the given language.
3196fn get_parser_for_language(lang: Language) -> PatternsResult<Parser> {
3197    let mut parser = Parser::new();
3198    let ts_lang =
3199        ParserPool::get_ts_language(lang).ok_or_else(|| PatternsError::UnsupportedLanguage {
3200            language: lang.as_str().to_string(),
3201        })?;
3202    parser
3203        .set_language(&ts_lang)
3204        .map_err(|e| PatternsError::ParseError {
3205            file: PathBuf::from("<internal>"),
3206            message: format!("Failed to set {} language: {}", lang.as_str(), e),
3207        })?;
3208    Ok(parser)
3209}
3210
3211/// Get the function name from a node, handling language-specific declarator patterns.
3212/// For C/C++, the name is nested inside a `function_declarator` child of the `declarator` field.
3213/// For OCaml, value_definition wraps let_binding which has the pattern field.
3214fn get_function_name_from_node(
3215    node: Node,
3216    source: &[u8],
3217    patterns: &LangResourcePatterns,
3218) -> Option<String> {
3219    // OCaml: value_definition wraps let_binding(s)
3220    if node.kind() == "value_definition" {
3221        let mut cursor = node.walk();
3222        for child in node.children(&mut cursor) {
3223            if child.kind() == "let_binding" {
3224                if let Some(pattern) = child.child_by_field_name("pattern") {
3225                    return Some(node_text(pattern, source).to_string());
3226                }
3227            }
3228        }
3229        return None;
3230    }
3231
3232    // First try the standard name field
3233    if let Some(name_node) = node.child_by_field_name(patterns.name_field) {
3234        // For C/C++, the "declarator" field contains a function_declarator
3235        // which itself has a "declarator" field containing the actual identifier
3236        if name_node.kind() == "function_declarator" {
3237            if let Some(inner) = name_node.child_by_field_name("declarator") {
3238                return Some(node_text(inner, source).to_string());
3239            }
3240        }
3241        // For pointer_declarator -> function_declarator pattern
3242        if name_node.kind() == "pointer_declarator" {
3243            let mut cursor = name_node.walk();
3244            for child in name_node.children(&mut cursor) {
3245                if child.kind() == "function_declarator" {
3246                    if let Some(inner) = child.child_by_field_name("declarator") {
3247                        return Some(node_text(inner, source).to_string());
3248                    }
3249                }
3250            }
3251        }
3252        return Some(node_text(name_node, source).to_string());
3253    }
3254    None
3255}
3256
3257#[cfg(test)]
3258fn find_function_node<'a>(
3259    tree: &'a tree_sitter::Tree,
3260    function_name: &str,
3261    source: &[u8],
3262) -> Option<Node<'a>> {
3263    let root = tree.root_node();
3264    // Use Python patterns as default for backward compatibility
3265    let patterns = get_resource_patterns(Language::Python);
3266    find_function_recursive(root, function_name, source, &patterns)
3267}
3268
3269fn find_function_node_multilang<'a>(
3270    tree: &'a tree_sitter::Tree,
3271    function_name: &str,
3272    source: &[u8],
3273    lang: Language,
3274) -> Option<Node<'a>> {
3275    let root = tree.root_node();
3276    let patterns = get_resource_patterns(lang);
3277    find_function_recursive(root, function_name, source, &patterns)
3278}
3279
3280fn find_function_recursive<'a>(
3281    node: Node<'a>,
3282    function_name: &str,
3283    source: &[u8],
3284    patterns: &LangResourcePatterns,
3285) -> Option<Node<'a>> {
3286    let kind = node.kind();
3287    if patterns.function_kinds.contains(&kind) {
3288        if let Some(name) = get_function_name_from_node(node, source, patterns) {
3289            if name == function_name {
3290                return Some(node);
3291            }
3292        }
3293    }
3294
3295    // Check for arrow functions in variable declarations (TS/JS pattern):
3296    // lexical_declaration / variable_declaration -> variable_declarator -> name + value(arrow_function)
3297    if matches!(kind, "lexical_declaration" | "variable_declaration") {
3298        let mut decl_cursor = node.walk();
3299        for child in node.children(&mut decl_cursor) {
3300            if child.kind() == "variable_declarator" {
3301                if let Some(name_node) = child.child_by_field_name("name") {
3302                    let var_name = name_node.utf8_text(source).unwrap_or("");
3303                    if var_name == function_name {
3304                        if let Some(value_node) = child.child_by_field_name("value") {
3305                            if matches!(
3306                                value_node.kind(),
3307                                "arrow_function"
3308                                    | "function"
3309                                    | "function_expression"
3310                                    | "generator_function"
3311                            ) {
3312                                return Some(value_node);
3313                            }
3314                        }
3315                    }
3316                }
3317            }
3318        }
3319    }
3320
3321    let mut cursor = node.walk();
3322    for child in node.children(&mut cursor) {
3323        if let Some(found) = find_function_recursive(child, function_name, source, patterns) {
3324            return Some(found);
3325        }
3326    }
3327
3328    None
3329}
3330
3331fn find_all_functions_multilang<'a>(
3332    tree: &'a tree_sitter::Tree,
3333    source: &[u8],
3334    lang: Language,
3335) -> Vec<(String, Node<'a>)> {
3336    let mut functions = Vec::new();
3337    let patterns = get_resource_patterns(lang);
3338    collect_functions(tree.root_node(), source, &mut functions, &patterns);
3339    functions
3340}
3341
3342fn collect_functions<'a>(
3343    node: Node<'a>,
3344    source: &[u8],
3345    functions: &mut Vec<(String, Node<'a>)>,
3346    patterns: &LangResourcePatterns,
3347) {
3348    let kind = node.kind();
3349    if patterns.function_kinds.contains(&kind) {
3350        if let Some(name) = get_function_name_from_node(node, source, patterns) {
3351            functions.push((name, node));
3352        }
3353    }
3354
3355    // Check for arrow functions in variable declarations (TS/JS pattern):
3356    // lexical_declaration / variable_declaration -> variable_declarator -> name + value(arrow_function)
3357    if matches!(kind, "lexical_declaration" | "variable_declaration") {
3358        let mut decl_cursor = node.walk();
3359        for child in node.children(&mut decl_cursor) {
3360            if child.kind() == "variable_declarator" {
3361                if let Some(name_node) = child.child_by_field_name("name") {
3362                    if let Some(value_node) = child.child_by_field_name("value") {
3363                        if matches!(
3364                            value_node.kind(),
3365                            "arrow_function"
3366                                | "function"
3367                                | "function_expression"
3368                                | "generator_function"
3369                        ) {
3370                            let var_name = name_node.utf8_text(source).unwrap_or("").to_string();
3371                            functions.push((var_name, value_node));
3372                        }
3373                    }
3374                }
3375            }
3376        }
3377    }
3378
3379    let mut cursor = node.walk();
3380    for child in node.children(&mut cursor) {
3381        collect_functions(child, source, functions, patterns);
3382    }
3383}
3384
3385// =============================================================================
3386// Main Analysis Function
3387// =============================================================================
3388
3389fn analyze_function_with_lang(
3390    func_node: Node,
3391    source: &[u8],
3392    args: &ResourcesArgs,
3393    lang: Language,
3394) -> (
3395    Vec<ResourceInfo>,
3396    Vec<LeakInfo>,
3397    Vec<DoubleCloseInfo>,
3398    Vec<UseAfterCloseInfo>,
3399) {
3400    let check_leaks = args.check_leaks || args.check_all;
3401    let check_double_close = args.check_double_close || args.check_all;
3402    let check_use_after_close = args.check_use_after_close || args.check_all;
3403    // Detect resources
3404    let mut detector = ResourceDetector::with_language(lang);
3405    let resources = detector.detect_with_patterns(func_node, source);
3406
3407    // Detect leaks
3408    let leaks = if check_leaks {
3409        let cfg = build_cfg_multilang(func_node, source, lang);
3410        let mut leak_detector = LeakDetector::new();
3411        leak_detector.detect_multilang(&cfg, &resources, source, args.show_paths)
3412    } else {
3413        Vec::new()
3414    };
3415
3416    // Detect double-close
3417    let double_closes = if check_double_close {
3418        let detector = DoubleCloseDetector::with_language(lang);
3419        detector.detect_multilang(func_node, source)
3420    } else {
3421        Vec::new()
3422    };
3423
3424    // Detect use-after-close
3425    let use_after_closes = if check_use_after_close {
3426        let detector = UseAfterCloseDetector::with_language(lang);
3427        detector.detect_multilang(func_node, source)
3428    } else {
3429        Vec::new()
3430    };
3431
3432    (resources, leaks, double_closes, use_after_closes)
3433}
3434
3435// =============================================================================
3436// Entry Point
3437// =============================================================================
3438
3439/// Run the resources analysis command.
3440pub fn run(args: ResourcesArgs, global_format: GlobalOutputFormat) -> anyhow::Result<()> {
3441    let start_time = Instant::now();
3442
3443    // Validate path
3444    let path = if let Some(ref root) = args.project_root {
3445        validate_file_path_in_project(&args.file, root)?
3446    } else {
3447        validate_file_path(&args.file)?
3448    };
3449
3450    // Read file
3451    let source = read_file_safe(&path)?;
3452    let source_bytes = source.as_bytes();
3453
3454    // Detect language (multi-language support)
3455    let lang: Language = match args.lang {
3456        Some(l) => l,
3457        None => Language::from_path(&path).ok_or_else(|| {
3458            let ext = path
3459                .extension()
3460                .and_then(|e| e.to_str())
3461                .unwrap_or("unknown")
3462                .to_string();
3463            PatternsError::UnsupportedLanguage { language: ext }
3464        })?,
3465    };
3466
3467    // Parse file with language-appropriate parser
3468    let mut parser = get_parser_for_language(lang)?;
3469    let tree = parser
3470        .parse(&source, None)
3471        .ok_or_else(|| PatternsError::ParseError {
3472            file: path.clone(),
3473            message: format!("Failed to parse {} file", lang.as_str()),
3474        })?;
3475
3476    // Collect results
3477    let mut all_resources = Vec::new();
3478    let mut all_leaks = Vec::new();
3479    let mut all_double_closes = Vec::new();
3480    let mut all_use_after_closes = Vec::new();
3481
3482    if let Some(ref func_name) = args.function {
3483        // Analyze specific function
3484        if let Some(func_node) = find_function_node_multilang(&tree, func_name, source_bytes, lang)
3485        {
3486            let (resources, leaks, double_closes, use_after_closes) =
3487                analyze_function_with_lang(func_node, source_bytes, &args, lang);
3488            all_resources = resources;
3489            all_leaks = leaks;
3490            all_double_closes = double_closes;
3491            all_use_after_closes = use_after_closes;
3492        } else {
3493            return Err(PatternsError::FunctionNotFound {
3494                function: func_name.clone(),
3495                file: path.clone(),
3496            }
3497            .into());
3498        }
3499    } else {
3500        // Analyze all functions
3501        let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3502        for (_name, func_node) in functions {
3503            let (resources, leaks, double_closes, use_after_closes) =
3504                analyze_function_with_lang(func_node, source_bytes, &args, lang);
3505            all_resources.extend(resources);
3506            all_leaks.extend(leaks);
3507            all_double_closes.extend(double_closes);
3508            all_use_after_closes.extend(use_after_closes);
3509        }
3510    }
3511
3512    // Generate suggestions
3513    let suggestions = if args.suggest_context {
3514        suggest_context_manager_multilang(&all_resources, lang)
3515    } else {
3516        Vec::new()
3517    };
3518
3519    // Generate constraints
3520    let constraints = if args.constraints {
3521        generate_constraints(
3522            path.to_str().unwrap_or(""),
3523            args.function.as_deref(),
3524            &all_resources,
3525            &all_leaks,
3526            &all_double_closes,
3527            &all_use_after_closes,
3528        )
3529    } else {
3530        Vec::new()
3531    };
3532
3533    // Build summary
3534    let summary = ResourceSummary {
3535        resources_detected: all_resources.len() as u32,
3536        leaks_found: all_leaks.len() as u32,
3537        double_closes_found: all_double_closes.len() as u32,
3538        use_after_closes_found: all_use_after_closes.len() as u32,
3539    };
3540
3541    let elapsed_ms = start_time.elapsed().as_millis() as u64;
3542
3543    // Build report
3544    let report = ResourceReport {
3545        file: path.to_string_lossy().to_string(),
3546        language: lang.as_str().to_string(),
3547        function: args.function.clone(),
3548        resources: all_resources,
3549        leaks: all_leaks,
3550        double_closes: all_double_closes,
3551        use_after_closes: all_use_after_closes,
3552        suggestions,
3553        constraints,
3554        summary,
3555        analysis_time_ms: elapsed_ms,
3556    };
3557
3558    // Output: global -f flag takes priority over hidden --output-format
3559    let use_text = matches!(global_format, GlobalOutputFormat::Text)
3560        || matches!(args.output_format, OutputFormat::Text);
3561    let output = if use_text {
3562        format_resources_text(&report)
3563    } else {
3564        serde_json::to_string_pretty(&report)?
3565    };
3566
3567    println!("{}", output);
3568
3569    // Exit code 3 if issues found
3570    let has_issues = report.summary.leaks_found > 0
3571        || report.summary.double_closes_found > 0
3572        || report.summary.use_after_closes_found > 0;
3573
3574    if has_issues {
3575        std::process::exit(3);
3576    }
3577
3578    Ok(())
3579}
3580
3581// =============================================================================
3582// L2 Integration API
3583// =============================================================================
3584
3585/// Aggregated resource analysis results for L2 consumption.
3586///
3587/// Each finding is paired with the function name where it was detected.
3588/// This avoids requiring callers to handle tree-sitter nodes directly.
3589pub struct ResourceAnalysisResults {
3590    /// Detected leaks: `(function_name, LeakInfo)`.
3591    pub leaks: Vec<(String, LeakInfo)>,
3592    /// Detected double-close issues: `(function_name, DoubleCloseInfo)`.
3593    pub double_closes: Vec<(String, DoubleCloseInfo)>,
3594    /// Detected use-after-close issues: `(function_name, UseAfterCloseInfo)`.
3595    pub use_after_closes: Vec<(String, UseAfterCloseInfo)>,
3596}
3597
3598/// Analyze source code for resource lifecycle issues.
3599///
3600/// Parses the source with tree-sitter for the given language, finds all function
3601/// nodes, and runs the full resource analysis (leak, double-close, use-after-close)
3602/// on each function.
3603///
3604/// This is the primary entry point for L2 finding extractors that need resource
3605/// analysis without constructing `ResourcesArgs` or tree-sitter nodes themselves.
3606///
3607/// # Arguments
3608/// * `source` - Source code to analyze
3609/// * `lang` - Programming language for parsing
3610///
3611/// # Returns
3612/// `ResourceAnalysisResults` with all detected issues, or an error if parsing fails.
3613pub fn analyze_source_for_resource_issues(
3614    source: &str,
3615    lang: Language,
3616) -> PatternsResult<ResourceAnalysisResults> {
3617    let source_bytes = source.as_bytes();
3618
3619    // Parse source with tree-sitter
3620    let mut parser = get_parser_for_language(lang)?;
3621    let tree = parser
3622        .parse(source, None)
3623        .ok_or_else(|| PatternsError::ParseError {
3624            file: PathBuf::from("<in-memory>"),
3625            message: format!(
3626                "Failed to parse {} source for resource analysis",
3627                lang.as_str()
3628            ),
3629        })?;
3630
3631    // Build args with all checks enabled
3632    let args = ResourcesArgs {
3633        file: PathBuf::from("<in-memory>"),
3634        function: None,
3635        lang: Some(lang),
3636        check_leaks: true,
3637        check_double_close: true,
3638        check_use_after_close: true,
3639        check_all: true,
3640        suggest_context: false,
3641        show_paths: false,
3642        constraints: false,
3643        summary: false,
3644        output_format: OutputFormat::Json,
3645        project_root: None,
3646    };
3647
3648    let mut all_leaks = Vec::new();
3649    let mut all_double_closes = Vec::new();
3650    let mut all_use_after_closes = Vec::new();
3651
3652    // Find all functions and analyze each
3653    let functions = find_all_functions_multilang(&tree, source_bytes, lang);
3654    for (func_name, func_node) in functions {
3655        let (_resources, leaks, double_closes, use_after_closes) =
3656            analyze_function_with_lang(func_node, source_bytes, &args, lang);
3657
3658        for leak in leaks {
3659            all_leaks.push((func_name.clone(), leak));
3660        }
3661        for dc in double_closes {
3662            all_double_closes.push((func_name.clone(), dc));
3663        }
3664        for uac in use_after_closes {
3665            all_use_after_closes.push((func_name.clone(), uac));
3666        }
3667    }
3668
3669    Ok(ResourceAnalysisResults {
3670        leaks: all_leaks,
3671        double_closes: all_double_closes,
3672        use_after_closes: all_use_after_closes,
3673    })
3674}
3675
3676// =============================================================================
3677// Unit Tests
3678// =============================================================================
3679
3680#[cfg(test)]
3681mod tests {
3682    use super::*;
3683
3684    const TEST_LEAKY_FUNCTION: &str = r#"
3685def leaky_function(path):
3686    f = open(path)
3687    if some_condition():
3688        return None
3689    content = f.read()
3690    f.close()
3691    return content
3692"#;
3693
3694    const TEST_SAFE_WITH_CONTEXT: &str = r#"
3695def safe_with_context(path):
3696    with open(path) as f:
3697        return f.read()
3698"#;
3699
3700    const TEST_DOUBLE_CLOSE: &str = r#"
3701def double_close(path):
3702    f = open(path)
3703    content = f.read()
3704    f.close()
3705    f.close()
3706    return content
3707"#;
3708
3709    const TEST_USE_AFTER_CLOSE: &str = r#"
3710def use_after_close(path):
3711    f = open(path)
3712    f.close()
3713    content = f.read()
3714    return content
3715"#;
3716
3717    #[test]
3718    fn test_resource_creators_constant() {
3719        assert!(RESOURCE_CREATORS.contains(&"open"));
3720        assert!(RESOURCE_CREATORS.contains(&"socket"));
3721        assert!(RESOURCE_CREATORS.contains(&"connect"));
3722        assert!(RESOURCE_CREATORS.contains(&"cursor"));
3723    }
3724
3725    #[test]
3726    fn test_resource_closers_constant() {
3727        assert!(RESOURCE_CLOSERS.contains(&"close"));
3728        assert!(RESOURCE_CLOSERS.contains(&"shutdown"));
3729        assert!(RESOURCE_CLOSERS.contains(&"disconnect"));
3730    }
3731
3732    #[test]
3733    fn test_max_paths_constant() {
3734        assert_eq!(MAX_PATHS, 1000);
3735    }
3736
3737    #[test]
3738    fn test_resource_detector_finds_open() {
3739        let mut parser = get_python_parser().unwrap();
3740        let tree = parser.parse(TEST_LEAKY_FUNCTION, None).unwrap();
3741        let source = TEST_LEAKY_FUNCTION.as_bytes();
3742
3743        let func_node = find_function_node(&tree, "leaky_function", source).unwrap();
3744        let mut detector = ResourceDetector::new();
3745        let resources = detector.detect(func_node, source);
3746
3747        assert_eq!(resources.len(), 1);
3748        assert_eq!(resources[0].name, "f");
3749        assert_eq!(resources[0].resource_type, "file");
3750        assert!(!resources[0].closed);
3751    }
3752
3753    #[test]
3754    fn test_resource_detector_context_manager() {
3755        let mut parser = get_python_parser().unwrap();
3756        let tree = parser.parse(TEST_SAFE_WITH_CONTEXT, None).unwrap();
3757        let source = TEST_SAFE_WITH_CONTEXT.as_bytes();
3758
3759        let func_node = find_function_node(&tree, "safe_with_context", source).unwrap();
3760        let mut detector = ResourceDetector::new();
3761        let resources = detector.detect(func_node, source);
3762
3763        assert_eq!(resources.len(), 1);
3764        assert!(
3765            resources[0].closed,
3766            "Context manager resource should be marked as closed"
3767        );
3768    }
3769
3770    #[test]
3771    fn test_double_close_detector() {
3772        let mut parser = get_python_parser().unwrap();
3773        let tree = parser.parse(TEST_DOUBLE_CLOSE, None).unwrap();
3774        let source = TEST_DOUBLE_CLOSE.as_bytes();
3775
3776        let func_node = find_function_node(&tree, "double_close", source).unwrap();
3777        let detector = DoubleCloseDetector::new();
3778        let issues = detector.detect(func_node, source);
3779
3780        assert_eq!(issues.len(), 1);
3781        assert_eq!(issues[0].resource, "f");
3782    }
3783
3784    #[test]
3785    fn test_use_after_close_detector() {
3786        let mut parser = get_python_parser().unwrap();
3787        let tree = parser.parse(TEST_USE_AFTER_CLOSE, None).unwrap();
3788        let source = TEST_USE_AFTER_CLOSE.as_bytes();
3789
3790        let func_node = find_function_node(&tree, "use_after_close", source).unwrap();
3791        let detector = UseAfterCloseDetector::new();
3792        let issues = detector.detect(func_node, source);
3793
3794        assert!(!issues.is_empty());
3795        assert_eq!(issues[0].resource, "f");
3796    }
3797
3798    #[test]
3799    fn test_suggest_context_manager() {
3800        let resources = vec![ResourceInfo {
3801            name: "f".to_string(),
3802            resource_type: "file".to_string(),
3803            line: 2,
3804            closed: false,
3805        }];
3806
3807        let suggestions = suggest_context_manager(&resources);
3808        assert_eq!(suggestions.len(), 1);
3809        assert!(suggestions[0].suggestion.contains("with open"));
3810    }
3811
3812    #[test]
3813    fn test_generate_constraints_for_leak() {
3814        let resources = vec![ResourceInfo {
3815            name: "f".to_string(),
3816            resource_type: "file".to_string(),
3817            line: 2,
3818            closed: false,
3819        }];
3820        let leaks = vec![LeakInfo {
3821            resource: "f".to_string(),
3822            line: 2,
3823            paths: None,
3824        }];
3825
3826        let constraints =
3827            generate_constraints("test.py", Some("test_func"), &resources, &leaks, &[], &[]);
3828
3829        assert!(!constraints.is_empty());
3830        assert!(constraints[0].rule.contains("must be closed"));
3831    }
3832
3833    #[test]
3834    fn test_leak_detector_path_limit() {
3835        let detector = LeakDetector::new();
3836        assert_eq!(detector.max_paths, MAX_PATHS);
3837    }
3838
3839    #[test]
3840    fn test_cfg_builder_basic() {
3841        let mut parser = get_python_parser().unwrap();
3842        let source = r#"
3843def simple():
3844    x = 1
3845    return x
3846"#;
3847        let tree = parser.parse(source, None).unwrap();
3848        let func_node = find_function_node(&tree, "simple", source.as_bytes()).unwrap();
3849        let cfg = build_cfg(func_node, source.as_bytes());
3850
3851        assert!(!cfg.blocks.is_empty());
3852        assert!(!cfg.exit_blocks.is_empty());
3853    }
3854
3855    #[test]
3856    fn test_cfg_builder_with_if() {
3857        let mut parser = get_python_parser().unwrap();
3858        let source = r#"
3859def with_if(x):
3860    if x > 0:
3861        return x
3862    return -x
3863"#;
3864        let tree = parser.parse(source, None).unwrap();
3865        let func_node = find_function_node(&tree, "with_if", source.as_bytes()).unwrap();
3866        let cfg = build_cfg(func_node, source.as_bytes());
3867
3868        // Should have multiple blocks for the branching
3869        assert!(cfg.blocks.len() > 1);
3870    }
3871
3872    #[test]
3873    fn test_format_resources_text() {
3874        let report = ResourceReport {
3875            file: "test.py".to_string(),
3876            language: "python".to_string(),
3877            function: Some("test".to_string()),
3878            resources: vec![ResourceInfo {
3879                name: "f".to_string(),
3880                resource_type: "file".to_string(),
3881                line: 2,
3882                closed: false,
3883            }],
3884            leaks: vec![],
3885            double_closes: vec![],
3886            use_after_closes: vec![],
3887            suggestions: vec![],
3888            constraints: vec![],
3889            summary: ResourceSummary::default(),
3890            analysis_time_ms: 10,
3891        };
3892
3893        let text = format_resources_text(&report);
3894        assert!(text.contains("Resource Analysis: test.py"));
3895        assert!(text.contains("Function: test"));
3896        assert!(text.contains("file"));
3897    }
3898
3899    #[test]
3900    fn test_find_ts_arrow_function_resources() {
3901        let ts_source = r#"
3902const getDuration = (start: Date, end: Date): number => {
3903    const conn = createConnection();
3904    const result = end.getTime() - start.getTime();
3905    conn.close();
3906    return result;
3907};
3908
3909function regularFunc(x: number): number {
3910    return x * 2;
3911}
3912"#;
3913        let tree = tldr_core::ast::parser::parse(ts_source, Language::TypeScript).unwrap();
3914        let source_bytes = ts_source.as_bytes();
3915
3916        // Regular function should be found
3917        let regular =
3918            find_function_node_multilang(&tree, "regularFunc", source_bytes, Language::TypeScript);
3919        assert!(regular.is_some(), "Should find regular TS function");
3920
3921        // Arrow function assigned to const should also be found
3922        let arrow =
3923            find_function_node_multilang(&tree, "getDuration", source_bytes, Language::TypeScript);
3924        assert!(
3925            arrow.is_some(),
3926            "Should find TS arrow function 'getDuration'"
3927        );
3928    }
3929
3930    #[test]
3931    fn test_resources_args_lang_flag() {
3932        // Verify ResourcesArgs has a lang field of type Option<Language> (not language: String)
3933        let args = ResourcesArgs {
3934            file: PathBuf::from("src/db.go"),
3935            function: None,
3936            lang: Some(Language::Go),
3937            check_leaks: true,
3938            check_double_close: false,
3939            check_use_after_close: false,
3940            check_all: false,
3941            suggest_context: false,
3942            show_paths: false,
3943            constraints: false,
3944            summary: false,
3945            output_format: OutputFormat::Json,
3946            project_root: None,
3947        };
3948        assert_eq!(args.lang, Some(Language::Go));
3949
3950        // Also test None case (auto-detect)
3951        let args_auto = ResourcesArgs {
3952            file: PathBuf::from("src/db.py"),
3953            function: None,
3954            lang: None,
3955            check_leaks: true,
3956            check_double_close: false,
3957            check_use_after_close: false,
3958            check_all: false,
3959            suggest_context: false,
3960            show_paths: false,
3961            constraints: false,
3962            summary: false,
3963            output_format: OutputFormat::Json,
3964            project_root: None,
3965        };
3966        assert_eq!(args_auto.lang, None);
3967    }
3968}