vsec 0.0.1

Detect secrets and in Rust codebases
Documentation
// src/parser/file_parser.rs

use std::path::Path;

use crate::error::{Result, SecretraceError};

/// Result of parsing a file
pub struct ParseResult {
    /// The parsed AST
    pub ast: syn::File,

    /// The original source code
    pub source: String,
}

impl ParseResult {
    /// Get line count
    pub fn line_count(&self) -> usize {
        self.source.lines().count()
    }

    /// Get a specific line (1-indexed)
    pub fn get_line(&self, line: u32) -> Option<&str> {
        self.source.lines().nth(line.saturating_sub(1) as usize)
    }

    /// Get lines around a position (for context)
    pub fn get_context(&self, line: u32, before: usize, after: usize) -> Vec<(u32, &str)> {
        let start = line.saturating_sub(before as u32);
        let end = line.saturating_add(after as u32);

        self.source
            .lines()
            .enumerate()
            .skip(start.saturating_sub(1) as usize)
            .take((end - start + 1) as usize)
            .map(|(i, l)| ((i + 1) as u32, l))
            .collect()
    }
}

/// Parse a Rust source file
pub fn parse_file(path: impl AsRef<Path>) -> Result<ParseResult> {
    let path = path.as_ref();
    let source = std::fs::read_to_string(path).map_err(|e| SecretraceError::FileRead {
        path: path.to_path_buf(),
        source: e,
    })?;

    let ast = syn::parse_file(&source).map_err(|e| SecretraceError::Parse {
        path: path.to_path_buf(),
        source: e,
    })?;

    Ok(ParseResult { ast, source })
}

/// Parse Rust source code from a string
pub fn parse_str(source: &str) -> std::result::Result<syn::File, syn::Error> {
    syn::parse_file(source)
}

/// Check if a file is likely parseable as Rust
pub fn is_rust_file(path: impl AsRef<Path>) -> bool {
    path.as_ref()
        .extension()
        .map(|e| e == "rs")
        .unwrap_or(false)
}

/// Check if a file is too large to parse efficiently
pub fn is_file_too_large(path: impl AsRef<Path>, max_bytes: usize) -> bool {
    std::fs::metadata(path.as_ref())
        .map(|m| m.len() as usize > max_bytes)
        .unwrap_or(true)
}

/// Quick check if file content looks like Rust
pub fn looks_like_rust(content: &str) -> bool {
    // Check for common Rust patterns
    content.contains("fn ")
        || content.contains("use ")
        || content.contains("mod ")
        || content.contains("struct ")
        || content.contains("enum ")
        || content.contains("impl ")
        || content.contains("const ")
        || content.contains("static ")
        || content.contains("pub ")
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    #[test]
    fn test_parse_valid_rust() {
        let code = r#"
            fn main() {
                println!("Hello");
            }
        "#;

        let result = parse_str(code);
        assert!(result.is_ok());
    }

    #[test]
    fn test_parse_invalid_rust() {
        let code = "fn broken( {";
        let result = parse_str(code);
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_file() {
        let mut file = NamedTempFile::new().unwrap();
        writeln!(file, "fn main() {{}}").unwrap();

        let result = parse_file(file.path());
        assert!(result.is_ok());
    }

    #[test]
    fn test_is_rust_file() {
        assert!(is_rust_file("src/main.rs"));
        assert!(is_rust_file("lib.rs"));
        assert!(!is_rust_file("README.md"));
        assert!(!is_rust_file("Cargo.toml"));
    }

    #[test]
    fn test_looks_like_rust() {
        assert!(looks_like_rust("fn main() {}"));
        assert!(looks_like_rust("use std::io;"));
        assert!(looks_like_rust("pub struct Foo;"));
        assert!(!looks_like_rust("Hello, World!"));
    }

    #[test]
    fn test_get_context() {
        let source = "line1\nline2\nline3\nline4\nline5".to_string();
        let result = ParseResult {
            ast: syn::parse_str("fn x(){}").unwrap(),
            source,
        };

        let context = result.get_context(3, 1, 1);
        assert_eq!(context.len(), 3);
        assert_eq!(context[0], (2, "line2"));
        assert_eq!(context[1], (3, "line3"));
        assert_eq!(context[2], (4, "line4"));
    }
}