From d95b0298627ac6e1569e028e3a04258146d9a38c Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Sun, 25 May 2025 23:16:38 -0500 Subject: [PATCH] [ty] Move diagnostics API for the server (#18308) ## Summary This PR moves the diagnostics API for the language server out from the request handler module to the diagnostics API module. This is in preparation to add support for publishing diagnostics. --- .../ty_server/src/server/api/diagnostics.rs | 163 +++++++++++++++++- .../src/server/api/requests/diagnostic.rs | 157 +---------------- 2 files changed, 167 insertions(+), 153 deletions(-) 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(), - }) -}