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    let comment_pattern = Regex::new(r"^\s*//")?;
40
41    let entries: Vec<_> = if path.is_file() {
42        vec![path.to_path_buf()]
43    } else {
44        WalkDir::new(path)
45            .into_iter()
46            .filter_map(|e| e.ok())
47            .filter(|e| {
48                e.path().is_file()
49                    && e.path()
50                        .extension()
51                        .and_then(|s| s.to_str())
52                        .map(|ext| matches!(ext, "c" | "h" | "cpp" | "hpp" | "cc" | "cxx"))
53                        .unwrap_or(false)
54            })
55            .map(|e| e.path().to_path_buf())
56            .collect()
57    };
58
59    for file_path in entries {
60        let content = fs::read_to_string(&file_path)
61            .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
62
63        // Find all C-style function calls
64        for cap in c_functions_pattern.find_iter(&content) {
65            let match_str = cap.as_str();
66            let start_offset = cap.start();
67            let end_offset = cap.end();
68
69            // Calculate line numbers from byte offsets
70            // Count newlines before the start position, then add 1
71            let line_number = content[..start_offset].matches('\n').count() + 1;
72            // For end line, count newlines up to the end position
73            let end_line_number = content[..end_offset].matches('\n').count() + 1;
74
75            // Get the line content (for display purposes, we'll get the first line of the match)
76            let line_start_offset = content[..start_offset]
77                .rfind('\n')
78                .map(|pos| pos + 1)
79                .unwrap_or(0);
80            let line_content = content[line_start_offset..]
81                .lines()
82                .next()
83                .unwrap_or("")
84                .to_string();
85
86            // Check if commented (check the beginning of the statement)
87            let is_commented = comment_pattern.is_match(&line_content);
88
89            if is_commented == find_commented {
90                // Extract original lines for multiline display
91                let multiline_content: Vec<String> = match_str.lines().map(|s| s.to_string()).collect();
92
93                matches.push(Match {
94                    file_path: file_path.clone(),
95                    line_number,
96                    end_line_number,
97                    line_content: match_str.replace('\n', " ").trim().to_string(),
98                    multiline_content,
99                });
100            }
101        }
102
103        // Find all C++ stream operations
104        for cap in cpp_stream_pattern.find_iter(&content) {
105            let match_str = cap.as_str();
106            let start_offset = cap.start();
107            let end_offset = cap.end();
108
109            // Calculate line numbers from byte offsets
110            // Count newlines before the start position, then add 1
111            let line_number = content[..start_offset].matches('\n').count() + 1;
112            // For end line, count newlines up to the end position
113            let end_line_number = content[..end_offset].matches('\n').count() + 1;
114
115            // Get the line content
116            let line_start_offset = content[..start_offset]
117                .rfind('\n')
118                .map(|pos| pos + 1)
119                .unwrap_or(0);
120            let line_content = content[line_start_offset..]
121                .lines()
122                .next()
123                .unwrap_or("")
124                .to_string();
125
126            // Check if commented
127            let is_commented = comment_pattern.is_match(&line_content);
128
129            if is_commented == find_commented {
130                // Extract original lines for multiline display
131                let multiline_content: Vec<String> = match_str.lines().map(|s| s.to_string()).collect();
132
133                matches.push(Match {
134                    file_path: file_path.clone(),
135                    line_number,
136                    end_line_number,
137                    line_content: match_str.replace('\n', " ").trim().to_string(),
138                    multiline_content,
139                });
140            }
141        }
142    }
143
144    Ok(matches)
145}