use lsp_server::ErrorCode; use lsp_types::notification::PublishDiagnostics; use lsp_types::{ Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, NumberOrString, PublishDiagnosticsParams, Range, Url, }; use ruff_db::diagnostic::{Annotation, Severity, SubDiagnostic}; use ruff_db::files::FileRange; use ruff_db::source::{line_index, source_text}; use ty_project::{Db, ProjectDatabase}; use crate::DocumentSnapshot; use crate::PositionEncoding; use crate::document::{FileRangeExt, ToRangeExt}; use crate::server::Result; use crate::server::client::Notifier; use super::LSPResult; pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> { notifier .notify::(PublishDiagnosticsParams { uri: uri.clone(), diagnostics: vec![], version: None, }) .with_failure_code(ErrorCode::InternalError)?; Ok(()) } pub(super) fn compute_diagnostics( db: &ProjectDatabase, snapshot: &DocumentSnapshot, ) -> Vec { let Some(file) = snapshot.file(db) else { tracing::info!( "No file found for snapshot for `{}`", snapshot.query().file_url() ); return vec![]; }; let diagnostics = match db.check_file(file) { Ok(diagnostics) => diagnostics, Err(cancelled) => { tracing::info!("Diagnostics computation {cancelled}"); return vec![]; } }; diagnostics .as_slice() .iter() .map(|message| to_lsp_diagnostic(db, message, snapshot.encoding())) .collect() } /// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP /// [`Diagnostic`]. fn to_lsp_diagnostic( db: &dyn Db, diagnostic: &ruff_db::diagnostic::Diagnostic, encoding: PositionEncoding, ) -> Diagnostic { let range = if let Some(span) = diagnostic.primary_span() { let file = span.expect_ty_file(); let index = line_index(db.upcast(), file); let source = source_text(db.upcast(), file); span.range() .map(|range| range.to_lsp_range(&source, &index, encoding)) .unwrap_or_default() } else { Range::default() }; let severity = match diagnostic.severity() { Severity::Info => DiagnosticSeverity::INFORMATION, Severity::Warning => DiagnosticSeverity::WARNING, Severity::Error | Severity::Fatal => DiagnosticSeverity::ERROR, }; let tags = diagnostic .primary_tags() .map(|tags| { tags.iter() .map(|tag| match tag { ruff_db::diagnostic::DiagnosticTag::Unnecessary => DiagnosticTag::UNNECESSARY, ruff_db::diagnostic::DiagnosticTag::Deprecated => DiagnosticTag::DEPRECATED, }) .collect::>() }) .filter(|mapped_tags| !mapped_tags.is_empty()); let code_description = diagnostic .id() .is_lint() .then(|| { Some(lsp_types::CodeDescription { href: lsp_types::Url::parse(&format!("https://ty.dev/rules#{}", diagnostic.id())) .ok()?, }) }) .flatten(); let mut related_information = Vec::new(); related_information.extend( diagnostic .secondary_annotations() .filter_map(|annotation| annotation_to_related_information(db, annotation, encoding)), ); for sub_diagnostic in diagnostic.sub_diagnostics() { related_information.extend(sub_diagnostic_to_related_information( db, sub_diagnostic, encoding, )); related_information.extend( sub_diagnostic .annotations() .iter() .filter_map(|annotation| { annotation_to_related_information(db, annotation, encoding) }), ); } Diagnostic { range, severity: Some(severity), tags, code: Some(NumberOrString::String(diagnostic.id().to_string())), code_description, source: Some("ty".into()), message: diagnostic.concise_message().to_string(), related_information: Some(related_information), data: None, } } /// Converts an [`Annotation`] to a [`DiagnosticRelatedInformation`]. fn annotation_to_related_information( db: &dyn Db, annotation: &Annotation, encoding: PositionEncoding, ) -> Option { let span = annotation.get_span(); let annotation_message = annotation.get_message()?; let range = FileRange::try_from(span).ok()?; let location = range.to_location(db.upcast(), encoding)?; Some(DiagnosticRelatedInformation { location, message: annotation_message.to_string(), }) } /// Converts a [`SubDiagnostic`] to a [`DiagnosticRelatedInformation`]. fn sub_diagnostic_to_related_information( db: &dyn Db, diagnostic: &SubDiagnostic, encoding: PositionEncoding, ) -> Option { let primary_annotation = diagnostic.primary_annotation()?; let span = primary_annotation.get_span(); let range = FileRange::try_from(span).ok()?; let location = range.to_location(db.upcast(), encoding)?; Some(DiagnosticRelatedInformation { location, message: diagnostic.concise_message().to_string(), }) }