mirror of https://github.com/astral-sh/ruff
180 lines
5.4 KiB
Rust
180 lines
5.4 KiB
Rust
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::<PublishDiagnostics>(PublishDiagnosticsParams {
|
|
uri: uri.clone(),
|
|
diagnostics: vec![],
|
|
version: None,
|
|
})
|
|
.with_failure_code(ErrorCode::InternalError)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn compute_diagnostics(
|
|
db: &ProjectDatabase,
|
|
snapshot: &DocumentSnapshot,
|
|
) -> Vec<Diagnostic> {
|
|
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::<Vec<DiagnosticTag>>()
|
|
})
|
|
.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<DiagnosticRelatedInformation> {
|
|
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<DiagnosticRelatedInformation> {
|
|
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(),
|
|
})
|
|
}
|