mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +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,
|
/// characteristics in the inputs given to the tool. Typically, but not always,
|
||||||
/// a characteristic is a deficiency. An example of a characteristic that is
|
/// a characteristic is a deficiency. An example of a characteristic that is
|
||||||
/// _not_ a deficiency is the `reveal_type` diagnostic for our type checker.
|
/// _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 {
|
pub struct Diagnostic {
|
||||||
/// The actual 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 {
|
struct DiagnosticInner {
|
||||||
id: DiagnosticId,
|
id: DiagnosticId,
|
||||||
severity: Severity,
|
severity: Severity,
|
||||||
|
@ -555,7 +555,7 @@ impl Eq for RenderingSortKey<'_> {}
|
||||||
/// Currently, the order in which sub-diagnostics are rendered relative to one
|
/// 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
|
/// another (for a single parent diagnostic) is the order in which they were
|
||||||
/// attached to the diagnostic.
|
/// attached to the diagnostic.
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, get_size2::GetSize)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash, get_size2::GetSize)]
|
||||||
pub struct SubDiagnostic {
|
pub struct SubDiagnostic {
|
||||||
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
|
/// Like with `Diagnostic`, we box the `SubDiagnostic` to make it
|
||||||
/// pointer-sized.
|
/// 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 {
|
struct SubDiagnosticInner {
|
||||||
severity: SubDiagnosticSeverity,
|
severity: SubDiagnosticSeverity,
|
||||||
message: DiagnosticMessage,
|
message: DiagnosticMessage,
|
||||||
|
@ -687,7 +687,7 @@ struct SubDiagnosticInner {
|
||||||
///
|
///
|
||||||
/// Messages attached to annotations should also be as brief and specific as
|
/// Messages attached to annotations should also be as brief and specific as
|
||||||
/// possible. Long messages could negative impact the quality of rendering.
|
/// 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 {
|
pub struct Annotation {
|
||||||
/// The span of this annotation, corresponding to some subsequence of the
|
/// The span of this annotation, corresponding to some subsequence of the
|
||||||
/// user's input that we want to highlight.
|
/// 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.
|
/// These tags are used to provide additional information about the annotation.
|
||||||
/// and are passed through to the language server protocol.
|
/// 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 {
|
pub enum DiagnosticTag {
|
||||||
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
|
||||||
Unnecessary,
|
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
|
/// 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.
|
/// 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 {
|
pub enum UnifiedFile {
|
||||||
Ty(File),
|
Ty(File),
|
||||||
Ruff(SourceFile),
|
Ruff(SourceFile),
|
||||||
|
@ -1080,7 +1080,7 @@ impl DiagnosticSource {
|
||||||
/// It consists of a `File` and an optional range into that file. When the
|
/// 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
|
/// 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.
|
/// 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 {
|
pub struct Span {
|
||||||
file: UnifiedFile,
|
file: UnifiedFile,
|
||||||
range: Option<TextRange>,
|
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 {
|
pub enum Severity {
|
||||||
Info,
|
Info,
|
||||||
Warning,
|
Warning,
|
||||||
|
@ -1193,7 +1193,7 @@ impl Severity {
|
||||||
/// This type only exists to add an additional `Help` severity that isn't present in `Severity` or
|
/// 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
|
/// used for main diagnostics. If we want to add `Severity::Help` in the future, this type could be
|
||||||
/// deleted and the two combined again.
|
/// 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 {
|
pub enum SubDiagnosticSeverity {
|
||||||
Help,
|
Help,
|
||||||
Info,
|
Info,
|
||||||
|
@ -1428,7 +1428,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||||
/// In most cases, callers shouldn't need to use this. Instead, there is
|
/// In most cases, callers shouldn't need to use this. Instead, there is
|
||||||
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
/// a blanket trait implementation for `IntoDiagnosticMessage` for
|
||||||
/// anything that implements `std::fmt::Display`.
|
/// 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>);
|
pub struct DiagnosticMessage(Box<str>);
|
||||||
|
|
||||||
impl DiagnosticMessage {
|
impl DiagnosticMessage {
|
||||||
|
|
|
@ -43,7 +43,7 @@ pub enum IsolationLevel {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A collection of [`Edit`] elements to be applied to a source file.
|
/// 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))]
|
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||||
pub struct Fix {
|
pub struct Fix {
|
||||||
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.
|
/// The [`Edit`] elements to be applied, sorted by [`Edit::start`] in ascending order.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt::{Debug, Display, Formatter};
|
use std::fmt::{Debug, Display, Formatter};
|
||||||
|
use std::hash::Hash;
|
||||||
use std::sync::{Arc, OnceLock};
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[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`].
|
/// 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.
|
/// 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))]
|
#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
|
||||||
pub struct SourceFile {
|
pub struct SourceFile {
|
||||||
inner: Arc<SourceFileInner>,
|
inner: Arc<SourceFileInner>,
|
||||||
|
@ -241,6 +242,13 @@ impl PartialEq for SourceFileInner {
|
||||||
|
|
||||||
impl Eq 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.
|
/// The line and column of an offset in a source file.
|
||||||
///
|
///
|
||||||
/// See [`LineIndex::line_column`] for more information.
|
/// 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::logging::{LogLevel, init_logging};
|
||||||
pub use crate::server::Server;
|
pub use crate::server::Server;
|
||||||
pub use crate::session::ClientOptions;
|
pub use crate::session::{ClientOptions, DiagnosticMode};
|
||||||
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
|
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
|
||||||
pub(crate) use session::{DocumentQuery, Session};
|
pub(crate) use session::{DocumentQuery, Session};
|
||||||
|
|
||||||
|
|
|
@ -98,7 +98,10 @@ impl<S> tracing_subscriber::layer::Filter<S> for LogLevelFilter {
|
||||||
meta: &tracing::Metadata<'_>,
|
meta: &tracing::Metadata<'_>,
|
||||||
_: &tracing_subscriber::layer::Context<'_, S>,
|
_: &tracing_subscriber::layer::Context<'_, S>,
|
||||||
) -> bool {
|
) -> 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()
|
self.filter.trace_level()
|
||||||
} else {
|
} else {
|
||||||
tracing::Level::WARN
|
tracing::Level::WARN
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::hash::{DefaultHasher, Hash as _, Hasher as _};
|
||||||
|
|
||||||
use lsp_types::notification::PublishDiagnostics;
|
use lsp_types::notification::PublishDiagnostics;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
|
||||||
|
@ -15,17 +17,77 @@ use crate::document::{DocumentKey, FileRangeExt, ToRangeExt};
|
||||||
use crate::session::DocumentSnapshot;
|
use crate::session::DocumentSnapshot;
|
||||||
use crate::session::client::Client;
|
use crate::session::client::Client;
|
||||||
use crate::system::{AnySystemPath, file_to_url};
|
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.
|
/// Represents the diagnostics for a text document or a notebook document.
|
||||||
pub(super) enum Diagnostics {
|
pub(super) enum LspDiagnostics {
|
||||||
TextDocument(Vec<Diagnostic>),
|
TextDocument(Vec<Diagnostic>),
|
||||||
|
|
||||||
/// A map of cell URLs to the diagnostics for that cell.
|
/// A map of cell URLs to the diagnostics for that cell.
|
||||||
NotebookDocument(FxHashMap<Url, Vec<Diagnostic>>),
|
NotebookDocument(FxHashMap<Url, Vec<Diagnostic>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Diagnostics {
|
impl LspDiagnostics {
|
||||||
/// Returns the diagnostics for a text document.
|
/// Returns the diagnostics for a text document.
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
@ -33,8 +95,8 @@ impl Diagnostics {
|
||||||
/// Panics if the diagnostics are for a notebook document.
|
/// Panics if the diagnostics are for a notebook document.
|
||||||
pub(super) fn expect_text_document(self) -> Vec<Diagnostic> {
|
pub(super) fn expect_text_document(self) -> Vec<Diagnostic> {
|
||||||
match self {
|
match self {
|
||||||
Diagnostics::TextDocument(diagnostics) => diagnostics,
|
LspDiagnostics::TextDocument(diagnostics) => diagnostics,
|
||||||
Diagnostics::NotebookDocument(_) => {
|
LspDiagnostics::NotebookDocument(_) => {
|
||||||
panic!("Expected a text document diagnostics, but got notebook diagnostics")
|
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 {
|
match diagnostics.to_lsp_diagnostics(db) {
|
||||||
Diagnostics::TextDocument(diagnostics) => {
|
LspDiagnostics::TextDocument(diagnostics) => {
|
||||||
publish_diagnostics_notification(url, diagnostics);
|
publish_diagnostics_notification(url, diagnostics);
|
||||||
}
|
}
|
||||||
Diagnostics::NotebookDocument(cell_diagnostics) => {
|
LspDiagnostics::NotebookDocument(cell_diagnostics) => {
|
||||||
for (cell_url, diagnostics) in cell_diagnostics {
|
for (cell_url, diagnostics) in cell_diagnostics {
|
||||||
publish_diagnostics_notification(cell_url, 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,
|
db: &ProjectDatabase,
|
||||||
snapshot: &DocumentSnapshot,
|
snapshot: &'a DocumentSnapshot,
|
||||||
) -> Option<Diagnostics> {
|
) -> Option<Diagnostics<'a>> {
|
||||||
let document = match snapshot.document() {
|
let document = match snapshot.document() {
|
||||||
Ok(document) => document,
|
Ok(document) => document,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
@ -206,41 +268,11 @@ pub(super) fn compute_diagnostics(
|
||||||
|
|
||||||
let diagnostics = db.check_file(file);
|
let diagnostics = db.check_file(file);
|
||||||
|
|
||||||
if let Some(notebook) = document.as_notebook() {
|
Some(Diagnostics {
|
||||||
let mut cell_diagnostics: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default();
|
items: diagnostics,
|
||||||
|
encoding: snapshot.encoding(),
|
||||||
// Populates all relevant URLs with an empty diagnostic list. This ensures that documents
|
document,
|
||||||
// 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(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts the tool specific [`Diagnostic`][ruff_db::diagnostic::Diagnostic] to an LSP
|
/// 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::request::DocumentDiagnosticRequest;
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
|
DocumentDiagnosticParams, DocumentDiagnosticReport, DocumentDiagnosticReportResult,
|
||||||
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport, Url,
|
FullDocumentDiagnosticReport, RelatedFullDocumentDiagnosticReport,
|
||||||
|
RelatedUnchangedDocumentDiagnosticReport, UnchangedDocumentDiagnosticReport, Url,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::server::Result;
|
use crate::server::Result;
|
||||||
use crate::server::api::diagnostics::{Diagnostics, compute_diagnostics};
|
use crate::server::api::diagnostics::compute_diagnostics;
|
||||||
use crate::server::api::traits::{
|
use crate::server::api::traits::{
|
||||||
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||||
};
|
};
|
||||||
|
@ -30,20 +31,38 @@ impl BackgroundDocumentRequestHandler for DocumentDiagnosticRequestHandler {
|
||||||
db: &ProjectDatabase,
|
db: &ProjectDatabase,
|
||||||
snapshot: DocumentSnapshot,
|
snapshot: DocumentSnapshot,
|
||||||
_client: &Client,
|
_client: &Client,
|
||||||
_params: DocumentDiagnosticParams,
|
params: DocumentDiagnosticParams,
|
||||||
) -> Result<DocumentDiagnosticReportResult> {
|
) -> 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 {
|
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
|
||||||
related_documents: None,
|
related_documents: None,
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
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
|
// SAFETY: Pull diagnostic requests are only called for text documents, not for
|
||||||
// notebook documents.
|
// notebook documents.
|
||||||
items: compute_diagnostics(db, &snapshot)
|
items: diagnostics.to_lsp_diagnostics(db).expect_text_document(),
|
||||||
.map_or_else(Vec::new, Diagnostics::expect_text_document),
|
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
))
|
};
|
||||||
|
|
||||||
|
Ok(DocumentDiagnosticReportResult::Report(report))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,20 @@
|
||||||
use lsp_types::request::WorkspaceDiagnosticRequest;
|
use std::collections::BTreeMap;
|
||||||
use lsp_types::{
|
|
||||||
FullDocumentDiagnosticReport, Url, WorkspaceDiagnosticParams, WorkspaceDiagnosticReport,
|
|
||||||
WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
|
|
||||||
WorkspaceFullDocumentDiagnosticReport,
|
|
||||||
};
|
|
||||||
use rustc_hash::FxHashMap;
|
|
||||||
|
|
||||||
use crate::server::Result;
|
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::{
|
use crate::server::api::traits::{
|
||||||
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
|
BackgroundRequestHandler, RequestHandler, RetriableRequestHandler,
|
||||||
};
|
};
|
||||||
use crate::session::SessionSnapshot;
|
use crate::session::SessionSnapshot;
|
||||||
use crate::session::client::Client;
|
use crate::session::client::Client;
|
||||||
use crate::system::file_to_url;
|
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;
|
pub(crate) struct WorkspaceDiagnosticRequestHandler;
|
||||||
|
|
||||||
|
@ -25,26 +26,31 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||||
fn run(
|
fn run(
|
||||||
snapshot: SessionSnapshot,
|
snapshot: SessionSnapshot,
|
||||||
_client: &Client,
|
_client: &Client,
|
||||||
_params: WorkspaceDiagnosticParams,
|
params: WorkspaceDiagnosticParams,
|
||||||
) -> Result<WorkspaceDiagnosticReportResult> {
|
) -> Result<WorkspaceDiagnosticReportResult> {
|
||||||
let index = snapshot.index();
|
let index = snapshot.index();
|
||||||
|
|
||||||
if !index.global_settings().diagnostic_mode().is_workspace() {
|
if !index.global_settings().diagnostic_mode().is_workspace() {
|
||||||
// VS Code sends us the workspace diagnostic request every 2 seconds, so these logs can
|
tracing::debug!("Workspace diagnostics is disabled; returning empty report");
|
||||||
// be quite verbose.
|
|
||||||
tracing::trace!("Workspace diagnostics is disabled; returning empty report");
|
|
||||||
return Ok(WorkspaceDiagnosticReportResult::Report(
|
return Ok(WorkspaceDiagnosticReportResult::Report(
|
||||||
WorkspaceDiagnosticReport { items: vec![] },
|
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();
|
let mut items = Vec::new();
|
||||||
|
|
||||||
for db in snapshot.projects() {
|
for db in snapshot.projects() {
|
||||||
let diagnostics = db.check();
|
let diagnostics = db.check();
|
||||||
|
|
||||||
// Group diagnostics by URL
|
// 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 {
|
for diagnostic in diagnostics {
|
||||||
if let Some(span) = diagnostic.primary_span() {
|
if let Some(span) = diagnostic.primary_span() {
|
||||||
|
@ -66,6 +72,23 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|key| index.make_document_ref(key).ok())
|
.and_then(|key| index.make_document_ref(key).ok())
|
||||||
.map(|doc| i64::from(doc.version()));
|
.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
|
// Convert diagnostics to LSP format
|
||||||
let lsp_diagnostics = file_diagnostics
|
let lsp_diagnostics = file_diagnostics
|
||||||
|
@ -75,13 +98,13 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Diagnostics have changed or this is the first request, return full report
|
||||||
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
items.push(WorkspaceDocumentDiagnosticReport::Full(
|
||||||
WorkspaceFullDocumentDiagnosticReport {
|
WorkspaceFullDocumentDiagnosticReport {
|
||||||
uri: url,
|
uri: url,
|
||||||
version,
|
version,
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||||
// TODO: We don't implement result ID caching yet
|
result_id: Some(result_id),
|
||||||
result_id: None,
|
|
||||||
items: lsp_diagnostics,
|
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(
|
Ok(WorkspaceDiagnosticReportResult::Report(
|
||||||
WorkspaceDiagnosticReport { items },
|
WorkspaceDiagnosticReport { items },
|
||||||
))
|
))
|
||||||
|
|
|
@ -21,8 +21,8 @@ use ty_project::{ChangeResult, Db as _, ProjectDatabase, ProjectMetadata};
|
||||||
|
|
||||||
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
pub(crate) use self::capabilities::ResolvedClientCapabilities;
|
||||||
pub(crate) use self::index::DocumentQuery;
|
pub(crate) use self::index::DocumentQuery;
|
||||||
pub use self::options::ClientOptions;
|
pub(crate) use self::options::AllOptions;
|
||||||
pub(crate) use self::options::{AllOptions, DiagnosticMode};
|
pub use self::options::{ClientOptions, DiagnosticMode};
|
||||||
pub(crate) use self::settings::ClientSettings;
|
pub(crate) use self::settings::ClientSettings;
|
||||||
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
|
||||||
use crate::server::publish_settings_diagnostics;
|
use crate::server::publish_settings_diagnostics;
|
||||||
|
|
|
@ -65,7 +65,7 @@ pub struct ClientOptions {
|
||||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
|
||||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) enum DiagnosticMode {
|
pub enum DiagnosticMode {
|
||||||
/// Check only currently open files.
|
/// Check only currently open files.
|
||||||
#[default]
|
#[default]
|
||||||
OpenFilesOnly,
|
OpenFilesOnly,
|
||||||
|
@ -140,6 +140,13 @@ impl ClientOptions {
|
||||||
overrides,
|
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
|
// 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::{
|
use lsp_types::request::{
|
||||||
DocumentDiagnosticRequest, Initialize, Request, Shutdown, WorkspaceConfiguration,
|
DocumentDiagnosticRequest, Initialize, Request, Shutdown, WorkspaceConfiguration,
|
||||||
|
WorkspaceDiagnosticRequest,
|
||||||
};
|
};
|
||||||
use lsp_types::{
|
use lsp_types::{
|
||||||
ClientCapabilities, ConfigurationParams, DiagnosticClientCapabilities,
|
ClientCapabilities, ConfigurationParams, DiagnosticClientCapabilities,
|
||||||
DidChangeTextDocumentParams, DidChangeWatchedFilesClientCapabilities,
|
DidChangeTextDocumentParams, DidChangeWatchedFilesClientCapabilities,
|
||||||
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
|
DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
|
||||||
DocumentDiagnosticParams, DocumentDiagnosticReportResult, FileEvent, InitializeParams,
|
DocumentDiagnosticParams, DocumentDiagnosticReportResult, FileEvent, InitializeParams,
|
||||||
InitializeResult, InitializedParams, PartialResultParams, PublishDiagnosticsClientCapabilities,
|
InitializeResult, InitializedParams, PartialResultParams, PreviousResultId,
|
||||||
TextDocumentClientCapabilities, TextDocumentContentChangeEvent, TextDocumentIdentifier,
|
PublishDiagnosticsClientCapabilities, TextDocumentClientCapabilities,
|
||||||
TextDocumentItem, Url, VersionedTextDocumentIdentifier, WorkDoneProgressParams,
|
TextDocumentContentChangeEvent, TextDocumentIdentifier, TextDocumentItem, Url,
|
||||||
WorkspaceClientCapabilities, WorkspaceFolder,
|
VersionedTextDocumentIdentifier, WorkDoneProgressParams, WorkspaceClientCapabilities,
|
||||||
|
WorkspaceDiagnosticParams, WorkspaceDiagnosticReportResult, WorkspaceFolder,
|
||||||
};
|
};
|
||||||
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf, TestSystem};
|
use ruff_db::system::{OsSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
|
use serde_json::json;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use ty_server::{ClientOptions, LogLevel, Server, init_logging};
|
use ty_server::{ClientOptions, LogLevel, Server, init_logging};
|
||||||
|
@ -156,6 +159,7 @@ impl TestServer {
|
||||||
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
||||||
test_context: TestContext,
|
test_context: TestContext,
|
||||||
capabilities: ClientCapabilities,
|
capabilities: ClientCapabilities,
|
||||||
|
initialization_options: Option<ClientOptions>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
setup_tracing();
|
setup_tracing();
|
||||||
|
|
||||||
|
@ -204,7 +208,7 @@ impl TestServer {
|
||||||
workspace_configurations,
|
workspace_configurations,
|
||||||
registered_capabilities: Vec::new(),
|
registered_capabilities: Vec::new(),
|
||||||
}
|
}
|
||||||
.initialize(workspace_folders, capabilities)
|
.initialize(workspace_folders, capabilities, initialization_options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform LSP initialization handshake
|
/// Perform LSP initialization handshake
|
||||||
|
@ -212,13 +216,15 @@ impl TestServer {
|
||||||
mut self,
|
mut self,
|
||||||
workspace_folders: Vec<WorkspaceFolder>,
|
workspace_folders: Vec<WorkspaceFolder>,
|
||||||
capabilities: ClientCapabilities,
|
capabilities: ClientCapabilities,
|
||||||
|
initialization_options: Option<ClientOptions>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let init_params = InitializeParams {
|
let init_params = InitializeParams {
|
||||||
capabilities,
|
capabilities,
|
||||||
workspace_folders: Some(workspace_folders),
|
workspace_folders: Some(workspace_folders),
|
||||||
// TODO: This should be configurable by the test server builder. This might not be
|
// TODO: This should be configurable by the test server builder. This might not be
|
||||||
// required after client settings are implemented in the server.
|
// 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()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -591,7 +597,6 @@ impl TestServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a `textDocument/didChange` notification with the given content changes
|
/// Send a `textDocument/didChange` notification with the given content changes
|
||||||
#[expect(dead_code)]
|
|
||||||
pub(crate) fn change_text_document(
|
pub(crate) fn change_text_document(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: impl AsRef<SystemPath>,
|
path: impl AsRef<SystemPath>,
|
||||||
|
@ -630,19 +635,36 @@ impl TestServer {
|
||||||
pub(crate) fn document_diagnostic_request(
|
pub(crate) fn document_diagnostic_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: impl AsRef<SystemPath>,
|
path: impl AsRef<SystemPath>,
|
||||||
|
previous_result_id: Option<String>,
|
||||||
) -> Result<DocumentDiagnosticReportResult> {
|
) -> Result<DocumentDiagnosticReportResult> {
|
||||||
let params = DocumentDiagnosticParams {
|
let params = DocumentDiagnosticParams {
|
||||||
text_document: TextDocumentIdentifier {
|
text_document: TextDocumentIdentifier {
|
||||||
uri: self.file_uri(path),
|
uri: self.file_uri(path),
|
||||||
},
|
},
|
||||||
identifier: Some("ty".to_string()),
|
identifier: Some("ty".to_string()),
|
||||||
previous_result_id: None,
|
previous_result_id,
|
||||||
work_done_progress_params: WorkDoneProgressParams::default(),
|
work_done_progress_params: WorkDoneProgressParams::default(),
|
||||||
partial_result_params: PartialResultParams::default(),
|
partial_result_params: PartialResultParams::default(),
|
||||||
};
|
};
|
||||||
let id = self.send_request::<DocumentDiagnosticRequest>(params);
|
let id = self.send_request::<DocumentDiagnosticRequest>(params);
|
||||||
self.await_response::<DocumentDiagnosticReportResult>(id)
|
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 {
|
impl fmt::Debug for TestServer {
|
||||||
|
@ -709,6 +731,7 @@ impl Drop for TestServer {
|
||||||
pub(crate) struct TestServerBuilder {
|
pub(crate) struct TestServerBuilder {
|
||||||
test_context: TestContext,
|
test_context: TestContext,
|
||||||
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
workspaces: Vec<(WorkspaceFolder, ClientOptions)>,
|
||||||
|
initialization_options: Option<ClientOptions>,
|
||||||
client_capabilities: ClientCapabilities,
|
client_capabilities: ClientCapabilities,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -735,10 +758,16 @@ impl TestServerBuilder {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
workspaces: Vec::new(),
|
workspaces: Vec::new(),
|
||||||
test_context: TestContext::new()?,
|
test_context: TestContext::new()?,
|
||||||
|
initialization_options: None,
|
||||||
client_capabilities,
|
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.
|
/// 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
|
/// This option will be used to respond to the `workspace/configuration` request that the
|
||||||
|
@ -837,7 +866,12 @@ impl TestServerBuilder {
|
||||||
|
|
||||||
/// Build the test server
|
/// Build the test server
|
||||||
pub(crate) fn build(self) -> Result<TestServer> {
|
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 anyhow::Result;
|
||||||
|
use lsp_types::{
|
||||||
|
PreviousResultId, WorkspaceDiagnosticReportResult, WorkspaceDocumentDiagnosticReport,
|
||||||
|
};
|
||||||
use ruff_db::system::SystemPath;
|
use ruff_db::system::SystemPath;
|
||||||
use ty_server::ClientOptions;
|
use ty_server::{ClientOptions, DiagnosticMode};
|
||||||
|
|
||||||
use crate::TestServerBuilder;
|
use crate::TestServerBuilder;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn on_did_open() -> Result<()> {
|
fn on_did_open() -> Result<()> {
|
||||||
|
let _filter = filter_result_id();
|
||||||
|
|
||||||
let workspace_root = SystemPath::new("src");
|
let workspace_root = SystemPath::new("src");
|
||||||
let foo = SystemPath::new("src/foo.py");
|
let foo = SystemPath::new("src/foo.py");
|
||||||
let foo_content = "\
|
let foo_content = "\
|
||||||
|
@ -21,9 +26,305 @@ def foo() -> str:
|
||||||
.wait_until_workspaces_are_initialized()?;
|
.wait_until_workspaces_are_initialized()?;
|
||||||
|
|
||||||
server.open_text_document(foo, &foo_content, 1);
|
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);
|
insta::assert_debug_snapshot!(diagnostics);
|
||||||
|
|
||||||
Ok(())
|
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 {
|
RelatedFullDocumentDiagnosticReport {
|
||||||
related_documents: None,
|
related_documents: None,
|
||||||
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
full_document_diagnostic_report: FullDocumentDiagnosticReport {
|
||||||
result_id: None,
|
result_id: Some(
|
||||||
|
"[RESULT_ID]",
|
||||||
|
),
|
||||||
items: [
|
items: [
|
||||||
Diagnostic {
|
Diagnostic {
|
||||||
range: Range {
|
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