use crate::Fix;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceSpan {
pub file: PathBuf,
pub start_line: u32,
pub start_col: u32,
pub end_line: u32,
pub end_col: u32,
}
impl SourceSpan {
pub fn new(
file: impl Into<PathBuf>,
start_line: u32,
start_col: u32,
end_line: u32,
end_col: u32,
) -> Self {
Self {
file: file.into(),
start_line,
start_col,
end_line,
end_col,
}
}
pub fn from_point(file: impl Into<PathBuf>, line: u32, col: u32) -> Self {
Self {
file: file.into(),
start_line: line,
start_col: col,
end_line: line,
end_col: col.saturating_add(1),
}
}
pub fn contains(&self, line: u32, col: u32) -> bool {
if line < self.start_line || line > self.end_line {
return false;
}
if line == self.start_line && col < self.start_col {
return false;
}
if line == self.end_line && col > self.end_col {
return false;
}
true
}
pub fn overlaps(&self, other: &SourceSpan) -> bool {
if self.file != other.file {
return false;
}
!(self.end_line < other.start_line
|| (self.end_line == other.start_line && self.end_col < other.start_col)
|| other.end_line < self.start_line
|| (other.end_line == self.start_line && other.end_col < self.start_col))
}
}
impl std::fmt::Display for SourceSpan {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}:{}",
self.file.display(),
self.start_line,
self.start_col
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
Hint,
}
impl DiagnosticSeverity {
pub fn is_error(&self) -> bool {
matches!(self, DiagnosticSeverity::Error)
}
}
impl std::fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DiagnosticSeverity::Error => write!(f, "error"),
DiagnosticSeverity::Warning => write!(f, "warning"),
DiagnosticSeverity::Info => write!(f, "info"),
DiagnosticSeverity::Hint => write!(f, "hint"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextEdit {
pub range: SourceSpan,
pub new_text: String,
}
impl TextEdit {
pub fn new(range: SourceSpan, new_text: impl Into<String>) -> Self {
Self {
range,
new_text: new_text.into(),
}
}
pub fn insert(file: impl Into<PathBuf>, line: u32, col: u32, text: impl Into<String>) -> Self {
Self {
range: SourceSpan::from_point(file, line, col),
new_text: text.into(),
}
}
pub fn delete(range: SourceSpan) -> Self {
Self {
range,
new_text: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuickFix {
pub title: String,
pub edit: Option<TextEdit>,
pub command: Option<String>,
pub is_preferred: bool,
}
impl QuickFix {
pub fn with_edit(title: impl Into<String>, edit: TextEdit) -> Self {
Self {
title: title.into(),
edit: Some(edit),
command: None,
is_preferred: false,
}
}
pub fn with_command(title: impl Into<String>, command: impl Into<String>) -> Self {
Self {
title: title.into(),
edit: None,
command: Some(command.into()),
is_preferred: false,
}
}
pub fn suggestion(title: impl Into<String>) -> Self {
Self {
title: title.into(),
edit: None,
command: None,
is_preferred: false,
}
}
pub fn preferred(mut self) -> Self {
self.is_preferred = true;
self
}
pub fn to_fix(&self) -> Fix {
if let Some(ref cmd) = self.command {
Fix::with_command(&self.title, cmd)
} else {
Fix::new(&self.title)
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GhcDiagnostic {
pub span: Option<SourceSpan>,
pub severity: DiagnosticSeverity,
pub code: Option<String>,
pub warning_flag: Option<String>,
pub message: String,
pub hints: Vec<String>,
pub fixes: Vec<QuickFix>,
}
impl GhcDiagnostic {
pub fn error(message: impl Into<String>) -> Self {
Self {
span: None,
severity: DiagnosticSeverity::Error,
code: None,
warning_flag: None,
message: message.into(),
hints: Vec::new(),
fixes: Vec::new(),
}
}
pub fn warning(message: impl Into<String>) -> Self {
Self {
span: None,
severity: DiagnosticSeverity::Warning,
code: None,
warning_flag: None,
message: message.into(),
hints: Vec::new(),
fixes: Vec::new(),
}
}
pub fn with_span(mut self, span: SourceSpan) -> Self {
self.span = Some(span);
self
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
pub fn with_warning_flag(mut self, flag: impl Into<String>) -> Self {
self.warning_flag = Some(flag.into());
self
}
pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
self.hints.push(hint.into());
self
}
pub fn with_fix(mut self, fix: QuickFix) -> Self {
self.fixes.push(fix);
self
}
pub fn is_error(&self) -> bool {
self.severity.is_error()
}
pub fn is_unused(&self) -> bool {
if let Some(ref flag) = self.warning_flag {
flag.contains("unused") || flag.contains("redundant")
} else {
self.message.contains("not used")
|| self.message.contains("redundant")
|| self.message.contains("Defined but not used")
}
}
pub fn is_deprecated(&self) -> bool {
if let Some(ref flag) = self.warning_flag {
flag.contains("deprecated")
} else {
self.message.contains("deprecated")
}
}
pub fn file(&self) -> Option<&PathBuf> {
self.span.as_ref().map(|s| &s.file)
}
pub fn format(&self) -> String {
let mut output = String::new();
if let Some(ref span) = self.span {
output.push_str(&format!("{}: ", span));
}
output.push_str(&format!("{}", self.severity));
if let Some(ref code) = self.code {
output.push_str(&format!(" [{}]", code));
}
if let Some(ref flag) = self.warning_flag {
output.push_str(&format!(" [{}]", flag));
}
output.push_str(": ");
output.push_str(&self.message);
for hint in &self.hints {
output.push_str("\n ");
output.push_str(hint);
}
output
}
}
impl std::fmt::Display for GhcDiagnostic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.format())
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiagnosticReport {
pub by_file: HashMap<PathBuf, Vec<GhcDiagnostic>>,
pub general: Vec<GhcDiagnostic>,
}
impl DiagnosticReport {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, diagnostic: GhcDiagnostic) {
if let Some(ref span) = diagnostic.span {
self.by_file
.entry(span.file.clone())
.or_default()
.push(diagnostic);
} else {
self.general.push(diagnostic);
}
}
pub fn extend(&mut self, diagnostics: impl IntoIterator<Item = GhcDiagnostic>) {
for diag in diagnostics {
self.add(diag);
}
}
pub fn merge(&mut self, other: DiagnosticReport) {
for (file, diagnostics) in other.by_file {
self.by_file.entry(file).or_default().extend(diagnostics);
}
self.general.extend(other.general);
}
pub fn for_file(&self, file: &PathBuf) -> Option<&Vec<GhcDiagnostic>> {
self.by_file.get(file)
}
pub fn iter(&self) -> impl Iterator<Item = &GhcDiagnostic> {
self.by_file.values().flatten().chain(self.general.iter())
}
pub fn error_count(&self) -> usize {
self.iter().filter(|d| d.is_error()).count()
}
pub fn warning_count(&self) -> usize {
self.iter()
.filter(|d| d.severity == DiagnosticSeverity::Warning)
.count()
}
pub fn total_count(&self) -> usize {
self.by_file.values().map(|v| v.len()).sum::<usize>() + self.general.len()
}
pub fn has_errors(&self) -> bool {
self.iter().any(|d| d.is_error())
}
pub fn is_empty(&self) -> bool {
self.by_file.is_empty() && self.general.is_empty()
}
pub fn files(&self) -> impl Iterator<Item = &PathBuf> {
self.by_file.keys()
}
pub fn clear_file(&mut self, file: &PathBuf) {
self.by_file.remove(file);
}
pub fn clear(&mut self) {
self.by_file.clear();
self.general.clear();
}
pub fn errors_as_strings(&self) -> Vec<String> {
self.iter()
.filter(|d| d.is_error())
.map(|d| d.format())
.collect()
}
pub fn warnings_as_strings(&self) -> Vec<String> {
self.iter()
.filter(|d| d.severity == DiagnosticSeverity::Warning)
.map(|d| d.format())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_span_from_point() {
let span = SourceSpan::from_point("src/Main.hs", 10, 5);
assert_eq!(span.start_line, 10);
assert_eq!(span.start_col, 5);
assert_eq!(span.end_line, 10);
assert_eq!(span.end_col, 6);
}
#[test]
fn test_source_span_contains() {
let span = SourceSpan::new("test.hs", 10, 5, 10, 15);
assert!(span.contains(10, 5));
assert!(span.contains(10, 10));
assert!(span.contains(10, 15));
assert!(!span.contains(10, 4));
assert!(!span.contains(10, 16));
assert!(!span.contains(9, 10));
assert!(!span.contains(11, 10));
}
#[test]
fn test_source_span_overlaps() {
let span1 = SourceSpan::new("test.hs", 10, 5, 10, 15);
let span2 = SourceSpan::new("test.hs", 10, 10, 10, 20);
let span3 = SourceSpan::new("test.hs", 10, 20, 10, 30);
let span4 = SourceSpan::new("other.hs", 10, 5, 10, 15);
assert!(span1.overlaps(&span2));
assert!(!span1.overlaps(&span3));
assert!(!span1.overlaps(&span4)); }
#[test]
fn test_diagnostic_severity_display() {
assert_eq!(format!("{}", DiagnosticSeverity::Error), "error");
assert_eq!(format!("{}", DiagnosticSeverity::Warning), "warning");
}
#[test]
fn test_ghc_diagnostic_builder() {
let diag = GhcDiagnostic::error("Variable not in scope: foo")
.with_span(SourceSpan::from_point("src/Main.hs", 10, 5))
.with_code("GHC-88464")
.with_hint("Perhaps you meant 'fooBar'");
assert!(diag.is_error());
assert_eq!(diag.code, Some("GHC-88464".to_string()));
assert_eq!(diag.hints.len(), 1);
}
#[test]
fn test_diagnostic_report() {
let mut report = DiagnosticReport::new();
report.add(GhcDiagnostic::error("Error 1").with_span(SourceSpan::from_point("a.hs", 1, 1)));
report.add(
GhcDiagnostic::warning("Warning 1").with_span(SourceSpan::from_point("a.hs", 2, 1)),
);
report.add(GhcDiagnostic::error("Error 2").with_span(SourceSpan::from_point("b.hs", 1, 1)));
report.add(GhcDiagnostic::error("General error"));
assert_eq!(report.error_count(), 3);
assert_eq!(report.warning_count(), 1);
assert_eq!(report.total_count(), 4);
assert!(report.has_errors());
assert_eq!(report.files().count(), 2);
}
#[test]
fn test_quick_fix() {
let fix = QuickFix::with_command("Add import", "hx add text").preferred();
assert!(fix.is_preferred);
assert_eq!(fix.command, Some("hx add text".to_string()));
let core_fix = fix.to_fix();
assert_eq!(core_fix.command, Some("hx add text".to_string()));
}
#[test]
fn test_diagnostic_is_unused() {
let diag1 = GhcDiagnostic::warning("Defined but not used: 'x'");
assert!(diag1.is_unused());
let diag2 =
GhcDiagnostic::warning("Import is redundant").with_warning_flag("-Wunused-imports");
assert!(diag2.is_unused());
let diag3 = GhcDiagnostic::error("Type mismatch");
assert!(!diag3.is_unused());
}
}