use std::collections::HashMap;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Severity {
Remark,
Warning,
Error,
Fatal,
}
#[derive(Debug, Clone)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub file: Option<String>,
pub line: Option<u32>,
pub column: Option<u32>,
}
impl Diagnostic {
pub fn error(msg: impl Into<String>) -> Self {
Self {
severity: Severity::Error,
message: msg.into(),
file: None,
line: None,
column: None,
}
}
pub fn warning(msg: impl Into<String>) -> Self {
Self {
severity: Severity::Warning,
message: msg.into(),
file: None,
line: None,
column: None,
}
}
pub fn remark(msg: impl Into<String>) -> Self {
Self {
severity: Severity::Remark,
message: msg.into(),
file: None,
line: None,
column: None,
}
}
pub fn with_location(mut self, file: &str, line: u32, col: u32) -> Self {
self.file = Some(file.into());
self.line = Some(line);
self.column = Some(col);
self
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sev = match self.severity {
Severity::Remark => "remark",
Severity::Warning => "warning",
Severity::Error => "error",
Severity::Fatal => "fatal error",
};
if let (Some(file), Some(line), Some(col)) = (&self.file, self.line, self.column) {
write!(f, "{}:{}:{}: {}: {}", file, line, col, sev, self.message)
} else {
write!(f, "{}: {}", sev, self.message)
}
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticManager {
pub diagnostics: Vec<Diagnostic>,
pub error_count: usize,
pub warning_count: usize,
}
impl DiagnosticManager {
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
error_count: 0,
warning_count: 0,
}
}
pub fn emit(&mut self, diag: Diagnostic) {
match diag.severity {
Severity::Error | Severity::Fatal => self.error_count += 1,
Severity::Warning => self.warning_count += 1,
_ => {}
}
self.diagnostics.push(diag);
}
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
pub fn has_warnings(&self) -> bool {
self.warning_count > 0
}
pub fn print_all(&self) {
for diag in &self.diagnostics {
eprintln!("{}", diag);
}
}
}
impl Default for DiagnosticManager {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiagLevel {
Note,
Warning,
Error,
Fatal,
}
impl DiagLevel {
pub fn as_str(&self) -> &'static str {
match self {
DiagLevel::Note => "note",
DiagLevel::Warning => "warning",
DiagLevel::Error => "error",
DiagLevel::Fatal => "fatal error",
}
}
pub fn is_error(&self) -> bool {
matches!(self, DiagLevel::Error | DiagLevel::Fatal)
}
pub fn is_fatal(&self) -> bool {
matches!(self, DiagLevel::Fatal)
}
}
impl fmt::Display for DiagLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceLocation {
pub filename: String,
pub line: u32,
pub column: u32,
pub line_text: Option<String>,
}
impl SourceLocation {
pub fn new(filename: &str, line: u32, column: u32) -> Self {
Self {
filename: filename.to_string(),
line,
column,
line_text: None,
}
}
pub fn with_line_text(mut self, text: &str) -> Self {
self.line_text = Some(text.to_string());
self
}
pub fn format_short(&self) -> String {
format!("{}:{}:{}", self.filename, self.line, self.column)
}
}
impl fmt::Display for SourceLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}:{}", self.filename, self.line, self.column)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceRange {
pub start: SourceLocation,
pub end: SourceLocation,
}
impl SourceRange {
pub fn new(start: SourceLocation, end: SourceLocation) -> Self {
Self { start, end }
}
pub fn point(loc: SourceLocation) -> Self {
Self {
start: loc.clone(),
end: loc,
}
}
pub fn is_multiline(&self) -> bool {
self.start.filename != self.end.filename || self.start.line != self.end.line
}
pub fn format_short(&self) -> String {
if self.start.filename == self.end.filename {
if self.start.line == self.end.line {
format!(
"{}:{}:{}-{}",
self.start.filename, self.start.line, self.start.column, self.end.column
)
} else {
format!(
"{}:{}:{}-{}:{}",
self.start.filename,
self.start.line,
self.start.column,
self.end.line,
self.end.column
)
}
} else {
format!("{}-{}", self.start.format_short(), self.end.format_short())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FixItHint {
pub range: SourceRange,
pub replacement: String,
}
impl FixItHint {
pub fn new(range: SourceRange, replacement: &str) -> Self {
Self {
range,
replacement: replacement.to_string(),
}
}
pub fn insertion(loc: &SourceLocation, text: &str) -> Self {
Self {
range: SourceRange::point(loc.clone()),
replacement: text.to_string(),
}
}
pub fn removal(range: SourceRange) -> Self {
Self {
range,
replacement: String::new(),
}
}
pub fn replacement(range: SourceRange, text: &str) -> Self {
Self {
range,
replacement: text.to_string(),
}
}
pub fn format(&self) -> String {
format!("{}: \"{}\"", self.range.format_short(), self.replacement)
}
}
#[derive(Debug, Clone)]
pub struct RichDiagnostic {
pub level: DiagLevel,
pub message: String,
pub location: Option<SourceLocation>,
pub ranges: Vec<SourceRange>,
pub notes: Vec<DiagnosticNote>,
pub fixit_hints: Vec<FixItHint>,
pub error_code: Option<String>,
}
#[derive(Debug, Clone)]
pub struct DiagnosticNote {
pub message: String,
pub location: Option<SourceLocation>,
}
impl DiagnosticNote {
pub fn new(message: &str) -> Self {
Self {
message: message.to_string(),
location: None,
}
}
pub fn at(message: &str, location: SourceLocation) -> Self {
Self {
message: message.to_string(),
location: Some(location),
}
}
}
impl RichDiagnostic {
pub fn new(level: DiagLevel, message: &str) -> Self {
Self {
level,
message: message.to_string(),
location: None,
ranges: Vec::new(),
notes: Vec::new(),
fixit_hints: Vec::new(),
error_code: None,
}
}
pub fn at(mut self, location: SourceLocation) -> Self {
self.location = Some(location);
self
}
pub fn add_range(mut self, range: SourceRange) -> Self {
self.ranges.push(range);
self
}
pub fn add_note(mut self, note: DiagnosticNote) -> Self {
self.notes.push(note);
self
}
pub fn add_fixit(mut self, hint: FixItHint) -> Self {
self.fixit_hints.push(hint);
self
}
pub fn with_code(mut self, code: &str) -> Self {
self.error_code = Some(code.to_string());
self
}
}
impl fmt::Display for RichDiagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref loc) = self.location {
write!(f, "{}: {}: ", loc, self.level)?;
} else {
write!(f, "{}: ", self.level)?;
}
if let Some(ref code) = self.error_code {
write!(f, "[{}] ", code)?;
}
writeln!(f, "{}", self.message)?;
if let Some(ref loc) = self.location {
if let Some(ref line_text) = loc.line_text {
writeln!(f, " {} | {}", loc.line, line_text)?;
write!(f, " {} | ", " ".repeat(loc.line.to_string().len()))?;
for _ in 1..loc.column {
write!(f, " ")?;
}
if let Some(first_range) = self.ranges.first() {
let len = if first_range.start.line == first_range.end.line {
(first_range
.end
.column
.saturating_sub(first_range.start.column)
+ 1) as usize
} else {
1
};
for _ in 0..len {
write!(f, "~")?;
}
} else {
write!(f, "^")?;
}
writeln!(f)?;
}
}
for note in &self.notes {
if let Some(ref loc) = note.location {
writeln!(f, "{}: note: {}", loc, note.message)?;
} else {
writeln!(f, "note: {}", note.message)?;
}
}
for fixit in &self.fixit_hints {
writeln!(f, " fixit: {}", fixit.format())?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticEngine {
pub diagnostics: Vec<RichDiagnostic>,
pub error_count: usize,
pub warning_count: usize,
pub max_errors: usize,
pub warnings_as_errors: bool,
pub ignore_warnings: bool,
building: Option<RichDiagnostic>,
}
impl DiagnosticEngine {
pub fn new() -> Self {
Self {
diagnostics: Vec::new(),
error_count: 0,
warning_count: 0,
max_errors: 20,
warnings_as_errors: false,
ignore_warnings: false,
building: None,
}
}
pub fn error(&mut self, msg: &str) -> &mut Self {
self.finalize_building();
self.building = Some(RichDiagnostic::new(DiagLevel::Error, msg));
self
}
pub fn warning(&mut self, msg: &str) -> &mut Self {
self.finalize_building();
self.building = Some(RichDiagnostic::new(DiagLevel::Warning, msg));
self
}
pub fn note(&mut self, msg: &str) -> &mut Self {
self.finalize_building();
self.building = Some(RichDiagnostic::new(DiagLevel::Note, msg));
self
}
pub fn at(&mut self, loc: SourceLocation) -> &mut Self {
if let Some(ref mut diag) = self.building {
diag.location = Some(loc);
}
self
}
pub fn add_range(&mut self, range: SourceRange) -> &mut Self {
if let Some(ref mut diag) = self.building {
diag.ranges.push(range);
}
self
}
pub fn add_note(&mut self, note: &str) -> &mut Self {
if let Some(ref mut diag) = self.building {
diag.notes.push(DiagnosticNote::new(note));
}
self
}
pub fn add_fixit(&mut self, hint: FixItHint) -> &mut Self {
if let Some(ref mut diag) = self.building {
diag.fixit_hints.push(hint);
}
self
}
pub fn with_code(&mut self, code: &str) -> &mut Self {
if let Some(ref mut diag) = self.building {
diag.error_code = Some(code.to_string());
}
self
}
pub fn emit(&mut self) -> &mut Self {
self.finalize_building();
self
}
pub fn emit_diagnostic(&mut self, diag: RichDiagnostic) {
self.record(&diag);
self.diagnostics.push(diag);
}
pub fn has_errors(&self) -> bool {
self.error_count > 0
}
pub fn has_warnings(&self) -> bool {
self.warning_count > 0
}
pub fn error_limit_reached(&self) -> bool {
self.max_errors > 0 && self.error_count >= self.max_errors
}
pub fn clear(&mut self) {
self.diagnostics.clear();
self.error_count = 0;
self.warning_count = 0;
self.building = None;
}
pub fn print_all(&self) {
for diag in &self.diagnostics {
self.print_diagnostic(diag);
}
}
pub fn print_diagnostic(&self, diag: &RichDiagnostic) {
eprintln!("{}", diag);
}
pub fn format_all(&self) -> String {
let mut out = String::new();
for diag in &self.diagnostics {
out.push_str(&format!("{}\n", diag));
}
out
}
pub fn iter(&self) -> impl Iterator<Item = &RichDiagnostic> {
self.diagnostics.iter()
}
pub fn len(&self) -> usize {
self.diagnostics.len()
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
fn finalize_building(&mut self) {
if let Some(diag) = self.building.take() {
self.record(&diag);
self.diagnostics.push(diag);
}
}
fn record(&mut self, diag: &RichDiagnostic) {
match diag.level {
DiagLevel::Error | DiagLevel::Fatal => {
self.error_count += 1;
}
DiagLevel::Warning => {
if self.warnings_as_errors {
self.error_count += 1;
} else if !self.ignore_warnings {
self.warning_count += 1;
}
}
DiagLevel::Note => {}
}
}
}
impl Default for DiagnosticEngine {
fn default() -> Self {
Self::new()
}
}
impl Drop for DiagnosticEngine {
fn drop(&mut self) {
self.finalize_building();
}
}
#[derive(Debug, Clone)]
pub struct SourceManager {
pub files: HashMap<String, SourceFile>,
}
#[derive(Debug, Clone)]
pub struct SourceFile {
pub filename: String,
pub content: String,
pub lines: Vec<String>,
}
impl SourceFile {
pub fn new(filename: &str, content: &str) -> Self {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
Self {
filename: filename.to_string(),
content: content.to_string(),
lines,
}
}
pub fn get_line(&self, line: u32) -> Option<&str> {
if line == 0 || line as usize > self.lines.len() {
return None;
}
Some(&self.lines[line as usize - 1])
}
pub fn get_char(&self, offset: usize) -> Option<char> {
self.content[offset..].chars().next()
}
pub fn offset_to_location(&self, offset: usize) -> Option<(u32, u32)> {
if offset > self.content.len() {
return None;
}
let prefix = &self.content[..offset];
let line = prefix.chars().filter(|&c| c == '\n').count() as u32 + 1;
let last_newline = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = (offset - last_newline) + 1;
Some((line, col as u32))
}
pub fn location_to_offset(&self, line: u32, column: u32) -> Option<usize> {
if line == 0 || column == 0 {
return None;
}
let mut current_line = 1u32;
let mut offset = 0usize;
for ch in self.content.chars() {
if current_line == line {
let col_on_line = (offset
- self.content[..offset]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0))
+ 1;
if col_on_line as u32 == column {
return Some(offset);
}
}
if ch == '\n' {
current_line += 1;
if current_line > line {
break;
}
}
offset += ch.len_utf8();
}
None
}
pub fn get_location(&self, offset: usize) -> Option<SourceLocation> {
let (line, col) = self.offset_to_location(offset)?;
let line_text = self.get_line(line).map(|s| s.to_string());
Some(SourceLocation {
filename: self.filename.clone(),
line,
column: col,
line_text,
})
}
}
impl SourceManager {
pub fn new() -> Self {
Self {
files: HashMap::new(),
}
}
pub fn add_file(&mut self, filename: &str, content: &str) {
self.files
.insert(filename.to_string(), SourceFile::new(filename, content));
}
pub fn get_file(&self, filename: &str) -> Option<&SourceFile> {
self.files.get(filename)
}
pub fn get_location(&self, filename: &str, offset: usize) -> Option<SourceLocation> {
self.files.get(filename)?.get_location(offset)
}
pub fn get_line(&self, filename: &str, line: u32) -> Option<&str> {
self.files.get(filename)?.get_line(line)
}
pub fn get_location_at(&self, filename: &str, line: u32, column: u32) -> SourceLocation {
let line_text = self.get_line(filename, line).map(|s| s.to_string());
SourceLocation {
filename: filename.to_string(),
line,
column,
line_text,
}
}
pub fn has_file(&self, filename: &str) -> bool {
self.files.contains_key(filename)
}
pub fn remove_file(&mut self, filename: &str) {
self.files.remove(filename);
}
pub fn iter_files(&self) -> impl Iterator<Item = &SourceFile> {
self.files.values()
}
pub fn file_count(&self) -> usize {
self.files.len()
}
}
impl Default for SourceManager {
fn default() -> Self {
Self::new()
}
}
pub fn emit_error(engine: &mut DiagnosticEngine, msg: &str) {
engine.error(msg);
}
pub fn emit_warning(engine: &mut DiagnosticEngine, msg: &str) {
engine.warning(msg);
}
pub fn emit_note(engine: &mut DiagnosticEngine, msg: &str) {
engine.note(msg);
}
pub fn emit_error_at(
engine: &mut DiagnosticEngine,
msg: &str,
filename: &str,
line: u32,
column: u32,
) {
let loc = SourceLocation::new(filename, line, column);
engine.error(msg).at(loc);
}
pub fn emit_warning_at(
engine: &mut DiagnosticEngine,
msg: &str,
filename: &str,
line: u32,
column: u32,
) {
let loc = SourceLocation::new(filename, line, column);
engine.warning(msg).at(loc);
}
pub fn convert_diagnostic(diag: &Diagnostic) -> RichDiagnostic {
let level = match diag.severity {
Severity::Remark => DiagLevel::Note,
Severity::Warning => DiagLevel::Warning,
Severity::Error => DiagLevel::Error,
Severity::Fatal => DiagLevel::Fatal,
};
let location = match (&diag.file, diag.line, diag.column) {
(Some(file), Some(line), Some(col)) => Some(SourceLocation::new(file, line, col)),
_ => None,
};
RichDiagnostic {
level,
message: diag.message.clone(),
location,
ranges: Vec::new(),
notes: Vec::new(),
fixit_hints: Vec::new(),
error_code: None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_create() {
let d = Diagnostic::error("something went wrong");
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.message, "something went wrong");
}
#[test]
fn test_diagnostic_with_location() {
let d = Diagnostic::warning("unused variable").with_location("test.ll", 10, 5);
assert_eq!(d.file, Some("test.ll".into()));
assert_eq!(d.line, Some(10));
assert_eq!(d.column, Some(5));
}
#[test]
fn test_diagnostic_manager_emit() {
let mut dm = DiagnosticManager::new();
dm.emit(Diagnostic::error("e1"));
dm.emit(Diagnostic::warning("w1"));
dm.emit(Diagnostic::remark("r1"));
assert_eq!(dm.error_count, 1);
assert_eq!(dm.warning_count, 1);
assert!(dm.has_errors());
assert!(dm.has_warnings());
assert_eq!(dm.diagnostics.len(), 3);
}
#[test]
fn test_diagnostic_manager_no_errors() {
let mut dm = DiagnosticManager::new();
dm.emit(Diagnostic::remark("just a note"));
assert!(!dm.has_errors());
assert!(!dm.has_warnings());
}
#[test]
fn test_diaglevel_as_str() {
assert_eq!(DiagLevel::Note.as_str(), "note");
assert_eq!(DiagLevel::Warning.as_str(), "warning");
assert_eq!(DiagLevel::Error.as_str(), "error");
assert_eq!(DiagLevel::Fatal.as_str(), "fatal error");
}
#[test]
fn test_diaglevel_is_error() {
assert!(!DiagLevel::Note.is_error());
assert!(!DiagLevel::Warning.is_error());
assert!(DiagLevel::Error.is_error());
assert!(DiagLevel::Fatal.is_error());
}
#[test]
fn test_source_location_new() {
let loc = SourceLocation::new("test.ll", 5, 12);
assert_eq!(loc.filename, "test.ll");
assert_eq!(loc.line, 5);
assert_eq!(loc.column, 12);
assert!(loc.line_text.is_none());
}
#[test]
fn test_source_location_with_line_text() {
let loc = SourceLocation::new("test.ll", 3, 1).with_line_text(" ret void");
assert_eq!(loc.line_text, Some(" ret void".to_string()));
}
#[test]
fn test_source_location_format() {
let loc = SourceLocation::new("file.ll", 10, 5);
assert_eq!(loc.format_short(), "file.ll:10:5");
}
#[test]
fn test_source_range_point() {
let loc = SourceLocation::new("f.ll", 1, 1);
let range = SourceRange::point(loc.clone());
assert_eq!(range.start, range.end);
assert!(!range.is_multiline());
}
#[test]
fn test_source_range_multiline() {
let start = SourceLocation::new("f.ll", 1, 1);
let end = SourceLocation::new("f.ll", 3, 5);
let range = SourceRange::new(start, end);
assert!(range.is_multiline());
}
#[test]
fn test_fixit_insertion() {
let loc = SourceLocation::new("test.ll", 5, 1);
let fixit = FixItHint::insertion(&loc, " ");
assert_eq!(fixit.replacement, " ");
assert_eq!(fixit.range.start, loc);
assert_eq!(fixit.range.end, loc);
}
#[test]
fn test_fixit_removal() {
let start = SourceLocation::new("test.ll", 1, 1);
let end = SourceLocation::new("test.ll", 1, 10);
let range = SourceRange::new(start, end);
let fixit = FixItHint::removal(range);
assert_eq!(fixit.replacement, "");
}
#[test]
fn test_fixit_format() {
let start = SourceLocation::new("f.ll", 2, 3);
let end = SourceLocation::new("f.ll", 2, 6);
let hint = FixItHint::replacement(SourceRange::new(start, end), "foo");
let formatted = hint.format();
assert!(formatted.contains("f.ll"));
assert!(formatted.contains("foo"));
}
#[test]
fn test_rich_diagnostic_new() {
let diag = RichDiagnostic::new(DiagLevel::Error, "type mismatch");
assert_eq!(diag.level, DiagLevel::Error);
assert_eq!(diag.message, "type mismatch");
assert!(diag.location.is_none());
assert!(diag.ranges.is_empty());
assert!(diag.notes.is_empty());
assert!(diag.fixit_hints.is_empty());
}
#[test]
fn test_rich_diagnostic_builder() {
let loc = SourceLocation::new("test.ll", 10, 5);
let diag = RichDiagnostic::new(DiagLevel::Warning, "unused variable")
.at(loc.clone())
.add_note(DiagnosticNote::new("declared here"))
.with_code("Wunused");
assert_eq!(diag.location, Some(loc));
assert_eq!(diag.error_code, Some("Wunused".to_string()));
assert_eq!(diag.notes.len(), 1);
}
#[test]
fn test_engine_create() {
let engine = DiagnosticEngine::new();
assert!(!engine.has_errors());
assert!(!engine.has_warnings());
assert_eq!(engine.len(), 0);
assert!(engine.is_empty());
}
#[test]
fn test_engine_emit_error() {
let mut engine = DiagnosticEngine::new();
engine
.error("test error")
.at(SourceLocation::new("test.ll", 1, 1));
assert!(engine.has_errors());
assert!(!engine.has_warnings());
assert_eq!(engine.error_count, 1);
assert_eq!(engine.len(), 1);
}
#[test]
fn test_engine_emit_warning() {
let mut engine = DiagnosticEngine::new();
engine
.warning("test warning")
.at(SourceLocation::new("test.ll", 2, 1));
assert!(!engine.has_errors());
assert!(engine.has_warnings());
assert_eq!(engine.warning_count, 1);
}
#[test]
fn test_engine_warnings_as_errors() {
let mut engine = DiagnosticEngine::new();
engine.warnings_as_errors = true;
engine
.warning("treated as error")
.at(SourceLocation::new("test.ll", 1, 1));
assert!(engine.has_errors());
assert_eq!(engine.error_count, 1);
assert_eq!(engine.warning_count, 0);
}
#[test]
fn test_engine_ignore_warnings() {
let mut engine = DiagnosticEngine::new();
engine.ignore_warnings = true;
engine
.warning("ignored")
.at(SourceLocation::new("test.ll", 1, 1));
assert!(!engine.has_warnings());
assert_eq!(engine.warning_count, 0);
assert_eq!(engine.len(), 1); }
#[test]
fn test_engine_multiple_emits() {
let mut engine = DiagnosticEngine::new();
engine.error("e1").at(SourceLocation::new("test.ll", 1, 1));
assert_eq!(engine.error_count, 1);
engine.error("e2").at(SourceLocation::new("test.ll", 2, 1));
assert_eq!(engine.error_count, 2);
assert_eq!(engine.len(), 2);
}
#[test]
fn test_engine_clear() {
let mut engine = DiagnosticEngine::new();
engine
.error("test")
.at(SourceLocation::new("test.ll", 1, 1));
engine.clear();
assert!(!engine.has_errors());
assert_eq!(engine.len(), 0);
}
#[test]
fn test_engine_builder_chaining() {
let mut engine = DiagnosticEngine::new();
let loc = SourceLocation::new("test.ll", 3, 10);
let range = SourceRange::point(loc.clone());
engine
.error("syntax error")
.at(loc.clone())
.add_range(range)
.add_note("expected ';'")
.with_code("Esyntax");
assert_eq!(engine.error_count, 1);
assert_eq!(engine.len(), 1);
let diag = &engine.diagnostics[0];
assert_eq!(diag.ranges.len(), 1);
assert_eq!(diag.notes.len(), 1);
assert_eq!(diag.error_code, Some("Esyntax".to_string()));
}
#[test]
fn test_engine_format_all() {
let mut engine = DiagnosticEngine::new();
engine
.error("bad thing")
.at(SourceLocation::new("test.ll", 1, 1));
let text = engine.format_all();
assert!(text.contains("error"));
assert!(text.contains("bad thing"));
assert!(text.contains("test.ll"));
}
#[test]
fn test_source_file_new() {
let sf = SourceFile::new("test.ll", "line1\nline2\nline3");
assert_eq!(sf.filename, "test.ll");
assert_eq!(sf.lines.len(), 3);
assert_eq!(sf.get_line(1), Some("line1"));
assert_eq!(sf.get_line(2), Some("line2"));
assert_eq!(sf.get_line(3), Some("line3"));
assert_eq!(sf.get_line(4), None);
}
#[test]
fn test_source_file_offset_to_location() {
let sf = SourceFile::new("test.ll", "abc\ndef\nghi");
assert_eq!(sf.offset_to_location(0), Some((1, 1)));
assert_eq!(sf.offset_to_location(4), Some((2, 1)));
assert_eq!(sf.offset_to_location(8), Some((3, 1)));
assert_eq!(sf.offset_to_location(100), None);
}
#[test]
fn test_source_file_get_location() {
let sf = SourceFile::new("test.ll", "first line\nsecond line");
let loc = sf.get_location(6).unwrap(); assert_eq!(loc.filename, "test.ll");
assert_eq!(loc.line, 1);
assert_eq!(loc.column, 7); }
#[test]
fn test_source_manager_add_file() {
let mut sm = SourceManager::new();
sm.add_file("test.ll", "define void @main() {\n ret void\n}");
assert!(sm.has_file("test.ll"));
assert_eq!(sm.file_count(), 1);
}
#[test]
fn test_source_manager_get_line() {
let mut sm = SourceManager::new();
sm.add_file("test.ll", "line1\nline2\nline3");
assert_eq!(sm.get_line("test.ll", 1), Some("line1"));
assert_eq!(sm.get_line("test.ll", 3), Some("line3"));
assert_eq!(sm.get_line("test.ll", 99), None);
assert_eq!(sm.get_line("nonexistent.ll", 1), None);
}
#[test]
fn test_source_manager_get_location() {
let mut sm = SourceManager::new();
sm.add_file("test.ll", "ab\ncd");
let loc = sm.get_location("test.ll", 3).unwrap(); assert_eq!(loc.filename, "test.ll");
assert_eq!(loc.line, 2);
assert_eq!(loc.column, 1);
}
#[test]
fn test_source_manager_remove_file() {
let mut sm = SourceManager::new();
sm.add_file("test.ll", "content");
assert!(sm.has_file("test.ll"));
sm.remove_file("test.ll");
assert!(!sm.has_file("test.ll"));
}
#[test]
fn test_emit_error_fn() {
let mut engine = DiagnosticEngine::new();
emit_error(&mut engine, "test error");
assert!(engine.has_errors());
}
#[test]
fn test_emit_warning_fn() {
let mut engine = DiagnosticEngine::new();
emit_warning(&mut engine, "test warning");
assert!(engine.has_warnings());
}
#[test]
fn test_emit_error_at_fn() {
let mut engine = DiagnosticEngine::new();
emit_error_at(&mut engine, "error", "file.ll", 5, 10);
assert!(engine.has_errors());
assert_eq!(engine.diagnostics[0].location.as_ref().unwrap().line, 5);
}
#[test]
fn test_convert_diagnostic() {
let old = Diagnostic::error("old error").with_location("file.ll", 3, 8);
let rich = convert_diagnostic(&old);
assert_eq!(rich.level, DiagLevel::Error);
assert_eq!(rich.message, "old error");
assert_eq!(rich.location.unwrap().line, 3);
}
}