Skip to main content

rullst_core/error_console/
parser.rs

1//! Stack frame extraction and source context inspection for error debugging.
2
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6
7/// Represents a single frame in the panic stack trace.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct StackFrame {
10    /// The source file path containing the frame.
11    pub file: String,
12    /// The line number in the source file.
13    pub line: u32,
14    /// The name of the function where the panic occurred.
15    pub function: String,
16}
17
18/// Parses the stack trace to find the developer's source file and line.
19#[cfg_attr(mutants, mutants::skip)]
20pub fn find_source_location(bt_str: &str) -> Option<(String, u32)> {
21    for line in bt_str.lines() {
22        let trimmed = line.trim();
23        if trimmed.contains("at ")
24            && (trimmed.contains("/src/")
25                || trimmed.contains("\\src\\")
26                || trimmed.contains("/examples/")
27                || trimmed.contains("\\examples\\")
28                || trimmed.contains("/tests/")
29                || trimmed.contains("\\tests\\"))
30        {
31            // Find the location after "at "
32            if let Some(pos) = trimmed.find("at ") {
33                let path_part = &trimmed[pos + 3..];
34                if let Some((file, line_str)) = path_part.rsplit_once(':')
35                    && let Ok(line_num) = line_str.trim().parse::<u32>()
36                {
37                    return Some((file.trim().to_string(), line_num));
38                }
39            }
40        }
41    }
42    None
43}
44
45/// Reads a file and extracts surrounding context lines.
46#[cfg_attr(mutants, mutants::skip)]
47pub fn extract_source_context(
48    file_path: &str,
49    target_line: u32,
50    range: u32,
51) -> Option<Vec<(u32, String, bool)>> {
52    let project_root = std::env::current_dir()
53        .map(|cwd| cwd.canonicalize().unwrap_or(cwd))
54        .ok()?;
55
56    let target_path = Path::new(file_path);
57    if target_path
58        .components()
59        .any(|c| matches!(c, std::path::Component::ParentDir))
60    {
61        return None;
62    }
63
64    let absolute_path = project_root.join(target_path);
65
66    let canonical = absolute_path.canonicalize().ok()?;
67    if !canonical.starts_with(&project_root) {
68        return None;
69    }
70
71    let content = fs::read_to_string(&canonical).ok()?;
72    let lines: Vec<&str> = content.lines().collect();
73
74    let total_lines = lines.len() as u32;
75    let start = if target_line > range {
76        target_line - range
77    } else {
78        1
79    };
80    let end = std::cmp::min(target_line + range, total_lines);
81
82    let mut context = Vec::new();
83    for i in start..=end {
84        if i >= 1 && i <= total_lines {
85            let line_content = lines[(i - 1) as usize].to_string();
86            let is_target = i == target_line;
87            context.push((i, line_content, is_target));
88        }
89    }
90    Some(context)
91}