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