Skip to content

Commit af3e0dc

Browse files
committed
refactor: Extract pure helpers across modules
Extract pure functions and helpers following functional programming principles: - io/real.rs: Refactor LCOV parsing with LcovLine enum and functional pipeline using fold, add comprehensive tests for parsing functions - anti_pattern_detector.rs: Extract box formatting constants and pure helper functions for Display implementations - codebase_type_analyzer.rs: Extract is_rust_file, parse_file_snapshot, and collect_rust_file_paths helpers with tests - tui/results/mod.rs: Extract is_quit_key, poll_key_event, render_frame, and process_next_event for cleaner event loop
1 parent 0e6fdf3 commit af3e0dc

File tree

4 files changed

+595
-201
lines changed

4 files changed

+595
-201
lines changed

src/io/real.rs

Lines changed: 171 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -99,57 +99,135 @@ impl CoverageLoader for RealCoverageLoader {
9999
}
100100
}
101101

102-
/// Parse LCOV format content into CoverageData.
102+
/// Represents a parsed LCOV line.
103+
#[derive(Debug, PartialEq)]
104+
enum LcovLine {
105+
/// Source file declaration (SF:path)
106+
SourceFile(std::path::PathBuf),
107+
/// Line data (DA:line_number,hit_count)
108+
LineData { line: usize, hits: u64 },
109+
/// End of record marker
110+
EndOfRecord,
111+
/// Unknown or empty line (ignored)
112+
Unknown,
113+
}
114+
115+
/// Parse a single LCOV line into its structured representation.
103116
///
104-
/// This is a pure function that parses LCOV content without I/O.
105-
fn parse_lcov_content(content: &str, source_path: &Path) -> Result<CoverageData, AnalysisError> {
106-
let mut data = CoverageData::new();
107-
let mut current_file: Option<std::path::PathBuf> = None;
108-
let mut current_coverage: Option<FileCoverage> = None;
117+
/// This is a pure function with no side effects.
118+
fn parse_lcov_line(line: &str) -> LcovLine {
119+
let line = line.trim();
120+
121+
if let Some(sf) = line.strip_prefix("SF:") {
122+
LcovLine::SourceFile(sf.into())
123+
} else if let Some(da) = line.strip_prefix("DA:") {
124+
parse_line_data(da)
125+
} else if line == "end_of_record" {
126+
LcovLine::EndOfRecord
127+
} else {
128+
LcovLine::Unknown
129+
}
130+
}
109131

110-
for line in content.lines() {
111-
let line = line.trim();
132+
/// Parse DA (line data) format: "line_number,hit_count".
133+
fn parse_line_data(da: &str) -> LcovLine {
134+
let mut parts = da.split(',');
135+
136+
let parsed = parts
137+
.next()
138+
.and_then(|line_str| line_str.parse::<usize>().ok())
139+
.zip(
140+
parts
141+
.next()
142+
.and_then(|hits_str| hits_str.parse::<u64>().ok()),
143+
);
144+
145+
match parsed {
146+
Some((line, hits)) => LcovLine::LineData { line, hits },
147+
None => LcovLine::Unknown,
148+
}
149+
}
150+
151+
/// Parser state for LCOV content processing.
152+
struct LcovParserState {
153+
data: CoverageData,
154+
current_file: Option<std::path::PathBuf>,
155+
current_coverage: Option<FileCoverage>,
156+
}
157+
158+
impl LcovParserState {
159+
fn new() -> Self {
160+
Self {
161+
data: CoverageData::new(),
162+
current_file: None,
163+
current_coverage: None,
164+
}
165+
}
112166

113-
if let Some(sf) = line.strip_prefix("SF:") {
114-
// Source file
115-
if let (Some(path), Some(coverage)) = (current_file.take(), current_coverage.take()) {
116-
data.add_file_coverage(path, coverage);
167+
/// Process a single parsed line, returning updated state.
168+
fn process(mut self, line: LcovLine) -> Self {
169+
match line {
170+
LcovLine::SourceFile(path) => {
171+
self.finalize_current();
172+
self.current_file = Some(path);
173+
self.current_coverage = Some(FileCoverage::new());
117174
}
118-
current_file = Some(sf.into());
119-
current_coverage = Some(FileCoverage::new());
120-
} else if let Some(da) = line.strip_prefix("DA:") {
121-
// Line data: DA:line_number,hit_count
122-
if let Some(ref mut coverage) = current_coverage {
123-
let parts: Vec<&str> = da.split(',').collect();
124-
if parts.len() >= 2 {
125-
if let (Ok(line_num), Ok(hits)) =
126-
(parts[0].parse::<usize>(), parts[1].parse::<u64>())
127-
{
128-
coverage.add_line(line_num, hits);
129-
}
175+
LcovLine::LineData { line, hits } => {
176+
if let Some(ref mut coverage) = self.current_coverage {
177+
coverage.add_line(line, hits);
130178
}
131179
}
132-
} else if line == "end_of_record" {
133-
// End of file record
134-
if let (Some(path), Some(coverage)) = (current_file.take(), current_coverage.take()) {
135-
data.add_file_coverage(path, coverage);
180+
LcovLine::EndOfRecord => {
181+
self.finalize_current();
136182
}
183+
LcovLine::Unknown => {}
137184
}
185+
self
138186
}
139187

140-
// Handle last file if no end_of_record
141-
if let (Some(path), Some(coverage)) = (current_file, current_coverage) {
142-
data.add_file_coverage(path, coverage);
188+
/// Finalize the current file record if one exists.
189+
fn finalize_current(&mut self) {
190+
if let (Some(path), Some(coverage)) =
191+
(self.current_file.take(), self.current_coverage.take())
192+
{
193+
self.data.add_file_coverage(path, coverage);
194+
}
143195
}
144196

145-
// Check if we actually parsed anything
197+
/// Complete parsing and return the final coverage data.
198+
fn finish(mut self) -> CoverageData {
199+
self.finalize_current();
200+
self.data
201+
}
202+
}
203+
204+
/// Parse LCOV format content into CoverageData.
205+
///
206+
/// This function uses a functional pipeline to parse LCOV content:
207+
/// 1. Parse each line into a structured representation
208+
/// 2. Fold over lines to accumulate coverage data
209+
fn parse_lcov_content(content: &str, source_path: &Path) -> Result<CoverageData, AnalysisError> {
210+
let data = content
211+
.lines()
212+
.map(parse_lcov_line)
213+
.fold(LcovParserState::new(), LcovParserState::process)
214+
.finish();
215+
216+
validate_coverage_data(data, content, source_path)
217+
}
218+
219+
/// Validate that parsed coverage data is not unexpectedly empty.
220+
fn validate_coverage_data(
221+
data: CoverageData,
222+
content: &str,
223+
source_path: &Path,
224+
) -> Result<CoverageData, AnalysisError> {
146225
if data.files().next().is_none() && !content.is_empty() {
147226
return Err(AnalysisError::coverage_with_path(
148227
"No coverage data found in LCOV file",
149228
source_path,
150229
));
151230
}
152-
153231
Ok(data)
154232
}
155233

@@ -336,4 +414,64 @@ end_of_record
336414
cache.invalidate("key1").unwrap();
337415
cache.clear().unwrap();
338416
}
417+
418+
#[test]
419+
fn test_parse_lcov_line_source_file() {
420+
let line = parse_lcov_line("SF:src/main.rs");
421+
assert_eq!(line, LcovLine::SourceFile("src/main.rs".into()));
422+
}
423+
424+
#[test]
425+
fn test_parse_lcov_line_line_data() {
426+
let line = parse_lcov_line("DA:42,5");
427+
assert_eq!(line, LcovLine::LineData { line: 42, hits: 5 });
428+
}
429+
430+
#[test]
431+
fn test_parse_lcov_line_end_of_record() {
432+
let line = parse_lcov_line("end_of_record");
433+
assert_eq!(line, LcovLine::EndOfRecord);
434+
}
435+
436+
#[test]
437+
fn test_parse_lcov_line_unknown() {
438+
assert_eq!(parse_lcov_line(""), LcovLine::Unknown);
439+
assert_eq!(parse_lcov_line(" "), LcovLine::Unknown);
440+
assert_eq!(parse_lcov_line("# comment"), LcovLine::Unknown);
441+
assert_eq!(parse_lcov_line("TN:test"), LcovLine::Unknown);
442+
}
443+
444+
#[test]
445+
fn test_parse_lcov_line_whitespace_handling() {
446+
let line = parse_lcov_line(" SF:src/lib.rs ");
447+
assert_eq!(line, LcovLine::SourceFile("src/lib.rs".into()));
448+
}
449+
450+
#[test]
451+
fn test_parse_line_data_invalid() {
452+
// Missing hit count
453+
assert_eq!(parse_line_data("42"), LcovLine::Unknown);
454+
// Non-numeric line
455+
assert_eq!(parse_line_data("abc,5"), LcovLine::Unknown);
456+
// Non-numeric hits
457+
assert_eq!(parse_line_data("42,abc"), LcovLine::Unknown);
458+
// Empty string
459+
assert_eq!(parse_line_data(""), LcovLine::Unknown);
460+
}
461+
462+
#[test]
463+
fn test_parse_lcov_content_without_end_of_record() {
464+
// Some LCOV generators don't include end_of_record for the last file
465+
let lcov_content = "SF:src/main.rs\nDA:1,5\nDA:2,3";
466+
let data = parse_lcov_content(lcov_content, Path::new("test.lcov")).unwrap();
467+
let coverage = data.get_file_coverage(Path::new("src/main.rs")).unwrap();
468+
assert!((coverage - 100.0).abs() < 0.1); // Both lines hit
469+
}
470+
471+
#[test]
472+
fn test_parse_lcov_content_invalid_format() {
473+
// Non-empty content but no valid LCOV data
474+
let result = parse_lcov_content("garbage\nmore garbage", Path::new("test.lcov"));
475+
assert!(result.is_err());
476+
}
339477
}

0 commit comments

Comments
 (0)