flop_cli/
finder.rs

1use anyhow::{Context, Result};
2use regex::Regex;
3use std::fs;
4use std::path::Path;
5use walkdir::WalkDir;
6
7use crate::types::Match;
8
9pub fn find_debug_printfs(
10    path: &Path,
11    find_commented: bool,
12    detect_all: bool,
13) -> Result<Vec<Match>> {
14    let mut matches = Vec::new();
15
16    // Pattern to match C printf-like functions (multiline support with (?s))
17    // Match from function name to closing paren, then optional whitespace and semicolon
18    let c_functions_pattern = if detect_all {
19        // Match all output functions regardless of content
20        Regex::new(
21            r"(?s)(printf|fprintf|sprintf|snprintf|printf_debug|dprintf|puts|fputs|fputc|putchar|fputchar|write|perror)\s*\([^)]*\)\s*;",
22        )?
23    } else {
24        // Match only those with "debug" or "DEBUG"
25        Regex::new(
26            r"(?s)(printf|fprintf|sprintf|snprintf|printf_debug|dprintf|puts|fputs|fputc|putchar|fputchar|write|perror)\s*\([^)]*?(debug|DEBUG)[^)]*\)\s*;",
27        )?
28    };
29
30    // Pattern to match C++ streams (multiline support with (?s))
31    let cpp_stream_pattern = if detect_all {
32        // Match all stream output regardless of content
33        Regex::new(r"(?s)(std::cout|std::cerr|std::clog)\s*<<[^;]*?;")?
34    } else {
35        // Match only those with "debug" or "DEBUG"
36        Regex::new(r"(?s)(std::cout|std::cerr|std::clog)\s*<<[^;]*?(debug|DEBUG)[^;]*?;")?
37    };
38
39    // Pattern to match Rust macros (multiline support with (?s))
40    // Match macro_name!(...); where ... can contain anything except unbalanced parens
41    let rust_macro_pattern = if detect_all {
42        // Match all Rust output macros regardless of content
43        Regex::new(r"(?s)(println!|eprintln!|print!|eprint!|dbg!)\s*\([^)]*\)\s*;")?
44    } else {
45        // Match dbg! always, and other macros only if they contain "debug" or "DEBUG"
46        Regex::new(
47            r"(?s)dbg!\s*\([^)]*\)\s*;|(println!|eprintln!|print!|eprint!)\s*\([^)]*?(debug|DEBUG)[^)]*\)\s*;",
48        )?
49    };
50
51    // Pattern to match Java output statements (multiline support with (?s))
52    let java_pattern = if detect_all {
53        // Match all Java output statements regardless of content
54        Regex::new(
55            r"(?s)(System\.out\.println|System\.out\.printf|System\.out\.print|System\.err\.println|System\.err\.printf|System\.err\.print)\s*\([^)]*\)\s*;",
56        )?
57    } else {
58        // Match only those with "debug" or "DEBUG"
59        Regex::new(
60            r"(?s)(System\.out\.println|System\.out\.printf|System\.out\.print|System\.err\.println|System\.err\.printf|System\.err\.print)\s*\([^)]*?(debug|DEBUG)[^)]*\)\s*;",
61        )?
62    };
63
64    let comment_pattern = Regex::new(r"^\s*//")?;
65
66    let entries: Vec<_> = if path.is_file() {
67        vec![path.to_path_buf()]
68    } else {
69        WalkDir::new(path)
70            .into_iter()
71            .filter_map(|e| e.ok())
72            .filter(|e| {
73                e.path().is_file()
74                    && e.path()
75                        .extension()
76                        .and_then(|s| s.to_str())
77                        .map(|ext| {
78                            matches!(
79                                ext,
80                                "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "rs" | "java"
81                            )
82                        })
83                        .unwrap_or(false)
84            })
85            .map(|e| e.path().to_path_buf())
86            .collect()
87    };
88
89    for file_path in entries {
90        let content = fs::read_to_string(&file_path)
91            .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
92
93        // Find all C-style function calls
94        for cap in c_functions_pattern.find_iter(&content) {
95            let match_str = cap.as_str();
96            let start_offset = cap.start();
97            let end_offset = cap.end();
98
99            // Calculate line numbers from byte offsets
100            // Count newlines before the start position, then add 1
101            let line_number = content[..start_offset].matches('\n').count() + 1;
102            // For end line, count newlines up to the end position
103            let end_line_number = content[..end_offset].matches('\n').count() + 1;
104
105            // Get the line content (for display purposes, we'll get the first line of the match)
106            let line_start_offset = content[..start_offset]
107                .rfind('\n')
108                .map(|pos| pos + 1)
109                .unwrap_or(0);
110            let line_content = content[line_start_offset..]
111                .lines()
112                .next()
113                .unwrap_or("")
114                .to_string();
115
116            // Check if commented (check the beginning of the statement)
117            let is_commented = comment_pattern.is_match(&line_content);
118
119            if is_commented == find_commented {
120                // Extract original lines for multiline display
121                let multiline_content: Vec<String> =
122                    match_str.lines().map(|s| s.to_string()).collect();
123
124                matches.push(Match {
125                    file_path: file_path.clone(),
126                    line_number,
127                    end_line_number,
128                    line_content: match_str.replace('\n', " ").trim().to_string(),
129                    multiline_content,
130                });
131            }
132        }
133
134        // Find all C++ stream operations
135        for cap in cpp_stream_pattern.find_iter(&content) {
136            let match_str = cap.as_str();
137            let start_offset = cap.start();
138            let end_offset = cap.end();
139
140            // Calculate line numbers from byte offsets
141            // Count newlines before the start position, then add 1
142            let line_number = content[..start_offset].matches('\n').count() + 1;
143            // For end line, count newlines up to the end position
144            let end_line_number = content[..end_offset].matches('\n').count() + 1;
145
146            // Get the line content
147            let line_start_offset = content[..start_offset]
148                .rfind('\n')
149                .map(|pos| pos + 1)
150                .unwrap_or(0);
151            let line_content = content[line_start_offset..]
152                .lines()
153                .next()
154                .unwrap_or("")
155                .to_string();
156
157            // Check if commented
158            let is_commented = comment_pattern.is_match(&line_content);
159
160            if is_commented == find_commented {
161                // Extract original lines for multiline display
162                let multiline_content: Vec<String> =
163                    match_str.lines().map(|s| s.to_string()).collect();
164
165                matches.push(Match {
166                    file_path: file_path.clone(),
167                    line_number,
168                    end_line_number,
169                    line_content: match_str.replace('\n', " ").trim().to_string(),
170                    multiline_content,
171                });
172            }
173        }
174
175        // Find all Rust macro calls
176        for cap in rust_macro_pattern.find_iter(&content) {
177            let match_str = cap.as_str();
178            let start_offset = cap.start();
179            let end_offset = cap.end();
180
181            // Calculate line numbers from byte offsets
182            let line_number = content[..start_offset].matches('\n').count() + 1;
183            let end_line_number = content[..end_offset].matches('\n').count() + 1;
184
185            // Get the line content
186            let line_start_offset = content[..start_offset]
187                .rfind('\n')
188                .map(|pos| pos + 1)
189                .unwrap_or(0);
190            let line_content = content[line_start_offset..]
191                .lines()
192                .next()
193                .unwrap_or("")
194                .to_string();
195
196            // Check if commented
197            let is_commented = comment_pattern.is_match(&line_content);
198
199            if is_commented == find_commented {
200                // Extract original lines for multiline display
201                let multiline_content: Vec<String> =
202                    match_str.lines().map(|s| s.to_string()).collect();
203
204                matches.push(Match {
205                    file_path: file_path.clone(),
206                    line_number,
207                    end_line_number,
208                    line_content: match_str.replace('\n', " ").trim().to_string(),
209                    multiline_content,
210                });
211            }
212        }
213
214        // Find all Java output statements
215        for cap in java_pattern.find_iter(&content) {
216            let match_str = cap.as_str();
217            let start_offset = cap.start();
218            let end_offset = cap.end();
219
220            // Calculate line numbers from byte offsets
221            let line_number = content[..start_offset].matches('\n').count() + 1;
222            let end_line_number = content[..end_offset].matches('\n').count() + 1;
223
224            // Get the line content
225            let line_start_offset = content[..start_offset]
226                .rfind('\n')
227                .map(|pos| pos + 1)
228                .unwrap_or(0);
229            let line_content = content[line_start_offset..]
230                .lines()
231                .next()
232                .unwrap_or("")
233                .to_string();
234
235            // Check if commented
236            let is_commented = comment_pattern.is_match(&line_content);
237
238            if is_commented == find_commented {
239                // Extract original lines for multiline display
240                let multiline_content: Vec<String> =
241                    match_str.lines().map(|s| s.to_string()).collect();
242
243                matches.push(Match {
244                    file_path: file_path.clone(),
245                    line_number,
246                    end_line_number,
247                    line_content: match_str.replace('\n', " ").trim().to_string(),
248                    multiline_content,
249                });
250            }
251        }
252    }
253
254    // Remove duplicates based on file_path and line_number
255    // This can happen when the same line matches multiple patterns (e.g., printf in C and Java)
256    let mut seen = std::collections::HashSet::new();
257    matches.retain(|m| seen.insert((m.file_path.clone(), m.line_number)));
258
259    Ok(matches)
260}