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    // Pattern to match Go output statements (multiline support with (?s))
65    // Go doesn't require semicolons, so we match just the closing paren
66    let go_pattern = if detect_all {
67        // Match all Go output statements regardless of content
68        Regex::new(
69            r"(?s)(fmt\.Println|fmt\.Printf|fmt\.Print|fmt\.Fprintln|fmt\.Fprintf|fmt\.Fprint|log\.Println|fmt\.Printf|log\.Print|log\.Fatal|log\.Fatalf|log\.Fatalln|log\.Panic|log\.Panicf|log\.Panicln)\s*\([^)]*\)",
70        )?
71    } else {
72        // Match only those with "debug" or "DEBUG"
73        Regex::new(
74            r"(?s)(fmt\.Println|fmt\.Printf|fmt\.Print|fmt\.Fprintln|fmt\.Fprintf|fmt\.Fprint|log\.Println|log\.Printf|log\.Print|log\.Fatal|log\.Fatalf|log\.Fatalln|log\.Panic|log\.Panicf|log\.Panicln)\s*\([^)]*?(debug|DEBUG)[^)]*\)",
75        )?
76    };
77
78    let comment_pattern = Regex::new(r"^\s*//")?;
79
80    let entries: Vec<_> = if path.is_file() {
81        vec![path.to_path_buf()]
82    } else {
83        WalkDir::new(path)
84            .into_iter()
85            .filter_map(|e| e.ok())
86            .filter(|e| {
87                e.path().is_file()
88                    && e.path()
89                        .extension()
90                        .and_then(|s| s.to_str())
91                        .map(|ext| {
92                            matches!(
93                                ext,
94                                "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "rs" | "java" | "go"
95                            )
96                        })
97                        .unwrap_or(false)
98            })
99            .map(|e| e.path().to_path_buf())
100            .collect()
101    };
102
103    for file_path in entries {
104        let content = fs::read_to_string(&file_path)
105            .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
106
107        // Find all C-style function calls
108        for cap in c_functions_pattern.find_iter(&content) {
109            let match_str = cap.as_str();
110            let start_offset = cap.start();
111            let end_offset = cap.end();
112
113            // Calculate line numbers from byte offsets
114            // Count newlines before the start position, then add 1
115            let line_number = content[..start_offset].matches('\n').count() + 1;
116            // For end line, count newlines up to the end position
117            let end_line_number = content[..end_offset].matches('\n').count() + 1;
118
119            // Get the line content (for display purposes, we'll get the first line of the match)
120            let line_start_offset = content[..start_offset]
121                .rfind('\n')
122                .map(|pos| pos + 1)
123                .unwrap_or(0);
124            let line_content = content[line_start_offset..]
125                .lines()
126                .next()
127                .unwrap_or("")
128                .to_string();
129
130            // Check if commented (check the beginning of the statement)
131            let is_commented = comment_pattern.is_match(&line_content);
132
133            if is_commented == find_commented {
134                // Extract original lines for multiline display
135                let multiline_content: Vec<String> =
136                    match_str.lines().map(|s| s.to_string()).collect();
137
138                matches.push(Match {
139                    file_path: file_path.clone(),
140                    line_number,
141                    end_line_number,
142                    line_content: match_str.replace('\n', " ").trim().to_string(),
143                    multiline_content,
144                });
145            }
146        }
147
148        // Find all C++ stream operations
149        for cap in cpp_stream_pattern.find_iter(&content) {
150            let match_str = cap.as_str();
151            let start_offset = cap.start();
152            let end_offset = cap.end();
153
154            // Calculate line numbers from byte offsets
155            // Count newlines before the start position, then add 1
156            let line_number = content[..start_offset].matches('\n').count() + 1;
157            // For end line, count newlines up to the end position
158            let end_line_number = content[..end_offset].matches('\n').count() + 1;
159
160            // Get the line content
161            let line_start_offset = content[..start_offset]
162                .rfind('\n')
163                .map(|pos| pos + 1)
164                .unwrap_or(0);
165            let line_content = content[line_start_offset..]
166                .lines()
167                .next()
168                .unwrap_or("")
169                .to_string();
170
171            // Check if commented
172            let is_commented = comment_pattern.is_match(&line_content);
173
174            if is_commented == find_commented {
175                // Extract original lines for multiline display
176                let multiline_content: Vec<String> =
177                    match_str.lines().map(|s| s.to_string()).collect();
178
179                matches.push(Match {
180                    file_path: file_path.clone(),
181                    line_number,
182                    end_line_number,
183                    line_content: match_str.replace('\n', " ").trim().to_string(),
184                    multiline_content,
185                });
186            }
187        }
188
189        // Find all Rust macro calls
190        for cap in rust_macro_pattern.find_iter(&content) {
191            let match_str = cap.as_str();
192            let start_offset = cap.start();
193            let end_offset = cap.end();
194
195            // Calculate line numbers from byte offsets
196            let line_number = content[..start_offset].matches('\n').count() + 1;
197            let end_line_number = content[..end_offset].matches('\n').count() + 1;
198
199            // Get the line content
200            let line_start_offset = content[..start_offset]
201                .rfind('\n')
202                .map(|pos| pos + 1)
203                .unwrap_or(0);
204            let line_content = content[line_start_offset..]
205                .lines()
206                .next()
207                .unwrap_or("")
208                .to_string();
209
210            // Check if commented
211            let is_commented = comment_pattern.is_match(&line_content);
212
213            if is_commented == find_commented {
214                // Extract original lines for multiline display
215                let multiline_content: Vec<String> =
216                    match_str.lines().map(|s| s.to_string()).collect();
217
218                matches.push(Match {
219                    file_path: file_path.clone(),
220                    line_number,
221                    end_line_number,
222                    line_content: match_str.replace('\n', " ").trim().to_string(),
223                    multiline_content,
224                });
225            }
226        }
227
228        // Find all Java output statements
229        for cap in java_pattern.find_iter(&content) {
230            let match_str = cap.as_str();
231            let start_offset = cap.start();
232            let end_offset = cap.end();
233
234            // Calculate line numbers from byte offsets
235            let line_number = content[..start_offset].matches('\n').count() + 1;
236            let end_line_number = content[..end_offset].matches('\n').count() + 1;
237
238            // Get the line content
239            let line_start_offset = content[..start_offset]
240                .rfind('\n')
241                .map(|pos| pos + 1)
242                .unwrap_or(0);
243            let line_content = content[line_start_offset..]
244                .lines()
245                .next()
246                .unwrap_or("")
247                .to_string();
248
249            // Check if commented
250            let is_commented = comment_pattern.is_match(&line_content);
251
252            if is_commented == find_commented {
253                // Extract original lines for multiline display
254                let multiline_content: Vec<String> =
255                    match_str.lines().map(|s| s.to_string()).collect();
256
257                matches.push(Match {
258                    file_path: file_path.clone(),
259                    line_number,
260                    end_line_number,
261                    line_content: match_str.replace('\n', " ").trim().to_string(),
262                    multiline_content,
263                });
264            }
265        }
266
267        // Find all Go output statements
268        for cap in go_pattern.find_iter(&content) {
269            let match_str = cap.as_str();
270            let start_offset = cap.start();
271            let end_offset = cap.end();
272
273            // Calculate line numbers from byte offsets
274            let line_number = content[..start_offset].matches('\n').count() + 1;
275            let end_line_number = content[..end_offset].matches('\n').count() + 1;
276
277            // Get the line content
278            let line_start_offset = content[..start_offset]
279                .rfind('\n')
280                .map(|pos| pos + 1)
281                .unwrap_or(0);
282            let line_content = content[line_start_offset..]
283                .lines()
284                .next()
285                .unwrap_or("")
286                .to_string();
287
288            // Check if commented
289            let is_commented = comment_pattern.is_match(&line_content);
290
291            if is_commented == find_commented {
292                // Extract original lines for multiline display
293                let multiline_content: Vec<String> =
294                    match_str.lines().map(|s| s.to_string()).collect();
295
296                matches.push(Match {
297                    file_path: file_path.clone(),
298                    line_number,
299                    end_line_number,
300                    line_content: match_str.replace('\n', " ").trim().to_string(),
301                    multiline_content,
302                });
303            }
304        }
305    }
306
307    // Remove duplicates based on file_path and line_number
308    // This can happen when the same line matches multiple patterns (e.g., printf in C and Java)
309    let mut seen = std::collections::HashSet::new();
310    matches.retain(|m| seen.insert((m.file_path.clone(), m.line_number)));
311
312    Ok(matches)
313}