mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 13:51:16 +00:00
[ty] Implement diagnostic caching (#19605)
This commit is contained in:
parent
4ecf1d205a
commit
2a5ace6e55
15 changed files with 1322 additions and 100 deletions
|
@ -21,7 +21,7 @@ mod stylesheet;
|
|||
/// characteristics in the inputs given to the tool. Typically, but not always,
|
||||
/// a characteristic is a deficiency. An example of a characteristic that is
|
||||
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct Diagnostic {
|
||||
/// The actual diagnostic.
|
||||
///
|
||||
|
@ -479,7 +479,7 @@ impl Diagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
struct DiagnosticInner {
|
||||
id: DiagnosticId,
|
||||
severity: Severity,
|
||||
|
@ -555,7 +555,7 @@ impl Eq for RenderingSortKey<'_> {}
|
|||
/// Currently, the order in which sub-diagnostics are rendered relative to one
|
||||
/// another (for a single parent diagnostic) is the order in which they were
|
||||
/// attached to the diagnostic.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct SubDiagnostic {
|
||||
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
|
||||
/// pointer-sized.
|
||||
|
@ -659,7 +659,7 @@ impl SubDiagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
struct SubDiagnosticInner {
|
||||
severity: SubDiagnosticSeverity,
|
||||
message: DiagnosticMessage,
|
||||
|
@ -687,7 +687,7 @@ struct SubDiagnosticInner {
|
|||
///
|
||||
/// Messages attached to annotations should also be as brief and specific as
|
||||
/// possible. Long messages could negative impact the quality of rendering.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct Annotation {
|
||||
/// The span of this annotation, corresponding to some subsequence of the
|
||||
/// user's input that we want to highlight.
|
||||
|
@ -807,7 +807,7 @@ impl Annotation {
|
|||
///
|
||||
/// These tags are used to provide additional information about the annotation.
|
||||
/// and are passed through to the language server protocol.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub enum DiagnosticTag {
|
||||
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
||||
Unnecessary,
|
||||
|
@ -1016,7 +1016,7 @@ impl std::fmt::Display for DiagnosticId {
|
|||
///
|
||||
/// This enum presents a unified interface to these two types for the sake of creating [`Span`]s and
|
||||
/// emitting diagnostics from both ty and ruff.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub enum UnifiedFile {
|
||||
Ty(File),
|
||||
Ruff(SourceFile),
|
||||
|
@ -1080,7 +1080,7 @@ impl DiagnosticSource {
|
|||
/// It consists of a `File` and an optional range into that file. When the
|
||||
/// range isn't present, it semantically implies that the diagnostic refers to
|
||||
/// the entire file. For example, when the file should be executable but isn't.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
|
||||
pub struct Span {
|
||||
file: UnifiedFile,
|
||||
range: Option<TextRange>,
|
||||
|
@ -1158,7 +1158,7 @@ impl From<crate::files::FileRange> for Span {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
|
@ -1193,7 +1193,7 @@ impl Severity {
|
|||
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
|
||||
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
|
||||
/// deleted and the two combined again.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, get_size2::GetSize)]
|
||||
pub enum SubDiagnosticSeverity {
|
||||
Help,
|
||||
Info,
|
||||
|
@ -1428,7 +1428,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
|||
/// In most cases, callers shouldn't need to use this. Instead, there is
|
||||
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
||||
/// anything that implements `std::fmt::Display`.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||
pub struct DiagnosticMessage(Box<str>);
|
||||
|
||||
impl DiagnosticMessage {
|
||||
|
|
|
@ -43,7 +43,7 @@ pub enum IsolationLevel {
|
|||
}
|
||||
|
||||
/// A collection of [`Edit`] elements to be applied to a source file.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, get_size2::GetSize)]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Hash, get_size2::GetSize)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub struct Fix {
|
||||
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::cmp::Ordering;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::hash::Hash;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
|
@ -162,7 +163,7 @@ impl SourceFileBuilder {
|
|||
/// A source file that is identified by its name. Optionally stores the source code and [`LineIndex`].
|
||||
///
|
||||
/// Cloning a [`SourceFile`] is cheap, because it only requires bumping a reference count.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
#[derive(Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
|
||||
pub struct SourceFile {
|
||||
inner: Arc<SourceFileInner>,
|
||||
|
@ -241,6 +242,13 @@ impl PartialEq for SourceFileInner {
|
|||
|
||||
impl Eq for SourceFileInner {}
|
||||
|
||||
impl Hash for SourceFileInner {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.code.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// The line and column of an offset in a source file.
|
||||
///
|
||||
/// See [`LineIndex::line_column`] for more information.
|
||||
|
|
|
@ -6,7 +6,7 @@ use ruff_db::system::{OsSystem, SystemPathBuf};
|
|||
|
||||
pub use crate::logging::{LogLevel, init_logging};
|
||||
pub use crate::server::Server;
|
||||
pub use crate::session::ClientOptions;
|
||||
pub use crate::session::{ClientOptions, DiagnosticMode};
|
||||
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
|
||||
pub(crate) use session::{DocumentQuery, Session};
|
||||
|
||||
|
|
|
@ -98,7 +98,10 @@ impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
|
|||
meta: &tracing::Metadata<'_>,
|
||||
_: &tracing_subscriber::layer::Context<'_, S>,
|
||||
) -> bool {
|
||||
let filter = if meta.target().starts_with("ty") || meta.target().starts_with("ruff") {
|
||||
let filter = if meta.target().starts_with("ty")
|
||||
|| meta.target().starts_with("ruff")
|
||||
|| meta.target().starts_with("e2e")
|
||||
{
|
||||
self.filter.trace_level()
|
||||
} else {
|
||||
tracing::Level::WARN
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
|
||||
|
||||
use lsp_types::notification::PublishDiagnostics;
|
||||
use lsp_types::{
|
||||
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
||||
|
@ -15,17 +17,77 @@ use crate::document::{DocumentKey, FileRangeExt, ToRangeExt};
|
|||
use crate::session::DocumentSnapshot;
|
||||
use crate::session::client::Client;
|
||||
use crate::system::{AnySystemPath, file_to_url};
|
||||
use crate::{PositionEncoding, Session};
|
||||
use crate::{DocumentQuery, PositionEncoding, Session};
|
||||
|
||||
pub(super) struct Diagnostics<'a> {
|
||||
items: Vec<ruff_db::diagnostic::Diagnostic>,
|
||||
encoding: PositionEncoding,
|
||||
document: &'a DocumentQuery,
|
||||
}
|
||||
|
||||
impl Diagnostics<'_> {
|
||||
pub(super) fn result_id_from_hash(diagnostics: &[ruff_db::diagnostic::Diagnostic]) -> String {
|
||||
// Generate result ID based on raw diagnostic content only
|
||||
let mut hasher = DefaultHasher::new();
|
||||
|
||||
// Hash the length first to ensure different numbers of diagnostics produce different hashes
|
||||
diagnostics.hash(&mut hasher);
|
||||
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
|
||||
pub(super) fn result_id(&self) -> String {
|
||||
Self::result_id_from_hash(&self.items)
|
||||
}
|
||||
|
||||
pub(super) fn to_lsp_diagnostics(&self, db: &ProjectDatabase) -> LspDiagnostics {
|
||||
if let Some(notebook) = self.document.as_notebook() {
|
||||
let mut cell_diagnostics: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
|
||||
|
||||
// Populates all relevant URLs with an empty diagnostic list. This ensures that documents
|
||||
// without diagnostics still get updated.
|
||||
for cell_url in notebook.cell_urls() {
|
||||
cell_diagnostics.entry(cell_url.clone()).or_default();
|
||||
}
|
||||
|
||||
for (cell_index, diagnostic) in self.items.iter().map(|diagnostic| {
|
||||
(
|
||||
// TODO: Use the cell index instead using `SourceKind`
|
||||
usize::default(),
|
||||
to_lsp_diagnostic(db, diagnostic, self.encoding),
|
||||
)
|
||||
}) {
|
||||
let Some(cell_uri) = notebook.cell_uri_by_index(cell_index) else {
|
||||
tracing::warn!("Unable to find notebook cell at index {cell_index}");
|
||||
continue;
|
||||
};
|
||||
cell_diagnostics
|
||||
.entry(cell_uri.clone())
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
|
||||
LspDiagnostics::NotebookDocument(cell_diagnostics)
|
||||
} else {
|
||||
LspDiagnostics::TextDocument(
|
||||
self.items
|
||||
.iter()
|
||||
.map(|diagnostic| to_lsp_diagnostic(db, diagnostic, self.encoding))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the diagnostics for a text document or a notebook document.
|
||||
pub(super) enum Diagnostics {
|
||||
pub(super) enum LspDiagnostics {
|
||||
TextDocument(Vec<Diagnostic>),
|
||||
|
||||
/// A map of cell URLs to the diagnostics for that cell.
|
||||
NotebookDocument(FxHashMap<Url, Vec<Diagnostic>>),
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
impl LspDiagnostics {
|
||||
/// Returns the diagnostics for a text document.
|
||||
///
|
||||
/// # Panics
|
||||
|
@ -33,8 +95,8 @@ impl Diagnostics {
|
|||
/// Panics if the diagnostics are for a notebook document.
|
||||
pub(super) fn expect_text_document(self) -> Vec<Diagnostic> {
|
||||
match self {
|
||||
Diagnostics::TextDocument(diagnostics) => diagnostics,
|
||||
Diagnostics::NotebookDocument(_) => {
|
||||
LspDiagnostics::TextDocument(diagnostics) => diagnostics,
|
||||
LspDiagnostics::NotebookDocument(_) => {
|
||||
panic!("Expected a text document diagnostics, but got notebook diagnostics")
|
||||
}
|
||||
}
|
||||
|
@ -99,11 +161,11 @@ pub(super) fn publish_diagnostics(session: &Session, key: &DocumentKey, client:
|
|||
});
|
||||
};
|
||||
|
||||
match diagnostics {
|
||||
Diagnostics::TextDocument(diagnostics) => {
|
||||
match diagnostics.to_lsp_diagnostics(db) {
|
||||
LspDiagnostics::TextDocument(diagnostics) => {
|
||||
publish_diagnostics_notification(url, diagnostics);
|
||||
}
|
||||
Diagnostics::NotebookDocument(cell_diagnostics) => {
|
||||
LspDiagnostics::NotebookDocument(cell_diagnostics) => {
|
||||
for (cell_url, diagnostics) in cell_diagnostics {
|
||||
publish_diagnostics_notification(cell_url, diagnostics);
|
||||
}
|
||||
|
@ -187,10 +249,10 @@ pub(crate) fn publish_settings_diagnostics(
|
|||
}
|
||||
}
|
||||
|
||||
pub(super) fn compute_diagnostics(
|
||||
pub(super) fn compute_diagnostics<'a>(
|
||||
db: &ProjectDatabase,
|
||||
snapshot: &DocumentSnapshot,
|
||||
) -> Option<Diagnostics> {
|
||||
snapshot: &'a DocumentSnapshot,
|
||||
) -> Option<Diagnostics<'a>> {
|
||||
let document = match snapshot.document() {
|
||||
Ok(document) => document,
|
||||
Err(err) => {
|
||||
|
@ -206,41 +268,11 @@ pub(super) fn compute_diagnostics(
|
|||
|
||||
let diagnostics = db.check_file(file);
|
||||
|
||||
if let Some(notebook) = document.as_notebook() {
|
||||
let mut cell_diagnostics: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
|
||||
|
||||
// Populates all relevant URLs with an empty diagnostic list. This ensures that documents
|
||||
// without diagnostics still get updated.
|
||||
for cell_url in notebook.cell_urls() {
|
||||
cell_diagnostics.entry(cell_url.clone()).or_default();
|
||||
}
|
||||
|
||||
for (cell_index, diagnostic) in diagnostics.iter().map(|diagnostic| {
|
||||
(
|
||||
// TODO: Use the cell index instead using `SourceKind`
|
||||
usize::default(),
|
||||
to_lsp_diagnostic(db, diagnostic, snapshot.encoding()),
|
||||
)
|
||||
}) {
|
||||
let Some(cell_uri) = notebook.cell_uri_by_index(cell_index) else {
|
||||
tracing::warn!("Unable to find notebook cell at index {cell_index}");
|
||||
continue;
|
||||
};
|
||||
cell_diagnostics
|
||||
.entry(cell_uri.clone())
|
||||
.or_default()
|
||||
.push(diagnostic);
|
||||
}
|
||||
|
||||
Some(Diagnostics::NotebookDocument(cell_diagnostics))
|
||||
} else {
|
||||
Some(Diagnostics::TextDocument(
|
||||
diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| to_lsp_diagnostic(db, diagnostic, snapshot.encoding()))
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
Some(Diagnostics {
|
||||
items: diagnostics,
|
||||
encoding: snapshot.encoding(),
|
||||
document,
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP
|
||||
|
|
|
@ -3,11 +3,12 @@ use std::borrow::Cow;
|
|||
use lsp_types::request::DocumentDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
|
||||
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport, Url,
|
||||
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport,
|
||||
RelatedUnchangedDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url,
|
||||
};
|
||||
|
||||
use crate::server::Result;
|
||||
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
|
||||
use crate::server::api::diagnostics::compute_diagnostics;
|
||||
use crate::server::api::traits::{
|
||||
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||
};
|
||||
|
@ -30,20 +31,38 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
|||
db: &ProjectDatabase,
|
||||
snapshot: DocumentSnapshot,
|
||||
_client: &Client,
|
||||
_params: DocumentDiagnosticParams,
|
||||
params: DocumentDiagnosticParams,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
Ok(DocumentDiagnosticReportResult::Report(
|
||||
let diagnostics = compute_diagnostics(db, &snapshot);
|
||||
|
||||
let Some(diagnostics) = diagnostics else {
|
||||
return Ok(DocumentDiagnosticReportResult::Report(
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
|
||||
));
|
||||
};
|
||||
|
||||
let result_id = diagnostics.result_id();
|
||||
|
||||
let report = if params.previous_result_id.as_deref() == Some(&result_id) {
|
||||
DocumentDiagnosticReport::Unchanged(RelatedUnchangedDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
|
||||
result_id,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
result_id: Some(result_id),
|
||||
// SAFETY: Pull diagnostic requests are only called for text documents, not for
|
||||
// notebook documents.
|
||||
items: compute_diagnostics(db, &snapshot)
|
||||
.map_or_else(Vec::new, Diagnostics::expect_text_document),
|
||||
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
|
||||
},
|
||||
}),
|
||||
))
|
||||
})
|
||||
};
|
||||
|
||||
Ok(DocumentDiagnosticReportResult::Report(report))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
use lsp_types::request::WorkspaceDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
FullDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport,
|
||||
WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
|
||||
WorkspaceFullDocumentDiagnosticReport,
|
||||
};
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::server::Result;
|
||||
use crate::server::api::diagnostics::to_lsp_diagnostic;
|
||||
use crate::server::api::diagnostics::{Diagnostics, to_lsp_diagnostic};
|
||||
use crate::server::api::traits::{
|
||||
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||
};
|
||||
use crate::session::SessionSnapshot;
|
||||
use crate::session::client::Client;
|
||||
use crate::system::file_to_url;
|
||||
use lsp_types::request::WorkspaceDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
FullDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url,
|
||||
WorkspaceDiagnosticParams, WorkspaceDiagnosticReport, WorkspaceDiagnosticReportResult,
|
||||
WorkspaceDocumentDiagnosticReport, WorkspaceFullDocumentDiagnosticReport,
|
||||
WorkspaceUnchangedDocumentDiagnosticReport,
|
||||
};
|
||||
|
||||
pub(crate) struct WorkspaceDiagnosticRequestHandler;
|
||||
|
||||
|
@ -25,26 +26,31 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
|||
fn run(
|
||||
snapshot: SessionSnapshot,
|
||||
_client: &Client,
|
||||
_params: WorkspaceDiagnosticParams,
|
||||
params: WorkspaceDiagnosticParams,
|
||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||
let index = snapshot.index();
|
||||
|
||||
if !index.global_settings().diagnostic_mode().is_workspace() {
|
||||
// VS Code sends us the workspace diagnostic request every 2 seconds, so these logs can
|
||||
// be quite verbose.
|
||||
tracing::trace!("Workspace diagnostics is disabled; returning empty report");
|
||||
tracing::debug!("Workspace diagnostics is disabled; returning empty report");
|
||||
return Ok(WorkspaceDiagnosticReportResult::Report(
|
||||
WorkspaceDiagnosticReport { items: vec![] },
|
||||
));
|
||||
}
|
||||
|
||||
// Create a map of previous result IDs for efficient lookup
|
||||
let mut previous_results: BTreeMap<_, _> = params
|
||||
.previous_result_ids
|
||||
.into_iter()
|
||||
.map(|prev| (prev.uri, prev.value))
|
||||
.collect();
|
||||
|
||||
let mut items = Vec::new();
|
||||
|
||||
for db in snapshot.projects() {
|
||||
let diagnostics = db.check();
|
||||
|
||||
// Group diagnostics by URL
|
||||
let mut diagnostics_by_url: FxHashMap<Url, Vec<_>> = FxHashMap::default();
|
||||
let mut diagnostics_by_url: BTreeMap<Url, Vec<_>> = BTreeMap::default();
|
||||
|
||||
for diagnostic in diagnostics {
|
||||
if let Some(span) = diagnostic.primary_span() {
|
||||
|
@ -66,6 +72,23 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
|||
.ok()
|
||||
.and_then(|key| index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
let result_id = Diagnostics::result_id_from_hash(&file_diagnostics);
|
||||
|
||||
// Check if this file's diagnostics have changed since the previous request
|
||||
if let Some(previous_result_id) = previous_results.remove(&url) {
|
||||
if previous_result_id == result_id {
|
||||
// Diagnostics haven't changed, return unchanged report
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Unchanged(
|
||||
WorkspaceUnchangedDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
unchanged_document_diagnostic_report:
|
||||
UnchangedDocumentDiagnosticReport { result_id },
|
||||
},
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert diagnostics to LSP format
|
||||
let lsp_diagnostics = file_diagnostics
|
||||
|
@ -75,13 +98,13 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
|||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Diagnostics have changed or this is the first request, return full report
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
// TODO: We don't implement result ID caching yet
|
||||
result_id: None,
|
||||
result_id: Some(result_id),
|
||||
items: lsp_diagnostics,
|
||||
},
|
||||
},
|
||||
|
@ -89,6 +112,28 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle files that had diagnostics in previous request but no longer have any
|
||||
// Any remaining entries in previous_results are files that were fixed
|
||||
for (previous_url, _previous_result_id) in previous_results {
|
||||
// This file had diagnostics before but doesn't now, so we need to report it as having no diagnostics
|
||||
let version = index
|
||||
.key_from_url(previous_url.clone())
|
||||
.ok()
|
||||
.and_then(|key| index.make_document_ref(key).ok())
|
||||
.map(|doc| i64::from(doc.version()));
|
||||
|
||||
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: previous_url,
|
||||
version,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None, // No result ID needed for empty diagnostics
|
||||
items: vec![], // No diagnostics
|
||||
},
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
Ok(WorkspaceDiagnosticReportResult::Report(
|
||||
WorkspaceDiagnosticReport { items },
|
||||
))
|
||||
|
|
|
@ -21,8 +21,8 @@ use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata};
|
|||
|
||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||
pub(crate) use self::index::DocumentQuery;
|
||||
pub use self::options::ClientOptions;
|
||||
pub(crate) use self::options::{AllOptions, DiagnosticMode};
|
||||
pub(crate) use self::options::AllOptions;
|
||||
pub use self::options::{ClientOptions, DiagnosticMode};
|
||||
pub(crate) use self::settings::ClientSettings;
|
||||
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||
use crate::server::publish_settings_diagnostics;
|
||||
|
|
|
@ -65,7 +65,7 @@ pub struct ClientOptions {
|
|||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) enum DiagnosticMode {
|
||||
pub enum DiagnosticMode {
|
||||
/// Check only currently open files.
|
||||
#[default]
|
||||
OpenFilesOnly,
|
||||
|
@ -140,6 +140,13 @@ impl ClientOptions {
|
|||
overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new `ClientOptions` with the specified diagnostic mode
|
||||
#[must_use]
|
||||
pub fn with_diagnostic_mode(mut self, mode: DiagnosticMode) -> Self {
|
||||
self.diagnostic_mode = Some(mode);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dhruvmanila): We need to mirror the "python.*" namespace on the server side but ideally it
|
||||
|
|
|
@ -49,20 +49,23 @@ use lsp_types::notification::{
|
|||
};
|
||||
use lsp_types::request::{
|
||||
DocumentDiagnosticRequest, Initialize, Request, Shutdown, WorkspaceConfiguration,
|
||||
WorkspaceDiagnosticRequest,
|
||||
};
|
||||
use lsp_types::{
|
||||
ClientCapabilities, ConfigurationParams, DiagnosticClientCapabilities,
|
||||
DidChangeTextDocumentParams, DidChangeWatchedFilesClientCapabilities,
|
||||
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
|
||||
DocumentDiagnosticParams, DocumentDiagnosticReportResult, FileEvent, InitializeParams,
|
||||
InitializeResult, InitializedParams, PartialResultParams, PublishDiagnosticsClientCapabilities,
|
||||
TextDocumentClientCapabilities, TextDocumentContentChangeEvent, TextDocumentIdentifier,
|
||||
TextDocumentItem, Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams,
|
||||
WorkspaceClientCapabilities, WorkspaceFolder,
|
||||
InitializeResult, InitializedParams, PartialResultParams, PreviousResultId,
|
||||
PublishDiagnosticsClientCapabilities, TextDocumentClientCapabilities,
|
||||
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, Url,
|
||||
VersionedTextDocumentIdentifier, WorkDoneProgressParams, WorkspaceClientCapabilities,
|
||||
WorkspaceDiagnosticParams, WorkspaceDiagnosticReportResult, WorkspaceFolder,
|
||||
};
|
||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
use ty_server::{ClientOptions, LogLevel, Server, init_logging};
|
||||
|
@ -156,6 +159,7 @@ impl TestServer {
|
|||
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
||||
test_context: TestContext,
|
||||
capabilities: ClientCapabilities,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
) -> Result<Self> {
|
||||
setup_tracing();
|
||||
|
||||
|
@ -204,7 +208,7 @@ impl TestServer {
|
|||
workspace_configurations,
|
||||
registered_capabilities: Vec::new(),
|
||||
}
|
||||
.initialize(workspace_folders, capabilities)
|
||||
.initialize(workspace_folders, capabilities, initialization_options)
|
||||
}
|
||||
|
||||
/// Perform LSP initialization handshake
|
||||
|
@ -212,13 +216,15 @@ impl TestServer {
|
|||
mut self,
|
||||
workspace_folders: Vec<WorkspaceFolder>,
|
||||
capabilities: ClientCapabilities,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
) -> Result<Self> {
|
||||
let init_params = InitializeParams {
|
||||
capabilities,
|
||||
workspace_folders: Some(workspace_folders),
|
||||
// TODO: This should be configurable by the test server builder. This might not be
|
||||
// required after client settings are implemented in the server.
|
||||
initialization_options: Some(serde_json::Value::Object(serde_json::Map::new())),
|
||||
initialization_options: initialization_options
|
||||
.map(|options| json!({ "settings": options})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
@ -591,7 +597,6 @@ impl TestServer {
|
|||
}
|
||||
|
||||
/// Send a `textDocument/didChange` notification with the given content changes
|
||||
#[expect(dead_code)]
|
||||
pub(crate) fn change_text_document(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
|
@ -630,19 +635,36 @@ impl TestServer {
|
|||
pub(crate) fn document_diagnostic_request(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
previous_result_id: Option<String>,
|
||||
) -> Result<DocumentDiagnosticReportResult> {
|
||||
let params = DocumentDiagnosticParams {
|
||||
text_document: TextDocumentIdentifier {
|
||||
uri: self.file_uri(path),
|
||||
},
|
||||
identifier: Some("ty".to_string()),
|
||||
previous_result_id: None,
|
||||
previous_result_id,
|
||||
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||
partial_result_params: PartialResultParams::default(),
|
||||
};
|
||||
let id = self.send_request::<DocumentDiagnosticRequest>(params);
|
||||
self.await_response::<DocumentDiagnosticReportResult>(id)
|
||||
}
|
||||
|
||||
/// Send a `workspace/diagnostic` request with optional previous result IDs.
|
||||
pub(crate) fn workspace_diagnostic_request(
|
||||
&mut self,
|
||||
previous_result_ids: Option<Vec<PreviousResultId>>,
|
||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||
let params = WorkspaceDiagnosticParams {
|
||||
identifier: Some("ty".to_string()),
|
||||
previous_result_ids: previous_result_ids.unwrap_or_default(),
|
||||
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||
partial_result_params: PartialResultParams::default(),
|
||||
};
|
||||
|
||||
let id = self.send_request::<WorkspaceDiagnosticRequest>(params);
|
||||
self.await_response::<WorkspaceDiagnosticReportResult>(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for TestServer {
|
||||
|
@ -709,6 +731,7 @@ impl Drop for TestServer {
|
|||
pub(crate) struct TestServerBuilder {
|
||||
test_context: TestContext,
|
||||
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
||||
initialization_options: Option<ClientOptions>,
|
||||
client_capabilities: ClientCapabilities,
|
||||
}
|
||||
|
||||
|
@ -735,10 +758,16 @@ impl TestServerBuilder {
|
|||
Ok(Self {
|
||||
workspaces: Vec::new(),
|
||||
test_context: TestContext::new()?,
|
||||
initialization_options: None,
|
||||
client_capabilities,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn with_initialization_options(mut self, options: ClientOptions) -> Self {
|
||||
self.initialization_options = Some(options);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a workspace to the test server with the given root path and options.
|
||||
///
|
||||
/// This option will be used to respond to the `workspace/configuration` request that the
|
||||
|
@ -837,7 +866,12 @@ impl TestServerBuilder {
|
|||
|
||||
/// Build the test server
|
||||
pub(crate) fn build(self) -> Result<TestServer> {
|
||||
TestServer::new(self.workspaces, self.test_context, self.client_capabilities)
|
||||
TestServer::new(
|
||||
self.workspaces,
|
||||
self.test_context,
|
||||
self.client_capabilities,
|
||||
self.initialization_options,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
use anyhow::Result;
|
||||
use lsp_types::{
|
||||
PreviousResultId, WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
|
||||
};
|
||||
use ruff_db::system::SystemPath;
|
||||
use ty_server::ClientOptions;
|
||||
use ty_server::{ClientOptions, DiagnosticMode};
|
||||
|
||||
use crate::TestServerBuilder;
|
||||
|
||||
#[test]
|
||||
fn on_did_open() -> Result<()> {
|
||||
let _filter = filter_result_id();
|
||||
|
||||
let workspace_root = SystemPath::new("src");
|
||||
let foo = SystemPath::new("src/foo.py");
|
||||
let foo_content = "\
|
||||
|
@ -21,9 +26,305 @@ def foo() -> str:
|
|||
.wait_until_workspaces_are_initialized()?;
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
let diagnostics = server.document_diagnostic_request(foo)?;
|
||||
let diagnostics = server.document_diagnostic_request(foo, None)?;
|
||||
|
||||
insta::assert_debug_snapshot!(diagnostics);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_diagnostic_caching_unchanged() -> Result<()> {
|
||||
let _filter = filter_result_id();
|
||||
|
||||
let workspace_root = SystemPath::new("src");
|
||||
let foo = SystemPath::new("src/foo.py");
|
||||
let foo_content = "\
|
||||
def foo() -> str:
|
||||
return 42
|
||||
";
|
||||
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.with_workspace(workspace_root, ClientOptions::default())?
|
||||
.with_file(foo, foo_content)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
|
||||
server.open_text_document(foo, &foo_content, 1);
|
||||
|
||||
// First request with no previous result ID
|
||||
let first_response = server.document_diagnostic_request(foo, None)?;
|
||||
|
||||
// Extract result ID from first response
|
||||
let result_id = match &first_response {
|
||||
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||
lsp_types::DocumentDiagnosticReport::Full(report),
|
||||
) => report
|
||||
.full_document_diagnostic_report
|
||||
.result_id
|
||||
.as_ref()
|
||||
.expect("First response should have a result ID")
|
||||
.clone(),
|
||||
_ => panic!("First response should be a full report"),
|
||||
};
|
||||
|
||||
// Second request with the previous result ID - should return Unchanged
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id))?;
|
||||
|
||||
// Verify it's an unchanged report
|
||||
match second_response {
|
||||
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||
lsp_types::DocumentDiagnosticReport::Unchanged(_),
|
||||
) => {
|
||||
// Success - got unchanged report as expected
|
||||
}
|
||||
_ => panic!("Expected an unchanged report when diagnostics haven't changed"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_diagnostic_caching_changed() -> Result<()> {
|
||||
let _filter = filter_result_id();
|
||||
|
||||
let workspace_root = SystemPath::new("src");
|
||||
let foo = SystemPath::new("src/foo.py");
|
||||
let foo_content_v1 = "\
|
||||
def foo() -> str:
|
||||
return 42
|
||||
";
|
||||
let foo_content_v2 = "\
|
||||
def foo() -> str:
|
||||
return \"fixed\"
|
||||
";
|
||||
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.with_workspace(workspace_root, ClientOptions::default())?
|
||||
.with_file(foo, foo_content_v1)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
|
||||
server.open_text_document(foo, &foo_content_v1, 1);
|
||||
|
||||
// First request with no previous result ID
|
||||
let first_response = server.document_diagnostic_request(foo, None)?;
|
||||
|
||||
// Extract result ID from first response
|
||||
let result_id = match &first_response {
|
||||
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||
lsp_types::DocumentDiagnosticReport::Full(report),
|
||||
) => report
|
||||
.full_document_diagnostic_report
|
||||
.result_id
|
||||
.as_ref()
|
||||
.expect("First response should have a result ID")
|
||||
.clone(),
|
||||
_ => panic!("First response should be a full report"),
|
||||
};
|
||||
|
||||
// Change the document to fix the error
|
||||
server.change_text_document(
|
||||
foo,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: None,
|
||||
range_length: None,
|
||||
text: foo_content_v2.to_string(),
|
||||
}],
|
||||
2,
|
||||
);
|
||||
|
||||
// Second request with the previous result ID - should return a new full report
|
||||
let second_response = server.document_diagnostic_request(foo, Some(result_id))?;
|
||||
|
||||
// Verify it's a full report (not unchanged)
|
||||
match second_response {
|
||||
lsp_types::DocumentDiagnosticReportResult::Report(
|
||||
lsp_types::DocumentDiagnosticReport::Full(report),
|
||||
) => {
|
||||
// Should have no diagnostics now
|
||||
assert_eq!(report.full_document_diagnostic_report.items.len(), 0);
|
||||
}
|
||||
_ => panic!("Expected a full report when diagnostics have changed"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_diagnostic_caching() -> Result<()> {
|
||||
let _filter = filter_result_id();
|
||||
|
||||
let workspace_root = SystemPath::new("src");
|
||||
|
||||
// File A: Will have an unchanged diagnostic
|
||||
let file_a = SystemPath::new("src/unchanged.py");
|
||||
let file_a_content = "\
|
||||
def foo() -> str:
|
||||
return 42 # This error will remain the same
|
||||
";
|
||||
|
||||
// File B: Initially no error, will get a new error (added diagnostic)
|
||||
let file_b = SystemPath::new("src/new_error.py");
|
||||
let file_b_content_v1 = "\
|
||||
def foo() -> int:
|
||||
return 42 # No error initially
|
||||
";
|
||||
let file_b_content_v2 = "\
|
||||
def foo() -> str:
|
||||
return 42 # Error appears
|
||||
";
|
||||
|
||||
// File C: Initially has error, will be fixed (removed diagnostic)
|
||||
let file_c = SystemPath::new("src/fixed_error.py");
|
||||
let file_c_content_v1 = "\
|
||||
def foo() -> str:
|
||||
return 42 # Error initially
|
||||
";
|
||||
let file_c_content_v2 = "\
|
||||
def foo() -> str:
|
||||
return \"fixed\" # Error removed
|
||||
";
|
||||
|
||||
// File D: Has error that changes content (changed diagnostic)
|
||||
let file_d = SystemPath::new("src/changed_error.py");
|
||||
let file_d_content_v1 = "\
|
||||
def foo() -> str:
|
||||
return 42 # First error: expected str, got int
|
||||
";
|
||||
let file_d_content_v2 = "\
|
||||
def foo() -> int:
|
||||
return \"hello\" # Different error: expected int, got str
|
||||
";
|
||||
|
||||
// File E: Modified but same diagnostic (e.g., new function added but original error remains)
|
||||
let file_e = SystemPath::new("src/modified_same_error.py");
|
||||
let file_e_content_v1 = "\
|
||||
def foo() -> str:
|
||||
return 42 # Error: expected str, got int
|
||||
";
|
||||
let file_e_content_v2 = "\
|
||||
def bar() -> int:
|
||||
return 100 # New function added at the top
|
||||
|
||||
def foo() -> str:
|
||||
return 42 # Same error: expected str, got int
|
||||
";
|
||||
|
||||
let global_options = ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace);
|
||||
|
||||
let mut server = TestServerBuilder::new()?
|
||||
.with_workspace(
|
||||
workspace_root,
|
||||
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace),
|
||||
)?
|
||||
.with_initialization_options(global_options)
|
||||
.with_file(file_a, file_a_content)?
|
||||
.with_file(file_b, file_b_content_v1)?
|
||||
.with_file(file_c, file_c_content_v1)?
|
||||
.with_file(file_d, file_d_content_v1)?
|
||||
.with_file(file_e, file_e_content_v1)?
|
||||
.enable_pull_diagnostics(true)
|
||||
.build()?
|
||||
.wait_until_workspaces_are_initialized()?;
|
||||
|
||||
server.open_text_document(file_a, &file_a_content, 1);
|
||||
|
||||
// First request with no previous result IDs
|
||||
let first_response = server.workspace_diagnostic_request(None)?;
|
||||
insta::assert_debug_snapshot!("workspace_diagnostic_initial_state", first_response);
|
||||
|
||||
// Extract result IDs from the first response
|
||||
let previous_result_ids = match first_response {
|
||||
WorkspaceDiagnosticReportResult::Report(report) => {
|
||||
report.items.into_iter().filter_map(|item| match item {
|
||||
WorkspaceDocumentDiagnosticReport::Full(full_report) => {
|
||||
let result_id = full_report.full_document_diagnostic_report.result_id?;
|
||||
Some(PreviousResultId {
|
||||
uri: full_report.uri,
|
||||
value: result_id,
|
||||
})
|
||||
}
|
||||
WorkspaceDocumentDiagnosticReport::Unchanged(_) => {
|
||||
panic!("The first response must be a full report, not unchanged");
|
||||
}
|
||||
})
|
||||
}
|
||||
WorkspaceDiagnosticReportResult::Partial(_) => {
|
||||
panic!("The first response must be a full report");
|
||||
}
|
||||
}
|
||||
.collect();
|
||||
|
||||
// Make changes to files B, C, D, and E (leave A unchanged)
|
||||
// Need to open files before changing them
|
||||
server.open_text_document(file_b, &file_b_content_v1, 1);
|
||||
server.open_text_document(file_c, &file_c_content_v1, 1);
|
||||
server.open_text_document(file_d, &file_d_content_v1, 1);
|
||||
server.open_text_document(file_e, &file_e_content_v1, 1);
|
||||
|
||||
// File B: Add a new error
|
||||
server.change_text_document(
|
||||
file_b,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: None,
|
||||
range_length: None,
|
||||
text: file_b_content_v2.to_string(),
|
||||
}],
|
||||
2,
|
||||
);
|
||||
|
||||
// File C: Fix the error
|
||||
server.change_text_document(
|
||||
file_c,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: None,
|
||||
range_length: None,
|
||||
text: file_c_content_v2.to_string(),
|
||||
}],
|
||||
2,
|
||||
);
|
||||
|
||||
// File D: Change the error
|
||||
server.change_text_document(
|
||||
file_d,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: None,
|
||||
range_length: None,
|
||||
text: file_d_content_v2.to_string(),
|
||||
}],
|
||||
2,
|
||||
);
|
||||
|
||||
// File E: Modify the file but keep the same diagnostic
|
||||
server.change_text_document(
|
||||
file_e,
|
||||
vec![lsp_types::TextDocumentContentChangeEvent {
|
||||
range: None,
|
||||
range_length: None,
|
||||
text: file_e_content_v2.to_string(),
|
||||
}],
|
||||
2,
|
||||
);
|
||||
|
||||
// Second request with previous result IDs
|
||||
// Expected results:
|
||||
// - File A: Unchanged report (diagnostic hasn't changed)
|
||||
// - File B: Full report (new diagnostic appeared)
|
||||
// - File C: Full report with empty diagnostics (diagnostic was removed)
|
||||
// - File D: Full report (diagnostic content changed)
|
||||
// - File E: Full report (the range changes)
|
||||
let second_response = server.workspace_diagnostic_request(Some(previous_result_ids))?;
|
||||
insta::assert_debug_snapshot!("workspace_diagnostic_after_changes", second_response);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Redact result_id values since they are hash-based and non-deterministic
|
||||
fn filter_result_id() -> insta::internals::SettingsBindDropGuard {
|
||||
let mut settings = insta::Settings::clone_current();
|
||||
settings.add_filter(r#""[a-f0-9]{16}""#, r#""[RESULT_ID]""#);
|
||||
settings.bind_to_scope()
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ Report(
|
|||
RelatedFullDocumentDiagnosticReport {
|
||||
related_documents: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
|
|
|
@ -0,0 +1,359 @@
|
|||
---
|
||||
source: crates/ty_server/tests/e2e/pull_diagnostics.rs
|
||||
expression: second_response
|
||||
---
|
||||
Report(
|
||||
WorkspaceDiagnosticReport {
|
||||
items: [
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/changed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
2,
|
||||
),
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 18,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `int`, found `Literal[/"hello/"]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/changed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `int` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/modified_same_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
2,
|
||||
),
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 4,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 4,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/modified_same_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 3,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 3,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/new_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
2,
|
||||
),
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/new_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Unchanged(
|
||||
WorkspaceUnchangedDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/unchanged.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
1,
|
||||
),
|
||||
unchanged_document_diagnostic_report: UnchangedDocumentDiagnosticReport {
|
||||
result_id: "[RESULT_ID]",
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/fixed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
2,
|
||||
),
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: None,
|
||||
items: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
|
@ -0,0 +1,412 @@
|
|||
---
|
||||
source: crates/ty_server/tests/e2e/pull_diagnostics.rs
|
||||
expression: first_response
|
||||
---
|
||||
Report(
|
||||
WorkspaceDiagnosticReport {
|
||||
items: [
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/changed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/changed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/fixed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/fixed_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/modified_same_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: None,
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/modified_same_error.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
Full(
|
||||
WorkspaceFullDocumentDiagnosticReport {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/unchanged.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
version: Some(
|
||||
1,
|
||||
),
|
||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||
result_id: Some(
|
||||
"[RESULT_ID]",
|
||||
),
|
||||
items: [
|
||||
Diagnostic {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 1,
|
||||
character: 11,
|
||||
},
|
||||
end: Position {
|
||||
line: 1,
|
||||
character: 13,
|
||||
},
|
||||
},
|
||||
severity: Some(
|
||||
Error,
|
||||
),
|
||||
code: Some(
|
||||
String(
|
||||
"invalid-return-type",
|
||||
),
|
||||
),
|
||||
code_description: Some(
|
||||
CodeDescription {
|
||||
href: Url {
|
||||
scheme: "https",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: Some(
|
||||
Domain(
|
||||
"ty.dev",
|
||||
),
|
||||
),
|
||||
port: None,
|
||||
path: "/rules",
|
||||
query: None,
|
||||
fragment: Some(
|
||||
"invalid-return-type",
|
||||
),
|
||||
},
|
||||
},
|
||||
),
|
||||
source: Some(
|
||||
"ty",
|
||||
),
|
||||
message: "Return type does not match returned value: expected `str`, found `Literal[42]`",
|
||||
related_information: Some(
|
||||
[
|
||||
DiagnosticRelatedInformation {
|
||||
location: Location {
|
||||
uri: Url {
|
||||
scheme: "file",
|
||||
cannot_be_a_base: false,
|
||||
username: "",
|
||||
password: None,
|
||||
host: None,
|
||||
port: None,
|
||||
path: "<temp_dir>/src/unchanged.py",
|
||||
query: None,
|
||||
fragment: None,
|
||||
},
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 13,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 16,
|
||||
},
|
||||
},
|
||||
},
|
||||
message: "Expected `str` because of return type",
|
||||
},
|
||||
],
|
||||
),
|
||||
tags: None,
|
||||
data: None,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
Loading…
Add table
Add a link
Reference in a new issue