//! Elm-style diagnostic messages for beautiful error reporting //! //! This module provides Elm-quality error messages with: //! - Error codes (E0001, E0002, etc.) for easy searchability //! - Visual type diffs showing expected vs actual //! - "Did you mean?" suggestions using Levenshtein distance //! - Context-aware hints based on error type //! - Colored output with syntax highlighting #![allow(dead_code)] use crate::ast::Span; /// Error codes for all Lux compiler errors /// These follow the pattern E{category}{number}: /// - E01xx: Parse errors /// - E02xx: Type errors /// - E03xx: Name resolution errors /// - E04xx: Effect errors /// - E05xx: Pattern matching errors /// - E06xx: Import/module errors /// - E07xx: Behavioral type errors #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ErrorCode { // Parse errors (E01xx) E0100, // Generic parse error E0101, // Unexpected token E0102, // Unclosed delimiter E0103, // Invalid literal E0104, // Missing semicolon or delimiter E0105, // Invalid operator // Type errors (E02xx) E0200, // Generic type error E0201, // Type mismatch E0202, // Cannot unify types E0203, // Missing type annotation E0204, // Invalid type application E0205, // Recursive type without indirection E0206, // Field type mismatch E0207, // Missing record field E0208, // Extra record field E0209, // Wrong number of type arguments E0210, // Type not found E0211, // Return type mismatch // Name resolution errors (E03xx) E0300, // Generic name error E0301, // Undefined variable E0302, // Undefined function E0303, // Undefined type E0304, // Undefined module E0305, // Duplicate definition E0306, // Shadowing warning E0307, // Unused variable warning E0308, // Undefined field // Effect errors (E04xx) E0400, // Generic effect error E0401, // Unhandled effect E0402, // Effect not in scope E0403, // Missing effect handler E0404, // Invalid effect operation E0405, // Effect mismatch // Pattern matching errors (E05xx) E0500, // Generic pattern error E0501, // Non-exhaustive patterns E0502, // Unreachable pattern E0503, // Invalid pattern E0504, // Duplicate pattern binding // Import/module errors (E06xx) E0600, // Generic module error E0601, // Module not found E0602, // Circular import E0603, // Export not found E0604, // Private item accessed // Behavioral type errors (E07xx) E0700, // Generic behavioral error E0701, // Purity violation E0702, // Totality violation E0703, // Idempotency violation E0704, // Commutativity violation } impl ErrorCode { pub fn code(&self) -> &'static str { match self { // Parse errors ErrorCode::E0100 => "E0100", ErrorCode::E0101 => "E0101", ErrorCode::E0102 => "E0102", ErrorCode::E0103 => "E0103", ErrorCode::E0104 => "E0104", ErrorCode::E0105 => "E0105", // Type errors ErrorCode::E0200 => "E0200", ErrorCode::E0201 => "E0201", ErrorCode::E0202 => "E0202", ErrorCode::E0203 => "E0203", ErrorCode::E0204 => "E0204", ErrorCode::E0205 => "E0205", ErrorCode::E0206 => "E0206", ErrorCode::E0207 => "E0207", ErrorCode::E0208 => "E0208", ErrorCode::E0209 => "E0209", ErrorCode::E0210 => "E0210", ErrorCode::E0211 => "E0211", // Name errors ErrorCode::E0300 => "E0300", ErrorCode::E0301 => "E0301", ErrorCode::E0302 => "E0302", ErrorCode::E0303 => "E0303", ErrorCode::E0304 => "E0304", ErrorCode::E0305 => "E0305", ErrorCode::E0306 => "E0306", ErrorCode::E0307 => "E0307", ErrorCode::E0308 => "E0308", // Effect errors ErrorCode::E0400 => "E0400", ErrorCode::E0401 => "E0401", ErrorCode::E0402 => "E0402", ErrorCode::E0403 => "E0403", ErrorCode::E0404 => "E0404", ErrorCode::E0405 => "E0405", // Pattern errors ErrorCode::E0500 => "E0500", ErrorCode::E0501 => "E0501", ErrorCode::E0502 => "E0502", ErrorCode::E0503 => "E0503", ErrorCode::E0504 => "E0504", // Module errors ErrorCode::E0600 => "E0600", ErrorCode::E0601 => "E0601", ErrorCode::E0602 => "E0602", ErrorCode::E0603 => "E0603", ErrorCode::E0604 => "E0604", // Behavioral errors ErrorCode::E0700 => "E0700", ErrorCode::E0701 => "E0701", ErrorCode::E0702 => "E0702", ErrorCode::E0703 => "E0703", ErrorCode::E0704 => "E0704", } } pub fn title(&self) -> &'static str { match self { // Parse errors ErrorCode::E0100 => "Parse Error", ErrorCode::E0101 => "Unexpected Token", ErrorCode::E0102 => "Unclosed Delimiter", ErrorCode::E0103 => "Invalid Literal", ErrorCode::E0104 => "Missing Delimiter", ErrorCode::E0105 => "Invalid Operator", // Type errors ErrorCode::E0200 => "Type Error", ErrorCode::E0201 => "Type Mismatch", ErrorCode::E0202 => "Cannot Unify Types", ErrorCode::E0203 => "Missing Type Annotation", ErrorCode::E0204 => "Invalid Type Application", ErrorCode::E0205 => "Recursive Type Error", ErrorCode::E0206 => "Field Type Mismatch", ErrorCode::E0207 => "Missing Record Field", ErrorCode::E0208 => "Extra Record Field", ErrorCode::E0209 => "Wrong Type Arity", ErrorCode::E0210 => "Type Not Found", ErrorCode::E0211 => "Return Type Mismatch", // Name errors ErrorCode::E0300 => "Name Error", ErrorCode::E0301 => "Undefined Variable", ErrorCode::E0302 => "Undefined Function", ErrorCode::E0303 => "Undefined Type", ErrorCode::E0304 => "Undefined Module", ErrorCode::E0305 => "Duplicate Definition", ErrorCode::E0306 => "Variable Shadowing", ErrorCode::E0307 => "Unused Variable", ErrorCode::E0308 => "Undefined Field", // Effect errors ErrorCode::E0400 => "Effect Error", ErrorCode::E0401 => "Unhandled Effect", ErrorCode::E0402 => "Effect Not In Scope", ErrorCode::E0403 => "Missing Effect Handler", ErrorCode::E0404 => "Invalid Effect Operation", ErrorCode::E0405 => "Effect Mismatch", // Pattern errors ErrorCode::E0500 => "Pattern Error", ErrorCode::E0501 => "Non-Exhaustive Patterns", ErrorCode::E0502 => "Unreachable Pattern", ErrorCode::E0503 => "Invalid Pattern", ErrorCode::E0504 => "Duplicate Pattern Binding", // Module errors ErrorCode::E0600 => "Module Error", ErrorCode::E0601 => "Module Not Found", ErrorCode::E0602 => "Circular Import", ErrorCode::E0603 => "Export Not Found", ErrorCode::E0604 => "Private Item Access", // Behavioral errors ErrorCode::E0700 => "Behavioral Type Error", ErrorCode::E0701 => "Purity Violation", ErrorCode::E0702 => "Totality Violation", ErrorCode::E0703 => "Idempotency Violation", ErrorCode::E0704 => "Commutativity Violation", } } /// Get a URL to documentation about this error pub fn help_url(&self) -> String { format!("https://lux-lang.dev/errors/{}", self.code()) } } impl std::fmt::Display for ErrorCode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.code()) } } /// ANSI color codes for terminal output pub mod colors { pub const RESET: &str = "\x1b[0m"; pub const BOLD: &str = "\x1b[1m"; pub const DIM: &str = "\x1b[2m"; pub const RED: &str = "\x1b[31m"; pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; pub const BLUE: &str = "\x1b[34m"; pub const MAGENTA: &str = "\x1b[35m"; pub const CYAN: &str = "\x1b[36m"; pub const WHITE: &str = "\x1b[37m"; pub const GRAY: &str = "\x1b[90m"; } /// Apply color to text, respecting NO_COLOR / TERM=dumb pub fn c(color: &str, text: &str) -> String { if supports_color() { format!("{}{}{}", color, text, colors::RESET) } else { text.to_string() } } /// Apply bold + color to text pub fn bc(color: &str, text: &str) -> String { if supports_color() { format!("{}{}{}{}", colors::BOLD, color, text, colors::RESET) } else { text.to_string() } } /// Severity level for diagnostics #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Severity { Error, Warning, Hint, } impl Severity { fn color(&self) -> &'static str { match self { Severity::Error => colors::RED, Severity::Warning => colors::YELLOW, Severity::Hint => colors::CYAN, } } fn label(&self) -> &'static str { match self { Severity::Error => "ERROR", Severity::Warning => "WARNING", Severity::Hint => "HINT", } } } /// A diagnostic message with source location and optional hints #[derive(Debug, Clone)] pub struct Diagnostic { pub severity: Severity, pub code: Option, pub title: String, pub message: String, pub span: Span, pub hints: Vec, pub expected_type: Option, pub actual_type: Option, pub secondary_spans: Vec<(Span, String)>, } impl Diagnostic { pub fn error(title: impl Into, message: impl Into, span: Span) -> Self { Self { severity: Severity::Error, code: None, title: title.into(), message: message.into(), span, hints: Vec::new(), expected_type: None, actual_type: None, secondary_spans: Vec::new(), } } pub fn warning(title: impl Into, message: impl Into, span: Span) -> Self { Self { severity: Severity::Warning, code: None, title: title.into(), message: message.into(), span, hints: Vec::new(), expected_type: None, actual_type: None, secondary_spans: Vec::new(), } } /// Create an error with a specific error code pub fn with_code(code: ErrorCode, message: impl Into, span: Span) -> Self { Self { severity: Severity::Error, code: Some(code), title: code.title().to_string(), message: message.into(), span, hints: Vec::new(), expected_type: None, actual_type: None, secondary_spans: Vec::new(), } } pub fn with_hint(mut self, hint: impl Into) -> Self { self.hints.push(hint.into()); self } pub fn with_hints(mut self, hints: Vec) -> Self { self.hints.extend(hints); self } /// Add expected and actual types for type mismatch visualization pub fn with_types(mut self, expected: impl Into, actual: impl Into) -> Self { self.expected_type = Some(expected.into()); self.actual_type = Some(actual.into()); self } /// Add a secondary span with a label (e.g., "defined here") pub fn with_secondary(mut self, span: Span, label: impl Into) -> Self { self.secondary_spans.push((span, label.into())); self } /// Set the error code pub fn code(mut self, code: ErrorCode) -> Self { self.code = Some(code); self.title = code.title().to_string(); self } } /// Format a visual type diff between expected and actual types /// Highlights the differences in red pub fn format_type_diff(expected: &str, actual: &str) -> String { let mut output = String::new(); output.push_str(&format!( "\n {}{}Expected:{} {}{}{}\n", colors::BOLD, colors::CYAN, colors::RESET, colors::BOLD, expected, colors::RESET )); output.push_str(&format!( " {}{}Found: {} {}{}{}\n", colors::BOLD, colors::RED, colors::RESET, colors::BOLD, actual, colors::RESET )); // If types are structurally similar, show where they differ if let Some(diff) = compute_type_diff(expected, actual) { output.push_str(&format!("\n {}\n", diff)); } output } /// Compute a simple diff between two type strings fn compute_type_diff(expected: &str, actual: &str) -> Option { // Simple case: find the first difference let exp_chars: Vec = expected.chars().collect(); let act_chars: Vec = actual.chars().collect(); let mut diff_start = None; for (i, (e, a)) in exp_chars.iter().zip(act_chars.iter()).enumerate() { if e != a && diff_start.is_none() { diff_start = Some(i); break; } } // If the types are identical, no diff needed if diff_start.is_none() && exp_chars.len() == act_chars.len() { return None; } // Show the difference location with an arrow if let Some(start) = diff_start { let pointer = format!( "{}{}^-- difference starts here{}", " ".repeat(start + 9), // Account for "Found: " prefix colors::YELLOW, colors::RESET ); return Some(pointer); } // Length difference if exp_chars.len() != act_chars.len() { return Some(format!( "{}Note:{} Types have different lengths ({} vs {} characters)", colors::YELLOW, colors::RESET, exp_chars.len(), act_chars.len() )); } None } /// Format a plain type diff (no colors) pub fn format_type_diff_plain(expected: &str, actual: &str) -> String { format!( "\n Expected: {}\n Found: {}\n", expected, actual ) } /// Calculate the Levenshtein edit distance between two strings pub fn levenshtein_distance(a: &str, b: &str) -> usize { let a_len = a.chars().count(); let b_len = b.chars().count(); if a_len == 0 { return b_len; } if b_len == 0 { return a_len; } let a_chars: Vec = a.chars().collect(); let b_chars: Vec = b.chars().collect(); let mut matrix = vec![vec![0usize; b_len + 1]; a_len + 1]; for i in 0..=a_len { matrix[i][0] = i; } for j in 0..=b_len { matrix[0][j] = j; } for i in 1..=a_len { for j in 1..=b_len { let cost = if a_chars[i - 1] == b_chars[j - 1] { 0 } else { 1 }; matrix[i][j] = std::cmp::min( std::cmp::min( matrix[i - 1][j] + 1, // deletion matrix[i][j - 1] + 1, // insertion ), matrix[i - 1][j - 1] + cost, // substitution ); } } matrix[a_len][b_len] } /// Find similar names from a list of candidates /// Returns names within the given edit distance, sorted by similarity pub fn find_similar_names<'a>( target: &str, candidates: impl IntoIterator, max_distance: usize, ) -> Vec { let mut matches: Vec<(String, usize)> = candidates .into_iter() .filter(|&c| c != target) // Don't suggest the same name .map(|c| (c.to_string(), levenshtein_distance(target, c))) .filter(|(_, dist)| *dist <= max_distance && *dist > 0) .collect(); // Sort by distance (closest first), then alphabetically matches.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(&b.0))); // Return just the names, limited to top 3 matches.into_iter().take(3).map(|(name, _)| name).collect() } /// Format "Did you mean?" hint from suggestions pub fn format_did_you_mean(suggestions: &[String]) -> Option { match suggestions.len() { 0 => None, 1 => Some(format!("Did you mean '{}'?", suggestions[0])), 2 => Some(format!("Did you mean '{}' or '{}'?", suggestions[0], suggestions[1])), _ => Some(format!( "Did you mean '{}', '{}', or '{}'?", suggestions[0], suggestions[1], suggestions[2] )), } } /// Converts byte offset to (line, column) - 1-indexed pub fn offset_to_line_col(source: &str, offset: usize) -> (usize, usize) { let mut line = 1; let mut col = 1; for (i, ch) in source.char_indices() { if i >= offset { break; } if ch == '\n' { line += 1; col = 1; } else { col += 1; } } (line, col) } /// Extract a source line by line number (1-indexed) pub fn get_source_line(source: &str, line_num: usize) -> Option<&str> { source.lines().nth(line_num.saturating_sub(1)) } /// Get context lines around a span pub fn get_context_lines(source: &str, span: Span, context: usize) -> Vec<(usize, &str)> { let (start_line, _) = offset_to_line_col(source, span.start); let (end_line, _) = offset_to_line_col(source, span.end); let first_line = start_line.saturating_sub(context); let last_line = end_line + context; source .lines() .enumerate() .filter(|(i, _)| *i + 1 >= first_line && *i + 1 <= last_line) .map(|(i, line)| (i + 1, line)) .collect() } /// Render a diagnostic to a string with colors pub fn render_diagnostic( diagnostic: &Diagnostic, source: &str, filename: Option<&str>, ) -> String { let mut output = String::new(); let (line, col) = offset_to_line_col(source, diagnostic.span.start); let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end); let severity_color = diagnostic.severity.color(); let severity_label = diagnostic.severity.label(); // Header with error code: ── ERROR[E0201] ── filename.lux let filename_str = filename.unwrap_or(""); let code_str = diagnostic .code .map(|c| format!("[{}]", c.code())) .unwrap_or_default(); output.push_str(&format!( "{}{}── {}{} ──────────────────── {}{}\n", colors::BOLD, severity_color, severity_label, code_str, filename_str, colors::RESET )); // Location output.push_str(&format!( "\n{}{}{}:{}{}\n\n", colors::BOLD, colors::WHITE, line, col, colors::RESET )); // Error title/category output.push_str(&format!( "{}{}{}{}\n\n", colors::BOLD, severity_color, diagnostic.title, colors::RESET )); // Source code snippet with line numbers and context let context_lines = 2; // Show 2 lines before and after let start_line = line.saturating_sub(context_lines).max(1); let end_context_line = (end_line + context_lines).min(source.lines().count()); let line_num_width = end_context_line.to_string().len().max(4); // Show context before, error lines, and context after for line_num in start_line..=end_context_line { if let Some(source_line) = get_source_line(source, line_num) { let is_error_line = line_num >= line && line_num <= end_line; // Line number (dimmed for context, normal for error lines) if is_error_line { output.push_str(&format!( "{}{:>width$} │{} ", colors::DIM, line_num, colors::RESET, width = line_num_width )); // Source line (normal) output.push_str(source_line); } else { // Context lines are fully dimmed output.push_str(&format!( "{}{:>width$} │ {}{}", colors::DIM, line_num, source_line, colors::RESET, width = line_num_width )); } output.push('\n'); // Underline the error span (only for error lines) if is_error_line { let underline_start = if line_num == line { col - 1 } else { 0 }; let underline_end = if line_num == end_line { end_col - 1 } else { source_line.len() }; let underline_len = underline_end.saturating_sub(underline_start).max(1); output.push_str(&format!( "{}{:>width$} │{} ", colors::DIM, "", colors::RESET, width = line_num_width )); output.push_str(&" ".repeat(underline_start)); output.push_str(&format!( "{}{}{}", severity_color, "^".repeat(underline_len), colors::RESET )); output.push('\n'); } } } // Error message output.push_str(&format!("\n{}\n", diagnostic.message)); // Type diff visualization (if present) if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) { output.push_str(&format_type_diff(expected, actual)); } // Hints if !diagnostic.hints.is_empty() { output.push('\n'); for hint in &diagnostic.hints { output.push_str(&format!( "{}{}Hint:{} {}\n", colors::BOLD, colors::CYAN, colors::RESET, hint )); } } // Help URL for error codes if let Some(code) = diagnostic.code { output.push_str(&format!( "\n{}{}Learn more:{} {}\n", colors::DIM, colors::BLUE, colors::RESET, code.help_url() )); } output.push('\n'); output } /// Render a diagnostic without colors (for testing or non-TTY output) pub fn render_diagnostic_plain( diagnostic: &Diagnostic, source: &str, filename: Option<&str>, ) -> String { let mut output = String::new(); let (line, col) = offset_to_line_col(source, diagnostic.span.start); let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end); let filename_str = filename.unwrap_or(""); let code_str = diagnostic .code .map(|c| format!("[{}]", c.code())) .unwrap_or_default(); output.push_str(&format!( "-- {}{} ── {} ──────────────────────────────────\n", diagnostic.severity.label(), code_str, filename_str )); output.push_str(&format!("\n{}:{}\n\n", line, col)); output.push_str(&format!("{}\n\n", diagnostic.title)); let line_num_width = end_line.to_string().len().max(4); for line_num in line..=end_line { if let Some(source_line) = get_source_line(source, line_num) { output.push_str(&format!( "{:>width$} | ", line_num, width = line_num_width )); output.push_str(source_line); output.push('\n'); let underline_start = if line_num == line { col - 1 } else { 0 }; let underline_end = if line_num == end_line { end_col - 1 } else { source_line.len() }; let underline_len = underline_end.saturating_sub(underline_start).max(1); output.push_str(&format!( "{:>width$} | ", "", width = line_num_width )); output.push_str(&" ".repeat(underline_start)); output.push_str(&"^".repeat(underline_len)); output.push('\n'); } } output.push_str(&format!("\n{}\n", diagnostic.message)); // Type diff visualization (plain) if let (Some(expected), Some(actual)) = (&diagnostic.expected_type, &diagnostic.actual_type) { output.push_str(&format_type_diff_plain(expected, actual)); } if !diagnostic.hints.is_empty() { output.push('\n'); for hint in &diagnostic.hints { output.push_str(&format!("Hint: {}\n", hint)); } } // Help URL for error codes if let Some(code) = diagnostic.code { output.push_str(&format!("\nLearn more: {}\n", code.help_url())); } output.push('\n'); output } /// Check if stdout is a TTY (for color support) pub fn supports_color() -> bool { // Simple check - in production you'd use atty crate std::env::var("NO_COLOR").is_err() && std::env::var("TERM").map(|t| t != "dumb").unwrap_or(true) } /// Render diagnostic with automatic color detection pub fn render(diagnostic: &Diagnostic, source: &str, filename: Option<&str>) -> String { if supports_color() { render_diagnostic(diagnostic, source, filename) } else { render_diagnostic_plain(diagnostic, source, filename) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_offset_to_line_col() { let source = "line one\nline two\nline three"; assert_eq!(offset_to_line_col(source, 0), (1, 1)); assert_eq!(offset_to_line_col(source, 5), (1, 6)); assert_eq!(offset_to_line_col(source, 9), (2, 1)); assert_eq!(offset_to_line_col(source, 14), (2, 6)); } #[test] fn test_get_source_line() { let source = "line one\nline two\nline three"; assert_eq!(get_source_line(source, 1), Some("line one")); assert_eq!(get_source_line(source, 2), Some("line two")); assert_eq!(get_source_line(source, 3), Some("line three")); assert_eq!(get_source_line(source, 4), None); } #[test] fn test_render_diagnostic_plain() { let source = "fn test() {\n unknownVar\n}"; let diag = Diagnostic::error( "Unknown Variable", "The variable 'unknownVar' is not in scope.", Span { start: 16, end: 26 }, ) .with_hint("Did you mean 'knownVar'?"); let output = render_diagnostic_plain(&diag, source, Some("test.lux")); assert!(output.contains("ERROR")); assert!(output.contains("test.lux")); assert!(output.contains("Unknown Variable")); assert!(output.contains("unknownVar")); assert!(output.contains("Hint:")); } #[test] fn test_diagnostic_builder() { let diag = Diagnostic::error("Test", "Message", Span { start: 0, end: 5 }) .with_hint("Hint 1") .with_hint("Hint 2") .with_hints(vec!["Hint 3".to_string()]); assert_eq!(diag.hints.len(), 3); assert_eq!(diag.severity, Severity::Error); } #[test] fn test_multiline_span() { let source = "fn add(a: Int, b: Int): Int {\n a +\n b\n}"; let diag = Diagnostic::error( "Multiline Error", "This spans multiple lines.", Span { start: 29, end: 43 }, ); let output = render_diagnostic_plain(&diag, source, None); assert!(output.contains("Multiline Error")); // Should show both lines assert!(output.contains("a +")); assert!(output.contains("b")); } #[test] fn test_type_error_categorization() { use super::{Diagnostic, ErrorCode}; // Test that errors are properly categorized let source = "let x: Int = \"hello\""; // Simulate a type mismatch diagnostic with error code let diag = Diagnostic::with_code( ErrorCode::E0201, "Type mismatch: expected Int, got String", Span { start: 13, end: 20 }, ) .with_types("Int", "String") .with_hint("Check that the types on both sides of the expression are compatible."); let output = render_diagnostic_plain(&diag, source, Some("test.lux")); assert!(output.contains("Type Mismatch")); assert!(output.contains("\"hello\"")); assert!(output.contains("Hint:")); assert!(output.contains("[E0201]")); assert!(output.contains("Expected: Int")); assert!(output.contains("Found: String")); } #[test] fn test_empty_source() { let source = ""; let diag = Diagnostic::error("Empty", "No source.", Span { start: 0, end: 0 }); let output = render_diagnostic_plain(&diag, source, None); assert!(output.contains("Empty")); } #[test] fn test_single_char_error() { let source = "x + y"; let diag = Diagnostic::error("Operator", "Invalid op.", Span { start: 2, end: 3 }); let output = render_diagnostic_plain(&diag, source, None); assert!(output.contains("+")); assert!(output.contains("^")); } #[test] fn test_levenshtein_distance_identical() { assert_eq!(super::levenshtein_distance("hello", "hello"), 0); } #[test] fn test_levenshtein_distance_one_char() { assert_eq!(super::levenshtein_distance("hello", "hallo"), 1); assert_eq!(super::levenshtein_distance("cat", "car"), 1); } #[test] fn test_levenshtein_distance_insertion() { assert_eq!(super::levenshtein_distance("cat", "cats"), 1); } #[test] fn test_levenshtein_distance_deletion() { assert_eq!(super::levenshtein_distance("cats", "cat"), 1); } #[test] fn test_levenshtein_distance_empty() { assert_eq!(super::levenshtein_distance("", "hello"), 5); assert_eq!(super::levenshtein_distance("hello", ""), 5); } #[test] fn test_find_similar_names_basic() { let candidates = vec!["println", "print", "printf", "sprint"]; let similar = super::find_similar_names("prnt", candidates.into_iter(), 2); assert!(similar.contains(&"print".to_string())); } #[test] fn test_find_similar_names_no_match() { let candidates = vec!["apple", "banana", "cherry"]; let similar = super::find_similar_names("xyz", candidates.into_iter(), 2); assert!(similar.is_empty()); } #[test] fn test_find_similar_names_excludes_exact() { let candidates = vec!["hello", "hallo", "world"]; let similar = super::find_similar_names("hello", candidates.into_iter(), 2); assert!(!similar.contains(&"hello".to_string())); assert!(similar.contains(&"hallo".to_string())); } #[test] fn test_format_did_you_mean_none() { assert_eq!(super::format_did_you_mean(&[]), None); } #[test] fn test_format_did_you_mean_one() { let hint = super::format_did_you_mean(&["print".to_string()]); assert_eq!(hint, Some("Did you mean 'print'?".to_string())); } #[test] fn test_format_did_you_mean_two() { let hint = super::format_did_you_mean(&["print".to_string(), "println".to_string()]); assert_eq!(hint, Some("Did you mean 'print' or 'println'?".to_string())); } #[test] fn test_format_did_you_mean_three() { let hint = super::format_did_you_mean(&["a".to_string(), "b".to_string(), "c".to_string()]); assert_eq!(hint, Some("Did you mean 'a', 'b', or 'c'?".to_string())); } #[test] fn test_error_code_display() { assert_eq!(super::ErrorCode::E0201.code(), "E0201"); assert_eq!(super::ErrorCode::E0201.title(), "Type Mismatch"); assert!(super::ErrorCode::E0201.help_url().contains("E0201")); } #[test] fn test_diagnostic_with_code() { let diag = super::Diagnostic::with_code( super::ErrorCode::E0301, "Variable 'x' not found", Span { start: 0, end: 1 }, ); assert_eq!(diag.code, Some(super::ErrorCode::E0301)); assert_eq!(diag.title, "Undefined Variable"); } #[test] fn test_diagnostic_with_types() { let diag = super::Diagnostic::error("Test", "Msg", Span { start: 0, end: 1 }) .with_types("Int", "String"); assert_eq!(diag.expected_type, Some("Int".to_string())); assert_eq!(diag.actual_type, Some("String".to_string())); } #[test] fn test_type_diff_plain() { let diff = super::format_type_diff_plain("Int", "String"); assert!(diff.contains("Expected: Int")); assert!(diff.contains("Found: String")); } #[test] fn test_diagnostic_render_with_all_features() { let source = "let x: Int = \"hello\""; let diag = super::Diagnostic::with_code( super::ErrorCode::E0201, "This value should be an Int but is a String", Span { start: 13, end: 20 }, ) .with_types("Int", "String") .with_hint("Try using String.parseInt to convert the string to an integer"); let output = super::render_diagnostic_plain(&diag, source, Some("test.lux")); // Check all features are present assert!(output.contains("[E0201]")); assert!(output.contains("Type Mismatch")); assert!(output.contains("Expected: Int")); assert!(output.contains("Found: String")); assert!(output.contains("Hint:")); assert!(output.contains("parseInt")); assert!(output.contains("lux-lang.dev/errors/E0201")); } }