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