Compare commits

...

3 Commits

Author SHA1 Message Date
Brent Westbrook
cabdd969ec convert to serializable diagnostics 2025-07-21 16:09:52 -04:00
Brent Westbrook
2e5c8b9799 Revert "custom serializers"
This reverts commit e1219bc27c.
2025-07-21 16:09:43 -04:00
Brent Westbrook
e1219bc27c custom serializers 2025-07-21 16:09:27 -04:00
2 changed files with 220 additions and 45 deletions

View File

@@ -18,14 +18,12 @@ use rustc_hash::FxHashMap;
use tempfile::NamedTempFile;
use ruff_cache::{CacheKey, CacheKeyHasher};
use ruff_db::diagnostic::Diagnostic;
use ruff_db::diagnostic::{Diagnostic, SerializableDiagnostics};
use ruff_diagnostics::Fix;
use ruff_linter::message::create_lint_diagnostic;
use ruff_linter::package::PackageRoot;
use ruff_linter::{VERSION, warn_user};
use ruff_macros::CacheKey;
use ruff_notebook::NotebookIndex;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use ruff_workspace::Settings;
use ruff_workspace::resolver::Resolver;
@@ -345,22 +343,7 @@ impl FileCache {
let diagnostics = if lint.messages.is_empty() {
Vec::new()
} else {
let file = SourceFileBuilder::new(path.to_string_lossy(), &*lint.source).finish();
lint.messages
.iter()
.map(|msg| {
create_lint_diagnostic(
&msg.body,
msg.suggestion.as_ref(),
msg.range,
msg.fix.clone(),
msg.parent,
file.clone(),
msg.noqa_offset,
msg.rule,
)
})
.collect()
lint.messages.to_diagnostics()
};
let notebook_indexes = if let Some(notebook_index) = lint.notebook_index.as_ref() {
FxHashMap::from_iter([(path.to_string_lossy().to_string(), notebook_index.clone())])
@@ -415,7 +398,8 @@ pub(crate) struct LintCacheData {
/// Imports made.
// pub(super) imports: ImportMap,
/// Diagnostic messages.
pub(super) messages: Vec<CacheMessage>,
#[bincode(with_serde)]
pub(super) messages: SerializableDiagnostics,
/// Source code of the file.
///
/// # Notes
@@ -438,30 +422,7 @@ impl LintCacheData {
String::new() // No messages, no need to keep the source!
};
let messages = diagnostics
.iter()
// Parse the kebab-case rule name into a `Rule`. This will fail for syntax errors, so
// this also serves to filter them out, but we shouldn't be caching files with syntax
// errors anyway.
.filter_map(|msg| Some((msg.name().parse().ok()?, msg)))
.map(|(rule, msg)| {
// Make sure that all message use the same source file.
assert_eq!(
msg.expect_ruff_source_file(),
diagnostics.first().unwrap().expect_ruff_source_file(),
"message uses a different source file"
);
CacheMessage {
rule,
body: msg.body().to_string(),
suggestion: msg.suggestion().map(ToString::to_string),
range: msg.expect_range(),
parent: msg.parent(),
fix: msg.fix().cloned(),
noqa_offset: msg.noqa_offset(),
}
})
.collect();
let messages = SerializableDiagnostics::new(diagnostics);
Self {
messages,

View File

@@ -9,6 +9,9 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
pub use self::render::{DisplayDiagnostic, DisplayDiagnostics, FileResolver, Input};
use crate::{Db, files::File};
#[cfg(feature = "serde")]
pub use serde_diagnostics::SerializableDiagnostics;
mod render;
mod stylesheet;
@@ -790,6 +793,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)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticTag {
/// Unused or unnecessary code. Used for unused parameters, unreachable code, etc.
Unnecessary,
@@ -804,6 +808,7 @@ pub enum DiagnosticTag {
///
/// Rules use kebab case, e.g. `no-foo`.
#[derive(Debug, Copy, Clone, PartialOrd, Ord, PartialEq, Eq, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct LintName(&'static str);
impl LintName {
@@ -844,6 +849,7 @@ impl PartialEq<&str> for LintName {
/// Uniquely identifies the kind of a diagnostic.
#[derive(Debug, Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum DiagnosticId {
Panic,
@@ -1141,6 +1147,7 @@ impl From<crate::files::FileRange> for Span {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Severity {
Info,
Warning,
@@ -1344,6 +1351,7 @@ impl std::fmt::Display for ConciseMessage<'_> {
/// a blanket trait implementation for `IntoDiagnosticMessage` for
/// anything that implements `std::fmt::Display`.
#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DiagnosticMessage(Box<str>);
impl DiagnosticMessage {
@@ -1407,7 +1415,11 @@ impl<T: std::fmt::Display> IntoDiagnosticMessage for T {
///
/// For Ruff rules this means the noqa code.
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Hash, get_size2::GetSize)]
#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(transparent)
)]
pub struct SecondaryCode(String);
impl SecondaryCode {
@@ -1452,3 +1464,205 @@ impl From<&SecondaryCode> for SecondaryCode {
value.clone()
}
}
#[cfg(feature = "serde")]
mod serde_diagnostics {
use std::sync::Arc;
use ruff_diagnostics::Fix;
use ruff_source_file::{SourceFile, SourceFileBuilder};
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use super::{
Annotation, Diagnostic, DiagnosticId, DiagnosticInner, DiagnosticMessage, DiagnosticTag,
LintName, SecondaryCode, Severity, Span, SubDiagnostic, SubDiagnosticInner, UnifiedFile,
};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct SerializableDiagnostics {
source_files: FxHashMap<String, String>,
diagnostics: Vec<SerializableDiagnostic>,
}
impl SerializableDiagnostics {
pub fn new(diagnostics: &[Diagnostic]) -> Self {
let mut source_files = FxHashMap::default();
let mut serializable_diagnostics = Vec::with_capacity(diagnostics.len());
for diagnostic in diagnostics {
let mut subs = Vec::with_capacity(diagnostic.inner.subs.len());
for sub in &diagnostic.inner.subs {
subs.push(SerializableSubDiagnostic {
severity: sub.inner.severity,
message: sub.inner.message.clone(),
annotations: serializable_annotations(
&mut source_files,
&sub.inner.annotations,
),
});
}
serializable_diagnostics.push(SerializableDiagnostic {
id: diagnostic.inner.id,
severity: diagnostic.inner.severity,
message: diagnostic.inner.message.clone(),
annotations: serializable_annotations(
&mut source_files,
&diagnostic.inner.annotations,
),
subs,
fix: diagnostic.inner.fix.clone(),
parent: diagnostic.inner.parent,
noqa_offset: diagnostic.inner.noqa_offset,
secondary_code: diagnostic.inner.secondary_code.clone(),
});
}
Self {
source_files,
diagnostics: serializable_diagnostics,
}
}
pub fn is_empty(&self) -> bool {
self.diagnostics.is_empty()
}
pub fn to_diagnostics(&self) -> Vec<Diagnostic> {
let source_files: FxHashMap<&str, SourceFile> = self
.source_files
.iter()
.map(|(name, contents)| {
(
name.as_str(),
SourceFileBuilder::new(name.clone(), contents.clone()).finish(),
)
})
.collect();
self.diagnostics
.iter()
.map(|diag| Diagnostic {
inner: Arc::new(DiagnosticInner {
id: diag.id,
severity: diag.severity,
message: diag.message.clone(),
annotations: annotations(&source_files, &diag.annotations),
subs: subdiagnostics(&source_files, &diag.subs),
fix: diag.fix.clone(),
parent: diag.parent,
noqa_offset: diag.noqa_offset,
secondary_code: diag.secondary_code.clone(),
}),
})
.collect()
}
}
fn serializable_annotations(
source_files: &mut FxHashMap<String, String>,
annotations: &[Annotation],
) -> Vec<SerializableAnnotation> {
let mut serializable_annotations = Vec::with_capacity(annotations.len());
for annotation in annotations {
let file = annotation.span.expect_ruff_file();
source_files.insert(file.name().to_string(), file.source_text().to_string());
serializable_annotations.push(SerializableAnnotation {
span: SerializableSpan {
name: file.name().to_string(),
range: annotation.span.range,
},
message: annotation.message.clone(),
is_primary: annotation.is_primary,
tags: annotation.tags.clone(),
});
}
serializable_annotations
}
fn annotations(
source_files: &FxHashMap<&str, SourceFile>,
serializable_annotations: &[SerializableAnnotation],
) -> Vec<Annotation> {
serializable_annotations
.iter()
.map(|ann| {
let span = Span {
file: UnifiedFile::Ruff(
source_files
.get(ann.span.name.as_str())
.expect("Expected source file in cache")
.clone(),
),
range: ann.span.range,
};
Annotation {
span,
message: ann.message.clone(),
is_primary: ann.is_primary,
tags: ann.tags.clone(),
}
})
.collect()
}
fn subdiagnostics(
source_files: &FxHashMap<&str, SourceFile>,
serializable_annotations: &[SerializableSubDiagnostic],
) -> Vec<SubDiagnostic> {
serializable_annotations
.iter()
.map(|sub| SubDiagnostic {
inner: Box::new(SubDiagnosticInner {
severity: sub.severity,
message: sub.message.clone(),
annotations: annotations(source_files, &sub.annotations),
}),
})
.collect()
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableDiagnostic {
id: DiagnosticId,
severity: Severity,
message: DiagnosticMessage,
annotations: Vec<SerializableAnnotation>,
subs: Vec<SerializableSubDiagnostic>,
fix: Option<Fix>,
parent: Option<TextSize>,
noqa_offset: Option<TextSize>,
secondary_code: Option<SecondaryCode>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableAnnotation {
span: SerializableSpan,
message: Option<DiagnosticMessage>,
is_primary: bool,
tags: Vec<DiagnosticTag>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableSubDiagnostic {
severity: Severity,
message: DiagnosticMessage,
annotations: Vec<SerializableAnnotation>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct SerializableSpan {
name: String,
range: Option<TextRange>,
}
impl<'de> Deserialize<'de> for LintName {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?.into_boxed_str();
Ok(LintName::of(Box::leak(s)))
}
}
}