use crate::lsp::position::Range;
use crate::source_file::SourceFile;
use crate::span::Span;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum DiagnosticSeverity {
Hint = 4,
Info = 3,
Warning = 2,
#[default]
Error = 1,
}
impl DiagnosticSeverity {
pub const fn name(&self) -> &'static str {
match self {
Self::Error => "error",
Self::Warning => "warning",
Self::Info => "info",
Self::Hint => "hint",
}
}
pub const fn is_error(&self) -> bool {
matches!(self, Self::Error)
}
pub const fn is_warning(&self) -> bool {
matches!(self, Self::Warning)
}
}
impl fmt::Display for DiagnosticSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DiagnosticRelatedInfo {
pub file_name: String,
pub span: Span,
pub message: String,
}
impl DiagnosticRelatedInfo {
pub fn new(file_name: impl Into<String>, span: Span, message: impl Into<String>) -> Self {
Self {
file_name: file_name.into(),
span,
message: message.into(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum DiagnosticDomain {
#[default]
TypeScript,
Sound,
}
impl DiagnosticDomain {
pub const fn prefix(&self) -> &'static str {
match self {
Self::TypeScript => "TS",
Self::Sound => "TSZ",
}
}
pub const fn is_typescript(&self) -> bool {
matches!(self, Self::TypeScript)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Diagnostic {
pub file_name: String,
pub span: Span,
pub message: String,
pub severity: DiagnosticSeverity,
pub code: u32,
#[serde(skip_serializing_if = "DiagnosticDomain::is_typescript", default)]
pub domain: DiagnosticDomain,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub related: Vec<DiagnosticRelatedInfo>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub source: Option<String>,
}
impl Diagnostic {
pub fn new(
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
severity: DiagnosticSeverity,
code: u32,
) -> Self {
Self {
file_name: file_name.into(),
span,
message: message.into(),
severity,
code,
domain: DiagnosticDomain::TypeScript,
related: Vec::new(),
source: Some("typescript".to_string()),
}
}
pub fn with_domain(mut self, domain: DiagnosticDomain) -> Self {
self.domain = domain;
if matches!(domain, DiagnosticDomain::Sound) {
self.source = Some("tsz-sound".to_string());
}
self
}
pub fn error(
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) -> Self {
Self::new(file_name, span, message, DiagnosticSeverity::Error, code)
}
pub fn warning(
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) -> Self {
Self::new(file_name, span, message, DiagnosticSeverity::Warning, code)
}
pub fn info(
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) -> Self {
Self::new(file_name, span, message, DiagnosticSeverity::Info, code)
}
pub fn hint(
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) -> Self {
Self::new(file_name, span, message, DiagnosticSeverity::Hint, code)
}
pub fn with_related(mut self, info: DiagnosticRelatedInfo) -> Self {
self.related.push(info);
self
}
pub fn with_related_all(mut self, infos: Vec<DiagnosticRelatedInfo>) -> Self {
self.related.extend(infos);
self
}
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub const fn is_error(&self) -> bool {
self.severity.is_error()
}
pub const fn is_warning(&self) -> bool {
self.severity.is_warning()
}
pub const fn start(&self) -> u32 {
self.span.start
}
pub const fn length(&self) -> u32 {
self.span.len()
}
pub fn format(&self, source_file: &mut SourceFile) -> String {
let pos = source_file.offset_to_position(self.span.start);
format!(
"{}({},{}): {} {}{}: {}",
self.file_name,
pos.line + 1,
pos.character + 1,
self.severity,
self.domain.prefix(),
self.code,
self.message
)
}
pub fn format_simple(&self) -> String {
format!(
"{}[{}{}]: {}",
self.severity,
self.domain.prefix(),
self.code,
self.message
)
}
pub fn to_range(&self, source_file: &mut SourceFile) -> Range {
source_file.span_to_range(self.span)
}
}
impl fmt::Display for Diagnostic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_simple())
}
}
#[derive(Clone, Debug, Default)]
pub struct DiagnosticBag {
diagnostics: Vec<Diagnostic>,
default_file: String,
error_count: usize,
warning_count: usize,
}
impl DiagnosticBag {
pub const fn new() -> Self {
Self {
diagnostics: Vec::new(),
default_file: String::new(),
error_count: 0,
warning_count: 0,
}
}
pub fn with_file(file_name: impl Into<String>) -> Self {
Self {
diagnostics: Vec::new(),
default_file: file_name.into(),
error_count: 0,
warning_count: 0,
}
}
pub fn set_default_file(&mut self, file_name: impl Into<String>) {
self.default_file = file_name.into();
}
pub fn default_file(&self) -> &str {
&self.default_file
}
pub fn add(&mut self, diagnostic: Diagnostic) {
match diagnostic.severity {
DiagnosticSeverity::Error => self.error_count += 1,
DiagnosticSeverity::Warning => self.warning_count += 1,
_ => {}
}
self.diagnostics.push(diagnostic);
}
pub fn error(&mut self, span: Span, message: impl Into<String>, code: u32) {
self.add(Diagnostic::error(&self.default_file, span, message, code));
}
pub fn sound_error(&mut self, span: Span, message: impl Into<String>, code: u32) {
self.add(
Diagnostic::error(&self.default_file, span, message, code)
.with_domain(DiagnosticDomain::Sound),
);
}
pub fn error_in(
&mut self,
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) {
self.add(Diagnostic::error(file_name, span, message, code));
}
pub fn sound_error_in(
&mut self,
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) {
self.add(
Diagnostic::error(file_name, span, message, code).with_domain(DiagnosticDomain::Sound),
);
}
pub fn warning(&mut self, span: Span, message: impl Into<String>, code: u32) {
self.add(Diagnostic::warning(&self.default_file, span, message, code));
}
pub fn warning_in(
&mut self,
file_name: impl Into<String>,
span: Span,
message: impl Into<String>,
code: u32,
) {
self.add(Diagnostic::warning(file_name, span, message, code));
}
pub fn info(&mut self, span: Span, message: impl Into<String>, code: u32) {
self.add(Diagnostic::info(&self.default_file, span, message, code));
}
pub fn hint(&mut self, span: Span, message: impl Into<String>, code: u32) {
self.add(Diagnostic::hint(&self.default_file, span, message, code));
}
pub const fn has_diagnostics(&self) -> bool {
!self.diagnostics.is_empty()
}
pub const fn has_errors(&self) -> bool {
self.error_count > 0
}
pub const fn has_warnings(&self) -> bool {
self.warning_count > 0
}
pub const fn len(&self) -> usize {
self.diagnostics.len()
}
pub const fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub const fn error_count(&self) -> usize {
self.error_count
}
pub const fn warning_count(&self) -> usize {
self.warning_count
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn iter(&self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.iter()
}
pub fn errors(&self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Error)
}
pub fn warnings(&self) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Warning)
}
pub fn for_file<'a>(&'a self, file_name: &'a str) -> impl Iterator<Item = &'a Diagnostic> {
self.diagnostics
.iter()
.filter(move |d| d.file_name == file_name)
}
pub fn by_code(&self, code: u32) -> impl Iterator<Item = &Diagnostic> {
self.diagnostics.iter().filter(move |d| d.code == code)
}
pub fn sort(&mut self) {
self.diagnostics
.sort_by(|a, b| match a.file_name.cmp(&b.file_name) {
std::cmp::Ordering::Equal => a.span.start.cmp(&b.span.start),
other => other,
});
}
pub fn clear(&mut self) {
self.diagnostics.clear();
self.error_count = 0;
self.warning_count = 0;
}
pub fn take(&mut self) -> Vec<Diagnostic> {
self.error_count = 0;
self.warning_count = 0;
std::mem::take(&mut self.diagnostics)
}
pub fn merge(&mut self, other: Self) {
for diag in other.diagnostics {
self.add(diag);
}
}
pub fn error_codes(&self) -> Vec<u32> {
self.errors().map(|d| d.code).collect()
}
pub fn format_all(&self, source_file: &mut SourceFile) -> String {
let mut result = String::new();
for diag in &self.diagnostics {
if !result.is_empty() {
result.push('\n');
}
result.push_str(&diag.format(source_file));
}
result
}
}
impl IntoIterator for DiagnosticBag {
type Item = Diagnostic;
type IntoIter = std::vec::IntoIter<Diagnostic>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.into_iter()
}
}
impl<'a> IntoIterator for &'a DiagnosticBag {
type Item = &'a Diagnostic;
type IntoIter = std::slice::Iter<'a, Diagnostic>;
fn into_iter(self) -> Self::IntoIter {
self.diagnostics.iter()
}
}
impl Extend<Diagnostic> for DiagnosticBag {
fn extend<T: IntoIterator<Item = Diagnostic>>(&mut self, iter: T) {
for diag in iter {
self.add(diag);
}
}
}
pub fn format_message(template: &str, args: &[&str]) -> String {
let mut result = template.to_string();
for (i, arg) in args.iter().enumerate() {
result = result.replace(&format!("{{{i}}}"), arg);
}
result
}
pub fn format_code_snippet(text: &str, span: Span, _context_lines: usize) -> String {
let mut result = String::new();
let mut line_start = 0;
for (i, ch) in text.char_indices() {
if i >= span.start as usize {
break;
}
if ch == '\n' {
line_start = i + 1;
}
}
let line_end = text[line_start..]
.find('\n')
.map_or(text.len(), |i| line_start + i);
let line_text = &text[line_start..line_end];
result.push_str(line_text);
result.push('\n');
let col = span.start as usize - line_start;
let underline_len = (span.len() as usize)
.min(line_end - span.start as usize)
.max(1);
result.push_str(&" ".repeat(col));
result.push_str(&"^".repeat(underline_len));
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_severity() {
assert_eq!(DiagnosticSeverity::Error.name(), "error");
assert!(DiagnosticSeverity::Error.is_error());
assert!(!DiagnosticSeverity::Warning.is_error());
assert!(DiagnosticSeverity::Warning.is_warning());
}
#[test]
fn test_diagnostic_creation() {
let diag = Diagnostic::error("test.ts", Span::new(10, 20), "Test error", 2304);
assert_eq!(diag.file_name, "test.ts");
assert_eq!(diag.span, Span::new(10, 20));
assert_eq!(diag.message, "Test error");
assert_eq!(diag.code, 2304);
assert!(diag.is_error());
}
#[test]
fn test_diagnostic_with_related() {
let diag =
Diagnostic::error("test.ts", Span::new(10, 20), "Test error", 2304).with_related(
DiagnosticRelatedInfo::new("other.ts", Span::new(5, 10), "See here"),
);
assert_eq!(diag.related.len(), 1);
assert_eq!(diag.related[0].file_name, "other.ts");
}
#[test]
fn test_diagnostic_format_simple() {
let diag = Diagnostic::error("test.ts", Span::new(10, 20), "Cannot find name", 2304);
assert_eq!(diag.format_simple(), "error[TS2304]: Cannot find name");
}
#[test]
fn test_diagnostic_bag_basic() {
let mut bag = DiagnosticBag::with_file("test.ts");
assert!(bag.is_empty());
assert!(!bag.has_errors());
bag.error(Span::new(0, 5), "Error 1", 2304);
bag.warning(Span::new(10, 15), "Warning 1", 6133);
assert_eq!(bag.len(), 2);
assert!(bag.has_errors());
assert!(bag.has_warnings());
assert_eq!(bag.error_count(), 1);
assert_eq!(bag.warning_count(), 1);
}
#[test]
fn test_diagnostic_bag_iteration() {
let mut bag = DiagnosticBag::with_file("test.ts");
bag.error(Span::new(0, 5), "Error 1", 2304);
bag.error(Span::new(10, 15), "Error 2", 2322);
bag.warning(Span::new(20, 25), "Warning 1", 6133);
let errors: Vec<_> = bag.errors().collect();
assert_eq!(errors.len(), 2);
let warnings: Vec<_> = bag.warnings().collect();
assert_eq!(warnings.len(), 1);
}
#[test]
fn test_diagnostic_bag_filter_by_code() {
let mut bag = DiagnosticBag::with_file("test.ts");
bag.error(Span::new(0, 5), "Error 1", 2304);
bag.error(Span::new(10, 15), "Error 2", 2322);
bag.error(Span::new(20, 25), "Error 3", 2304);
let code_2304: Vec<_> = bag.by_code(2304).collect();
assert_eq!(code_2304.len(), 2);
}
#[test]
fn test_diagnostic_bag_merge() {
let mut bag1 = DiagnosticBag::with_file("test.ts");
bag1.error(Span::new(0, 5), "Error 1", 2304);
let mut bag2 = DiagnosticBag::with_file("other.ts");
bag2.error(Span::new(10, 15), "Error 2", 2322);
bag1.merge(bag2);
assert_eq!(bag1.len(), 2);
assert_eq!(bag1.error_count(), 2);
}
#[test]
fn test_diagnostic_bag_take() {
let mut bag = DiagnosticBag::with_file("test.ts");
bag.error(Span::new(0, 5), "Error 1", 2304);
let diagnostics = bag.take();
assert_eq!(diagnostics.len(), 1);
assert!(bag.is_empty());
assert_eq!(bag.error_count(), 0);
}
#[test]
fn test_diagnostic_bag_sort() {
let mut bag = DiagnosticBag::new();
bag.error_in("b.ts", Span::new(10, 15), "B error", 2304);
bag.error_in("a.ts", Span::new(5, 10), "A error 2", 2322);
bag.error_in("a.ts", Span::new(0, 5), "A error 1", 2304);
bag.sort();
let diagnostics: Vec<_> = bag.iter().collect();
assert_eq!(diagnostics[0].file_name, "a.ts");
assert_eq!(diagnostics[0].span.start, 0);
assert_eq!(diagnostics[1].file_name, "a.ts");
assert_eq!(diagnostics[1].span.start, 5);
assert_eq!(diagnostics[2].file_name, "b.ts");
}
#[test]
fn test_format_message() {
let msg = format_message(
"Type '{0}' is not assignable to type '{1}'.",
&["number", "string"],
);
assert_eq!(msg, "Type 'number' is not assignable to type 'string'.");
}
#[test]
fn test_format_code_snippet() {
let text = "const x = 1;";
let span = Span::new(6, 7); let snippet = format_code_snippet(text, span, 0);
assert!(snippet.contains("const x = 1;"));
assert!(snippet.contains("^"));
}
#[test]
fn test_diagnostic_format_with_source() {
let mut source = SourceFile::new("test.ts", "const x = 1;");
let diag = Diagnostic::error("test.ts", Span::new(6, 7), "Cannot find name 'x'", 2304);
let formatted = diag.format(&mut source);
assert!(formatted.contains("test.ts(1,7)"));
assert!(formatted.contains("error"));
assert!(formatted.contains("TS2304"));
}
#[test]
fn test_error_codes() {
let mut bag = DiagnosticBag::with_file("test.ts");
bag.error(Span::new(0, 5), "Error 1", 2304);
bag.error(Span::new(10, 15), "Error 2", 2322);
bag.warning(Span::new(20, 25), "Warning 1", 6133);
let codes = bag.error_codes();
assert_eq!(codes, vec![2304, 2322]);
}
}