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    let comment_pattern = Regex::new(r"^\s*//")?;
52
53    let entries: Vec<_> = if path.is_file() {
54        vec![path.to_path_buf()]
55    } else {
56        WalkDir::new(path)
57            .into_iter()
58            .filter_map(|e| e.ok())
59            .filter(|e| {
60                e.path().is_file()
61                    && e.path()
62                        .extension()
63                        .and_then(|s| s.to_str())
64                        .map(|ext| matches!(ext, "c" | "h" | "cpp" | "hpp" | "cc" | "cxx" | "rs"))
65                        .unwrap_or(false)
66            })
67            .map(|e| e.path().to_path_buf())
68            .collect()
69    };
70
71    for file_path in entries {
72        let content = fs::read_to_string(&file_path)
73            .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
74
75        // Find all C-style function calls
76        for cap in c_functions_pattern.find_iter(&content) {
77            let match_str = cap.as_str();
78            let start_offset = cap.start();
79            let end_offset = cap.end();
80
81            // Calculate line numbers from byte offsets
82            // Count newlines before the start position, then add 1
83            let line_number = content[..start_offset].matches('\n').count() + 1;
84            // For end line, count newlines up to the end position
85            let end_line_number = content[..end_offset].matches('\n').count() + 1;
86
87            // Get the line content (for display purposes, we'll get the first line of the match)
88            let line_start_offset = content[..start_offset]
89                .rfind('\n')
90                .map(|pos| pos + 1)
91                .unwrap_or(0);
92            let line_content = content[line_start_offset..]
93                .lines()
94                .next()
95                .unwrap_or("")
96                .to_string();
97
98            // Check if commented (check the beginning of the statement)
99            let is_commented = comment_pattern.is_match(&line_content);
100
101            if is_commented == find_commented {
102                // Extract original lines for multiline display
103                let multiline_content: Vec<String> =
104                    match_str.lines().map(|s| s.to_string()).collect();
105
106                matches.push(Match {
107                    file_path: file_path.clone(),
108                    line_number,
109                    end_line_number,
110                    line_content: match_str.replace('\n', " ").trim().to_string(),
111                    multiline_content,
112                });
113            }
114        }
115
116        // Find all C++ stream operations
117        for cap in cpp_stream_pattern.find_iter(&content) {
118            let match_str = cap.as_str();
119            let start_offset = cap.start();
120            let end_offset = cap.end();
121
122            // Calculate line numbers from byte offsets
123            // Count newlines before the start position, then add 1
124            let line_number = content[..start_offset].matches('\n').count() + 1;
125            // For end line, count newlines up to the end position
126            let end_line_number = content[..end_offset].matches('\n').count() + 1;
127
128            // Get the line content
129            let line_start_offset = content[..start_offset]
130                .rfind('\n')
131                .map(|pos| pos + 1)
132                .unwrap_or(0);
133            let line_content = content[line_start_offset..]
134                .lines()
135                .next()
136                .unwrap_or("")
137                .to_string();
138
139            // Check if commented
140            let is_commented = comment_pattern.is_match(&line_content);
141
142            if is_commented == find_commented {
143                // Extract original lines for multiline display
144                let multiline_content: Vec<String> =
145                    match_str.lines().map(|s| s.to_string()).collect();
146
147                matches.push(Match {
148                    file_path: file_path.clone(),
149                    line_number,
150                    end_line_number,
151                    line_content: match_str.replace('\n', " ").trim().to_string(),
152                    multiline_content,
153                });
154            }
155        }
156
157        // Find all Rust macro calls
158        for cap in rust_macro_pattern.find_iter(&content) {
159            let match_str = cap.as_str();
160            let start_offset = cap.start();
161            let end_offset = cap.end();
162
163            // Calculate line numbers from byte offsets
164            let line_number = content[..start_offset].matches('\n').count() + 1;
165            let end_line_number = content[..end_offset].matches('\n').count() + 1;
166
167            // Get the line content
168            let line_start_offset = content[..start_offset]
169                .rfind('\n')
170                .map(|pos| pos + 1)
171                .unwrap_or(0);
172            let line_content = content[line_start_offset..]
173                .lines()
174                .next()
175                .unwrap_or("")
176                .to_string();
177
178            // Check if commented
179            let is_commented = comment_pattern.is_match(&line_content);
180
181            if is_commented == find_commented {
182                // Extract original lines for multiline display
183                let multiline_content: Vec<String> =
184                    match_str.lines().map(|s| s.to_string()).collect();
185
186                matches.push(Match {
187                    file_path: file_path.clone(),
188                    line_number,
189                    end_line_number,
190                    line_content: match_str.replace('\n', " ").trim().to_string(),
191                    multiline_content,
192                });
193            }
194        }
195    }
196
197    Ok(matches)
198}