diff --git a/crates/ty_server/src/server/api/diagnostics.rs b/crates/ty_server/src/server/api/diagnostics.rs index c1b3449acc..367f65b96e 100644 --- a/crates/ty_server/src/server/api/diagnostics.rs +++ b/crates/ty_server/src/server/api/diagnostics.rs @@ -1,6 +1,18 @@ use lsp_server::ErrorCode; -use lsp_types::{PublishDiagnosticsParams, Url, notification::PublishDiagnostics}; +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; @@ -16,3 +28,152 @@ pub(super) fn clear_diagnostics(uri: &Url, notifier: &Notifier) -> Result<()> { .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(), + }) +} diff --git a/crates/ty_server/src/server/api/requests/diagnostic.rs b/crates/ty_server/src/server/api/requests/diagnostic.rs index 1824bc8693..a6b90f9fa1 100644 --- a/crates/ty_server/src/server/api/requests/diagnostic.rs +++ b/crates/ty_server/src/server/api/requests/diagnostic.rs @@ -2,20 +2,15 @@ use std::borrow::Cow; use lsp_types::request::DocumentDiagnosticRequest; use lsp_types::{ - Diagnostic, DiagnosticSeverity, DiagnosticTag, DocumentDiagnosticParams, - DocumentDiagnosticReport, DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, - NumberOrString, Range, RelatedFullDocumentDiagnosticReport, + DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult, + FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport, }; -use crate::PositionEncoding; -use crate::document::{FileRangeExt, ToRangeExt}; +use crate::server::api::diagnostics::compute_diagnostics; use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler}; use crate::server::{Result, client::Notifier}; use crate::session::DocumentSnapshot; -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 ty_project::ProjectDatabase; pub(crate) struct DocumentDiagnosticRequestHandler; @@ -34,7 +29,7 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { _notifier: Notifier, _params: DocumentDiagnosticParams, ) -> Result { - let diagnostics = compute_diagnostics(&snapshot, db); + let diagnostics = compute_diagnostics(db, &snapshot); Ok(DocumentDiagnosticReportResult::Report( DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport { @@ -47,145 +42,3 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler { )) } } - -fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &ProjectDatabase) -> 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() -} - -fn to_lsp_diagnostic( - db: &dyn Db, - diagnostic: &ruff_db::diagnostic::Diagnostic, - encoding: crate::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, - } -} - -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(lsp_types::DiagnosticRelatedInformation { - location, - message: annotation_message.to_string(), - }) -} - -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(lsp_types::DiagnosticRelatedInformation { - location, - message: diagnostic.concise_message().to_string(), - }) -}