use super::ErrorCode;
#[derive(Debug, Clone)]
pub struct CompilerDiagnostic {
pub code: ErrorCode,
pub severity: DiagnosticSeverity,
pub message: String,
pub span: SourceSpan,
pub labels: Vec<DiagnosticLabel>,
pub suggestions: Vec<CompilerSuggestion>,
pub expected: Option<TypeInfo>,
pub found: Option<TypeInfo>,
pub notes: Vec<String>,
}
impl CompilerDiagnostic {
#[must_use]
pub fn new(
code: ErrorCode,
severity: DiagnosticSeverity,
message: &str,
span: SourceSpan,
) -> Self {
Self {
code,
severity,
message: message.to_string(),
span,
labels: Vec::new(),
suggestions: Vec::new(),
expected: None,
found: None,
notes: Vec::new(),
}
}
#[must_use]
pub fn with_label(mut self, label: DiagnosticLabel) -> Self {
self.labels.push(label);
self
}
#[must_use]
pub fn with_suggestion(mut self, suggestion: CompilerSuggestion) -> Self {
self.suggestions.push(suggestion);
self
}
#[must_use]
pub fn with_expected(mut self, expected: TypeInfo) -> Self {
self.expected = Some(expected);
self
}
#[must_use]
pub fn with_found(mut self, found: TypeInfo) -> Self {
self.found = Some(found);
self
}
#[must_use]
pub fn with_note(mut self, note: &str) -> Self {
self.notes.push(note.to_string());
self
}
#[must_use]
pub fn is_type_error(&self) -> bool {
self.expected.is_some() && self.found.is_some()
}
#[must_use]
pub fn has_suggestions(&self) -> bool {
!self.suggestions.is_empty()
}
#[must_use]
pub fn machine_applicable_suggestions(&self) -> Vec<&CompilerSuggestion> {
self.suggestions
.iter()
.filter(|s| s.applicability == SuggestionApplicability::MachineApplicable)
.collect()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DiagnosticSeverity {
Error,
Warning,
Note,
Help,
}
impl DiagnosticSeverity {
#[must_use]
pub fn is_fatal(&self) -> bool {
matches!(self, DiagnosticSeverity::Error)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceSpan {
pub file: String,
pub line_start: usize,
pub line_end: usize,
pub column_start: usize,
pub column_end: usize,
pub byte_start: usize,
pub byte_end: usize,
}
impl SourceSpan {
#[must_use]
pub fn new(
file: &str,
line_start: usize,
line_end: usize,
column_start: usize,
column_end: usize,
) -> Self {
Self {
file: file.to_string(),
line_start,
line_end,
column_start,
column_end,
byte_start: 0,
byte_end: 0,
}
}
#[must_use]
pub fn single_line(file: &str, line: usize, column_start: usize, column_end: usize) -> Self {
Self::new(file, line, line, column_start, column_end)
}
#[must_use]
pub fn point(file: &str, line: usize, column: usize) -> Self {
Self::new(file, line, line, column, column + 1)
}
#[must_use]
pub fn is_single_line(&self) -> bool {
self.line_start == self.line_end
}
#[must_use]
pub fn len(&self) -> usize {
if self.byte_end > self.byte_start {
self.byte_end - self.byte_start
} else if self.is_single_line() {
self.column_end.saturating_sub(self.column_start)
} else {
(self.line_end - self.line_start + 1) * 40
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[must_use]
pub fn overlaps(&self, other: &SourceSpan) -> bool {
self.file == other.file
&& !(self.line_end < other.line_start || other.line_end < self.line_start)
}
}
impl Default for SourceSpan {
fn default() -> Self {
Self {
file: String::new(),
line_start: 1,
line_end: 1,
column_start: 1,
column_end: 1,
byte_start: 0,
byte_end: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct DiagnosticLabel {
pub message: String,
pub span: SourceSpan,
pub primary: bool,
}
impl DiagnosticLabel {
#[must_use]
pub fn primary(message: &str, span: SourceSpan) -> Self {
Self {
message: message.to_string(),
span,
primary: true,
}
}
#[must_use]
pub fn secondary(message: &str, span: SourceSpan) -> Self {
Self {
message: message.to_string(),
span,
primary: false,
}
}
}
#[derive(Debug, Clone)]
pub struct CompilerSuggestion {
pub message: String,
pub applicability: SuggestionApplicability,
pub replacement: CodeReplacement,
}
impl CompilerSuggestion {
#[must_use]
pub fn new(
message: &str,
applicability: SuggestionApplicability,
replacement: CodeReplacement,
) -> Self {
Self {
message: message.to_string(),
applicability,
replacement,
}
}
#[must_use]
pub fn is_auto_applicable(&self) -> bool {
self.applicability == SuggestionApplicability::MachineApplicable
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SuggestionApplicability {
MachineApplicable,
MaybeIncorrect,
HasPlaceholders,
Unspecified,
}
#[derive(Debug, Clone)]
pub struct CodeReplacement {
pub span: SourceSpan,
pub replacement: String,
}
impl CodeReplacement {
#[must_use]
pub fn new(span: SourceSpan, replacement: &str) -> Self {
Self {
span,
replacement: replacement.to_string(),
}
}
#[must_use]
pub fn apply(&self, source: &str) -> String {
if self.span.byte_start > 0 && self.span.byte_end > 0 {
let mut result = source[..self.span.byte_start].to_string();
result.push_str(&self.replacement);
result.push_str(&source[self.span.byte_end..]);
result
} else {
let lines: Vec<&str> = source.lines().collect();
let mut result = String::new();
for (i, line) in lines.iter().enumerate() {
let line_num = i + 1;
if line_num < self.span.line_start || line_num > self.span.line_end {
result.push_str(line);
result.push('\n');
} else if line_num == self.span.line_start && line_num == self.span.line_end {
let before = &line[..self.span.column_start.saturating_sub(1).min(line.len())];
let after = &line[self.span.column_end.saturating_sub(1).min(line.len())..];
result.push_str(before);
result.push_str(&self.replacement);
result.push_str(after);
result.push('\n');
}
}
if !source.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
result
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TypeInfo {
pub full: String,
pub base: String,
pub generics: Vec<String>,
pub is_reference: bool,
pub is_mutable: bool,
pub lifetime: Option<String>,
}
impl TypeInfo {
#[must_use]
pub fn new(type_str: &str) -> Self {
let trimmed = type_str.trim();
let (is_reference, is_mutable, rest) = if let Some(stripped) = trimmed.strip_prefix("&mut ")
{
(true, true, stripped)
} else if let Some(stripped) = trimmed.strip_prefix('&') {
(true, false, stripped)
} else {
(false, false, trimmed)
};
let (lifetime, rest) = if rest.starts_with('\'') {
let end = rest.find(' ').unwrap_or(rest.len());
(Some(rest[..end].to_string()), rest[end..].trim())
} else {
(None, rest)
};
let (base, generics) = if let Some(start) = rest.find('<') {
let base = rest[..start].to_string();
let generics_str = &rest[start + 1..rest.len() - 1];
let generics: Vec<String> = generics_str
.split(',')
.map(|s| s.trim().to_string())
.collect();
(base, generics)
} else {
(rest.to_string(), Vec::new())
};
Self {
full: type_str.to_string(),
base,
generics,
is_reference,
is_mutable,
lifetime,
}
}
#[must_use]
pub fn is_compatible_with(&self, other: &TypeInfo) -> bool {
self.base == other.base && self.generics == other.generics
}
#[must_use]
pub fn suggest_conversion_to(&self, target: &TypeInfo) -> Option<String> {
if self.base == "String" && target.base == "str" && target.is_reference {
return Some(".as_str()".to_string());
}
if self.base == "str" && self.is_reference && target.base == "String" {
return Some(".to_string()".to_string());
}
if self.base == "Vec" && target.is_reference {
return Some(".as_slice()".to_string());
}
if self.is_reference && target.base == "Vec" {
return Some(".to_vec()".to_string());
}
if !self.is_reference && target.is_reference && !target.is_mutable {
return Some("&".to_string());
}
if self.is_reference && !target.is_reference {
return Some(".clone()".to_string());
}
None
}
}
impl std::fmt::Display for TypeInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.full)
}
}
#[cfg(test)]
#[path = "diagnostic_tests.rs"]
mod tests;