feat: Elm-quality error messages with error codes
- Add ErrorCode enum with categorized codes (E01xx parse, E02xx type,
E03xx name, E04xx effect, E05xx pattern, E06xx module, E07xx behavioral)
- Extend Diagnostic struct with error code, expected/actual types, and
secondary spans
- Add format_type_diff() for visual type comparison in error messages
- Add help URLs linking to lux-lang.dev/errors/{code}
- Update typechecker, parser, and interpreter to use error codes
- Categorize errors with specific codes and helpful hints
Error messages now show:
- Error code in header: -- ERROR[E0301] ──
- Clear error category title
- Visual type diff for type mismatches
- Context-aware hints
- "Learn more" URL for documentation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,223 @@
|
||||
//! 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";
|
||||
@@ -46,30 +260,57 @@ impl Severity {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Diagnostic {
|
||||
pub severity: Severity,
|
||||
pub code: Option<ErrorCode>,
|
||||
pub title: String,
|
||||
pub message: String,
|
||||
pub span: Span,
|
||||
pub hints: Vec<String>,
|
||||
pub expected_type: Option<String>,
|
||||
pub actual_type: Option<String>,
|
||||
pub secondary_spans: Vec<(Span, String)>,
|
||||
}
|
||||
|
||||
impl Diagnostic {
|
||||
pub fn error(title: impl Into<String>, message: impl Into<String>, 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<String>, message: impl Into<String>, 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<String>, 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +323,111 @@ impl Diagnostic {
|
||||
self.hints.extend(hints);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add expected and actual types for type mismatch visualization
|
||||
pub fn with_types(mut self, expected: impl Into<String>, actual: impl Into<String>) -> 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<String>) -> 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<String> {
|
||||
// Simple case: find the first difference
|
||||
let exp_chars: Vec<char> = expected.chars().collect();
|
||||
let act_chars: Vec<char> = 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
|
||||
@@ -211,18 +557,23 @@ pub fn render_diagnostic(
|
||||
let severity_color = diagnostic.severity.color();
|
||||
let severity_label = diagnostic.severity.label();
|
||||
|
||||
// Header: -- ERROR ----------- filename.lux
|
||||
// Header with error code: ── ERROR[E0201] ── filename.lux
|
||||
let filename_str = filename.unwrap_or("<input>");
|
||||
let code_str = diagnostic
|
||||
.code
|
||||
.map(|c| format!("[{}]", c.code()))
|
||||
.unwrap_or_default();
|
||||
output.push_str(&format!(
|
||||
"{}{}{} ── {} ──────────────────────────────────\n",
|
||||
"{}{}── {}{} ──────────────────── {}{}\n",
|
||||
colors::BOLD,
|
||||
severity_color,
|
||||
severity_label,
|
||||
filename_str
|
||||
code_str,
|
||||
filename_str,
|
||||
colors::RESET
|
||||
));
|
||||
output.push_str(colors::RESET);
|
||||
|
||||
// Title
|
||||
// Location
|
||||
output.push_str(&format!(
|
||||
"\n{}{}{}:{}{}\n\n",
|
||||
colors::BOLD,
|
||||
@@ -309,6 +660,11 @@ pub fn render_diagnostic(
|
||||
// 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');
|
||||
@@ -323,6 +679,17 @@ pub fn render_diagnostic(
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -338,9 +705,14 @@ pub fn render_diagnostic_plain(
|
||||
let (end_line, end_col) = offset_to_line_col(source, diagnostic.span.end);
|
||||
|
||||
let filename_str = filename.unwrap_or("<input>");
|
||||
let code_str = diagnostic
|
||||
.code
|
||||
.map(|c| format!("[{}]", c.code()))
|
||||
.unwrap_or_default();
|
||||
output.push_str(&format!(
|
||||
"-- {} ── {} ──────────────────────────────────\n",
|
||||
"-- {}{} ── {} ──────────────────────────────────\n",
|
||||
diagnostic.severity.label(),
|
||||
code_str,
|
||||
filename_str
|
||||
));
|
||||
|
||||
@@ -381,6 +753,11 @@ pub fn render_diagnostic_plain(
|
||||
|
||||
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 {
|
||||
@@ -388,6 +765,11 @@ pub fn render_diagnostic_plain(
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -478,25 +860,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_type_error_categorization() {
|
||||
use super::Diagnostic;
|
||||
use super::{Diagnostic, ErrorCode};
|
||||
|
||||
// Test that errors are properly categorized
|
||||
let source = "let x: Int = \"hello\"";
|
||||
|
||||
// Simulate a type mismatch diagnostic
|
||||
let diag = Diagnostic {
|
||||
severity: Severity::Error,
|
||||
title: "Type Mismatch".to_string(),
|
||||
message: "Type mismatch: expected Int, got String".to_string(),
|
||||
span: Span { start: 13, end: 20 },
|
||||
hints: vec!["Check that the types on both sides of the expression are compatible.".to_string()],
|
||||
};
|
||||
// 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]
|
||||
@@ -589,4 +974,60 @@ mod tests {
|
||||
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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user