use anyhow::Result;
use colored::Colorize;
use ruchy::Parser as RuchyParser;
use std::fs;
use std::path::{Path, PathBuf};
pub fn handle_check_command(files: &[PathBuf], watch: bool) -> Result<()> {
validate_file_list(files)?;
if watch {
check_watch_mode(files)
} else if files.len() == 1 {
handle_check_syntax(&files[0])
} else {
check_multiple_files(files)
}
}
fn validate_file_list(files: &[PathBuf]) -> Result<()> {
if files.is_empty() {
anyhow::bail!("No files specified for checking");
}
Ok(())
}
fn check_watch_mode(files: &[PathBuf]) -> Result<()> {
if files.len() > 1 {
anyhow::bail!("Watch mode only supports checking a single file");
}
handle_watch_and_check(&files[0])
}
fn check_multiple_files(files: &[PathBuf]) -> Result<()> {
let mut all_valid = true;
for file in files {
if let Err(e) = handle_check_syntax(file) {
all_valid = false;
eprintln!("{e}");
}
}
if all_valid {
Ok(())
} else {
anyhow::bail!("Some files have syntax errors")
}
}
pub fn handle_check_syntax(file: &Path) -> Result<()> {
let source = super::read_file_with_context(file)?;
let mut parser = RuchyParser::new(&source);
match parser.parse() {
Ok(_) => {
println!("{}", "✓ Syntax is valid".green());
Ok(())
}
Err(e) => {
let filename = file.display();
let line_info = estimate_error_line(&source, &e.to_string());
let error_location = if let Some(line) = line_info {
format!("{filename}:{line}")
} else {
format!("{filename}")
};
eprintln!("{}", format!("✗ {error_location}: Syntax error: {e}").red());
Err(anyhow::anyhow!("{error_location}: Syntax error: {}", e))
}
}
}
pub fn estimate_error_line(source: &str, _error_msg: &str) -> Option<usize> {
let lines: Vec<&str> = source.lines().collect();
for (idx, line) in lines.iter().enumerate().rev() {
let trimmed = line.trim();
if !trimmed.is_empty() && !trimmed.starts_with("//") {
return Some(idx + 1); }
}
if lines.is_empty() {
None
} else {
Some(lines.len())
}
}
fn handle_watch_and_check(file: &Path) -> Result<()> {
use std::thread;
use std::time::Duration;
println!(
"{} Watching {} for changes...",
"👁".bright_cyan(),
file.display()
);
println!("Press Ctrl+C to stop watching\n");
handle_check_syntax(file)?;
let mut last_modified = fs::metadata(file)?.modified()?;
loop {
thread::sleep(Duration::from_millis(500));
let Ok(metadata) = fs::metadata(file) else {
continue; };
let Ok(modified) = metadata.modified() else {
continue;
};
if modified != last_modified {
last_modified = modified;
println!("\n{} File changed, checking...", "→".bright_cyan());
let _ = handle_check_syntax(file); }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_file_list_empty() {
let result = validate_file_list(&[]);
assert!(result.is_err());
}
#[test]
fn test_validate_file_list_non_empty() {
let files = vec![PathBuf::from("test.ruchy")];
let result = validate_file_list(&files);
assert!(result.is_ok());
}
#[test]
fn test_estimate_error_line_simple() {
let source = "line1\nline2\nline3";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(3));
}
#[test]
fn test_estimate_error_line_with_comments() {
let source = "line1\n// comment\n";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(1));
}
#[test]
fn test_estimate_error_line_empty() {
let source = "";
let line = estimate_error_line(source, "error");
assert_eq!(line, None);
}
#[test]
fn test_estimate_error_line_only_empty() {
let source = "\n\n\n";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(3)); }
#[test]
fn test_check_watch_mode_multiple_files() {
let files = vec![PathBuf::from("a.ruchy"), PathBuf::from("b.ruchy")];
let result = check_watch_mode(&files);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("only supports checking a single file"));
}
#[test]
fn test_estimate_error_line_single_line() {
let source = "let x = 42";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(1));
}
#[test]
fn test_estimate_error_line_all_comments() {
let source = "// comment1\n// comment2\n// comment3";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(3)); }
#[test]
fn test_estimate_error_line_mixed_content() {
let source = "let x = 1\n// comment\nlet y = 2\n// trailing";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(3)); }
#[test]
fn test_handle_check_command_empty_files() {
let result = handle_check_command(&[], false);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("No files specified"));
}
#[test]
fn test_handle_check_command_nonexistent_file() {
let files = vec![PathBuf::from("/nonexistent/file.ruchy")];
let result = handle_check_command(&files, false);
assert!(result.is_err());
}
#[test]
fn test_check_multiple_files_all_nonexistent() {
let files = vec![
PathBuf::from("/nonexistent/a.ruchy"),
PathBuf::from("/nonexistent/b.ruchy"),
];
let result = check_multiple_files(&files);
assert!(result.is_err());
}
#[test]
fn test_validate_file_list_multiple_files() {
let files = vec![
PathBuf::from("a.ruchy"),
PathBuf::from("b.ruchy"),
PathBuf::from("c.ruchy"),
];
let result = validate_file_list(&files);
assert!(result.is_ok());
}
#[test]
fn test_estimate_error_line_whitespace_only() {
let source = " \n \n ";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(3));
}
#[test]
fn test_handle_check_syntax_valid_file() {
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("valid.ruchy");
fs::write(&file_path, "let x = 42").unwrap();
let result = handle_check_syntax(&file_path);
assert!(result.is_ok());
}
#[test]
fn test_handle_check_syntax_invalid_file() {
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("invalid.ruchy");
fs::write(&file_path, "let x = {").unwrap();
let result = handle_check_syntax(&file_path);
assert!(result.is_err());
}
#[test]
fn test_handle_check_command_single_file() {
let temp_dir = tempfile::TempDir::new().unwrap();
let file_path = temp_dir.path().join("test.ruchy");
fs::write(&file_path, "42").unwrap();
let files = vec![file_path];
let result = handle_check_command(&files, false);
assert!(result.is_ok());
}
#[test]
fn test_estimate_error_line_code_then_comment() {
let source = "fun foo() { }\n// end of file";
let line = estimate_error_line(source, "error");
assert_eq!(line, Some(1));
}
#[test]
fn test_check_watch_mode_single_file() {
let files = vec![PathBuf::from("/nonexistent/file.ruchy")];
let result = check_watch_mode(&files);
assert!(result.is_err());
}
}