[ty_test] Refactor: rename diagnostic.rs to check_output.rs

SortedDiagnostics is now replaced by SortedCheckOutputs, which handles
both diagnostics and hover results. This refactoring:

- Renames diagnostic.rs to check_output.rs to better reflect its purpose
- Moves CheckOutput and SortedCheckOutputs definitions from matcher.rs
  to check_output.rs where they belong
- Removes the now-unused SortedDiagnostics infrastructure
- Ports the test to use SortedCheckOutputs instead of SortedDiagnostics
- Updates all imports throughout the codebase

The check_output module now serves as the central location for sorting
and grouping all types of check outputs (diagnostics and hover results)
by line number.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Douglas Creager
2025-10-08 13:20:18 -04:00
parent ab261360e4
commit ac1b68c56d
5 changed files with 216 additions and 339 deletions

View File

@@ -0,0 +1,205 @@
//! Sort and group check outputs (diagnostics and hover results) by line number,
//! so they can be correlated with assertions.
//!
//! We don't assume that we will get the outputs in source order.
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextSize;
use std::ops::Range;
/// Represents either a diagnostic or a hover result for matching against assertions.
#[derive(Debug, Clone)]
pub(crate) enum CheckOutput {
/// A regular diagnostic from the type checker
Diagnostic(Diagnostic),
/// A hover result for testing hover assertions
Hover {
/// The position where hover was requested
offset: TextSize,
/// The inferred type at that position
inferred_type: String,
},
}
impl CheckOutput {
fn line_number(&self, line_index: &LineIndex) -> OneIndexed {
match self {
CheckOutput::Diagnostic(diag) => diag
.primary_span()
.and_then(|span| span.range())
.map_or(OneIndexed::from_zero_indexed(0), |range| {
line_index.line_index(range.start())
}),
CheckOutput::Hover { offset, .. } => line_index.line_index(*offset),
}
}
}
/// All check outputs for one embedded Python file, sorted and grouped by line number.
///
/// The outputs are kept in a flat vector, sorted by line number. A separate vector of
/// [`LineOutputRange`] has one entry for each contiguous slice of the outputs vector
/// containing outputs which all start on the same line.
#[derive(Debug)]
pub(crate) struct SortedCheckOutputs {
outputs: Vec<CheckOutput>,
line_ranges: Vec<LineOutputRange>,
}
impl SortedCheckOutputs {
pub(crate) fn new(outputs: &[CheckOutput], line_index: &LineIndex) -> Self {
let mut outputs: Vec<_> = outputs
.iter()
.map(|output| OutputWithLine {
line_number: output.line_number(line_index),
output: output.clone(),
})
.collect();
outputs.sort_unstable_by_key(|output_with_line| output_with_line.line_number);
let mut result = Self {
outputs: Vec::with_capacity(outputs.len()),
line_ranges: vec![],
};
let mut current_line_number = None;
let mut start = 0;
for OutputWithLine {
line_number,
output,
} in outputs
{
match current_line_number {
None => {
current_line_number = Some(line_number);
}
Some(current) => {
if line_number != current {
let end = result.outputs.len();
result.line_ranges.push(LineOutputRange {
line_number: current,
output_index_range: start..end,
});
start = end;
current_line_number = Some(line_number);
}
}
}
result.outputs.push(output);
}
if let Some(line_number) = current_line_number {
result.line_ranges.push(LineOutputRange {
line_number,
output_index_range: start..result.outputs.len(),
});
}
result
}
pub(crate) fn iter_lines(&self) -> LineCheckOutputsIterator<'_> {
LineCheckOutputsIterator {
outputs: self.outputs.as_slice(),
inner: self.line_ranges.iter(),
}
}
}
#[derive(Debug)]
struct OutputWithLine {
line_number: OneIndexed,
output: CheckOutput,
}
/// Range delineating check outputs in [`SortedCheckOutputs`] that begin on a single line.
#[derive(Debug)]
struct LineOutputRange {
line_number: OneIndexed,
output_index_range: Range<usize>,
}
/// Iterator to group sorted check outputs by line.
pub(crate) struct LineCheckOutputsIterator<'a> {
outputs: &'a [CheckOutput],
inner: std::slice::Iter<'a, LineOutputRange>,
}
impl<'a> Iterator for LineCheckOutputsIterator<'a> {
type Item = LineCheckOutputs<'a>;
fn next(&mut self) -> Option<Self::Item> {
let LineOutputRange {
line_number,
output_index_range,
} = self.inner.next()?;
Some(LineCheckOutputs {
line_number: *line_number,
outputs: &self.outputs[output_index_range.clone()],
})
}
}
impl std::iter::FusedIterator for LineCheckOutputsIterator<'_> {}
/// All check outputs that start on a single line of source code in one embedded Python file.
#[derive(Debug)]
pub(crate) struct LineCheckOutputs<'a> {
/// Line number on which these outputs start.
pub(crate) line_number: OneIndexed,
/// Check outputs starting on this line.
pub(crate) outputs: &'a [CheckOutput],
}
#[cfg(test)]
mod tests {
use crate::db::Db;
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::source::line_index;
use ruff_db::system::DbWithWritableSystem as _;
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextRange, TextSize};
#[test]
fn sort_and_group() {
let mut db = Db::setup();
db.write_file("/src/test.py", "one\ntwo\n").unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap();
let lines = line_index(&db, file);
let ranges = [
TextRange::new(TextSize::new(0), TextSize::new(1)),
TextRange::new(TextSize::new(5), TextSize::new(10)),
TextRange::new(TextSize::new(1), TextSize::new(7)),
];
let check_outputs: Vec<_> = ranges
.into_iter()
.map(|range| {
let mut diag = Diagnostic::new(
DiagnosticId::Lint(LintName::of("dummy")),
Severity::Error,
"dummy",
);
let span = Span::from(file).with_range(range);
diag.annotate(Annotation::primary(span));
super::CheckOutput::Diagnostic(diag)
})
.collect();
let sorted = super::SortedCheckOutputs::new(&check_outputs, &lines);
let grouped = sorted.iter_lines().collect::<Vec<_>>();
let [line1, line2] = &grouped[..] else {
panic!("expected two lines");
};
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(0));
assert_eq!(line1.outputs.len(), 2);
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
assert_eq!(line2.outputs.len(), 1);
}
}

View File

@@ -1,190 +0,0 @@
//! Sort and group diagnostics by line number, so they can be correlated with assertions.
//!
//! We don't assume that we will get the diagnostics in source order.
use ruff_db::diagnostic::Diagnostic;
use ruff_source_file::{LineIndex, OneIndexed};
use std::ops::{Deref, Range};
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
///
/// The diagnostics are kept in a flat vector, sorted by line number. A separate vector of
/// [`LineDiagnosticRange`] has one entry for each contiguous slice of the diagnostics vector
/// containing diagnostics which all start on the same line.
#[derive(Debug)]
pub(crate) struct SortedDiagnostics<'a> {
diagnostics: Vec<&'a Diagnostic>,
line_ranges: Vec<LineDiagnosticRange>,
}
impl<'a> SortedDiagnostics<'a> {
pub(crate) fn new(
diagnostics: impl IntoIterator<Item = &'a Diagnostic>,
line_index: &LineIndex,
) -> Self {
let mut diagnostics: Vec<_> = diagnostics
.into_iter()
.map(|diagnostic| DiagnosticWithLine {
line_number: diagnostic
.primary_span()
.and_then(|span| span.range())
.map_or(OneIndexed::from_zero_indexed(0), |range| {
line_index.line_index(range.start())
}),
diagnostic,
})
.collect();
diagnostics.sort_unstable_by_key(|diagnostic_with_line| diagnostic_with_line.line_number);
let mut diags = Self {
diagnostics: Vec::with_capacity(diagnostics.len()),
line_ranges: vec![],
};
let mut current_line_number = None;
let mut start = 0;
for DiagnosticWithLine {
line_number,
diagnostic,
} in diagnostics
{
match current_line_number {
None => {
current_line_number = Some(line_number);
}
Some(current) => {
if line_number != current {
let end = diags.diagnostics.len();
diags.line_ranges.push(LineDiagnosticRange {
line_number: current,
diagnostic_index_range: start..end,
});
start = end;
current_line_number = Some(line_number);
}
}
}
diags.diagnostics.push(diagnostic);
}
if let Some(line_number) = current_line_number {
diags.line_ranges.push(LineDiagnosticRange {
line_number,
diagnostic_index_range: start..diags.diagnostics.len(),
});
}
diags
}
pub(crate) fn iter_lines(&self) -> LineDiagnosticsIterator<'_> {
LineDiagnosticsIterator {
diagnostics: self.diagnostics.as_slice(),
inner: self.line_ranges.iter(),
}
}
}
/// Range delineating diagnostics in [`SortedDiagnostics`] that begin on a single line.
#[derive(Debug)]
struct LineDiagnosticRange {
line_number: OneIndexed,
diagnostic_index_range: Range<usize>,
}
/// Iterator to group sorted diagnostics by line.
pub(crate) struct LineDiagnosticsIterator<'a> {
diagnostics: &'a [&'a Diagnostic],
inner: std::slice::Iter<'a, LineDiagnosticRange>,
}
impl<'a> Iterator for LineDiagnosticsIterator<'a> {
type Item = LineDiagnostics<'a>;
fn next(&mut self) -> Option<Self::Item> {
let LineDiagnosticRange {
line_number,
diagnostic_index_range,
} = self.inner.next()?;
Some(LineDiagnostics {
line_number: *line_number,
diagnostics: &self.diagnostics[diagnostic_index_range.clone()],
})
}
}
impl std::iter::FusedIterator for LineDiagnosticsIterator<'_> {}
/// All diagnostics that start on a single line of source code in one embedded Python file.
#[derive(Debug)]
pub(crate) struct LineDiagnostics<'a> {
/// Line number on which these diagnostics start.
pub(crate) line_number: OneIndexed,
/// Diagnostics starting on this line.
pub(crate) diagnostics: &'a [&'a Diagnostic],
}
impl<'a> Deref for LineDiagnostics<'a> {
type Target = [&'a Diagnostic];
fn deref(&self) -> &Self::Target {
self.diagnostics
}
}
#[derive(Debug)]
struct DiagnosticWithLine<'a> {
line_number: OneIndexed,
diagnostic: &'a Diagnostic,
}
#[cfg(test)]
mod tests {
use crate::db::Db;
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, LintName, Severity, Span};
use ruff_db::files::system_path_to_file;
use ruff_db::source::line_index;
use ruff_db::system::DbWithWritableSystem as _;
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextRange, TextSize};
#[test]
fn sort_and_group() {
let mut db = Db::setup();
db.write_file("/src/test.py", "one\ntwo\n").unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap();
let lines = line_index(&db, file);
let ranges = [
TextRange::new(TextSize::new(0), TextSize::new(1)),
TextRange::new(TextSize::new(5), TextSize::new(10)),
TextRange::new(TextSize::new(1), TextSize::new(7)),
];
let diagnostics: Vec<_> = ranges
.into_iter()
.map(|range| {
let mut diag = Diagnostic::new(
DiagnosticId::Lint(LintName::of("dummy")),
Severity::Error,
"dummy",
);
let span = Span::from(file).with_range(range);
diag.annotate(Annotation::primary(span));
diag
})
.collect();
let sorted = super::SortedDiagnostics::new(diagnostics.iter(), &lines);
let grouped = sorted.iter_lines().collect::<Vec<_>>();
let [line1, line2] = &grouped[..] else {
panic!("expected two lines");
};
assert_eq!(line1.line_number, OneIndexed::from_zero_indexed(0));
assert_eq!(line1.diagnostics.len(), 2);
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
assert_eq!(line2.diagnostics.len(), 1);
}
}

View File

@@ -3,7 +3,7 @@
//! This module provides functionality to extract hover assertions from comments,
//! infer types at specified positions, and generate hover check outputs for matching.
use crate::matcher;
use crate::check_output::CheckOutput;
use ruff_db::files::File;
use ruff_db::parsed::parsed_module;
use ruff_db::source::{line_index, source_text};
@@ -74,7 +74,7 @@ pub(crate) fn generate_hover_outputs(
db: &Db,
file: File,
assertions: &crate::assertion::InlineFileAssertions,
) -> Vec<matcher::CheckOutput> {
) -> Vec<CheckOutput> {
let source = source_text(db, file);
let lines = line_index(db, file);
@@ -107,7 +107,7 @@ pub(crate) fn generate_hover_outputs(
continue;
};
hover_outputs.push(matcher::CheckOutput::Hover {
hover_outputs.push(CheckOutput::Hover {
offset: hover_offset,
inferred_type,
});

View File

@@ -24,9 +24,9 @@ use ty_python_semantic::{
};
mod assertion;
mod check_output;
mod config;
mod db;
mod diagnostic;
mod hover;
mod matcher;
mod parser;
@@ -372,9 +372,9 @@ fn run_test(
});
// Convert diagnostics to CheckOutput
let mut check_outputs: Vec<matcher::CheckOutput> = diagnostics
let mut check_outputs: Vec<check_output::CheckOutput> = diagnostics
.iter()
.map(|diag| matcher::CheckOutput::Diagnostic(diag.clone()))
.map(|diag| check_output::CheckOutput::Diagnostic(diag.clone()))
.collect();
// Parse assertions to get hover assertions with correct line numbers

View File

@@ -10,150 +10,11 @@ use ruff_db::diagnostic::{Diagnostic, DiagnosticId};
use ruff_db::files::File;
use ruff_db::source::{SourceText, line_index, source_text};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::TextSize;
use crate::assertion::{InlineFileAssertions, ParsedAssertion, UnparsedAssertion};
use crate::check_output::{CheckOutput, LineCheckOutputs, SortedCheckOutputs};
use crate::db::Db;
/// Represents either a diagnostic or a hover result for matching against assertions.
#[derive(Debug, Clone)]
pub(crate) enum CheckOutput {
/// A regular diagnostic from the type checker
Diagnostic(Diagnostic),
/// A hover result for testing hover assertions
Hover {
/// The position where hover was requested
offset: TextSize,
/// The inferred type at that position
inferred_type: String,
},
}
impl CheckOutput {
fn line_number(&self, line_index: &LineIndex) -> OneIndexed {
match self {
CheckOutput::Diagnostic(diag) => diag
.primary_span()
.and_then(|span| span.range())
.map_or(OneIndexed::from_zero_indexed(0), |range| {
line_index.line_index(range.start())
}),
CheckOutput::Hover { offset, .. } => line_index.line_index(*offset),
}
}
}
/// All check outputs for one embedded Python file, sorted and grouped by line number.
///
/// Similar to `SortedDiagnostics` but works with `CheckOutput` instead.
#[derive(Debug)]
struct SortedCheckOutputs {
outputs: Vec<CheckOutput>,
line_ranges: Vec<LineOutputRange>,
}
impl SortedCheckOutputs {
fn new(outputs: &[CheckOutput], line_index: &LineIndex) -> Self {
let mut outputs: Vec<_> = outputs
.iter()
.map(|output| OutputWithLine {
line_number: output.line_number(line_index),
output: output.clone(),
})
.collect();
outputs.sort_unstable_by_key(|output_with_line| output_with_line.line_number);
let mut result = Self {
outputs: Vec::with_capacity(outputs.len()),
line_ranges: vec![],
};
let mut current_line_number = None;
let mut start = 0;
for OutputWithLine {
line_number,
output,
} in outputs
{
match current_line_number {
None => {
current_line_number = Some(line_number);
}
Some(current) => {
if line_number != current {
let end = result.outputs.len();
result.line_ranges.push(LineOutputRange {
line_number: current,
output_index_range: start..end,
});
start = end;
current_line_number = Some(line_number);
}
}
}
result.outputs.push(output);
}
if let Some(line_number) = current_line_number {
result.line_ranges.push(LineOutputRange {
line_number,
output_index_range: start..result.outputs.len(),
});
}
result
}
fn iter_lines(&self) -> LineCheckOutputsIterator<'_> {
LineCheckOutputsIterator {
outputs: self.outputs.as_slice(),
inner: self.line_ranges.iter(),
}
}
}
#[derive(Debug)]
struct OutputWithLine {
line_number: OneIndexed,
output: CheckOutput,
}
#[derive(Debug)]
struct LineOutputRange {
line_number: OneIndexed,
output_index_range: Range<usize>,
}
/// Iterator to group sorted check outputs by line.
struct LineCheckOutputsIterator<'a> {
outputs: &'a [CheckOutput],
inner: std::slice::Iter<'a, LineOutputRange>,
}
impl<'a> Iterator for LineCheckOutputsIterator<'a> {
type Item = LineCheckOutputs<'a>;
fn next(&mut self) -> Option<Self::Item> {
let LineOutputRange {
line_number,
output_index_range,
} = self.inner.next()?;
Some(LineCheckOutputs {
line_number: *line_number,
outputs: &self.outputs[output_index_range.clone()],
})
}
}
impl std::iter::FusedIterator for LineCheckOutputsIterator<'_> {}
/// All check outputs that start on a single line of source code.
#[derive(Debug)]
struct LineCheckOutputs<'a> {
line_number: OneIndexed,
outputs: &'a [CheckOutput],
}
#[derive(Debug, Default)]
pub(super) struct FailuresByLine {
failures: Vec<String>,
@@ -566,6 +427,7 @@ impl Matcher {
#[cfg(test)]
mod tests {
use super::FailuresByLine;
use crate::check_output::CheckOutput;
use ruff_db::Db;
use ruff_db::diagnostic::{Annotation, Diagnostic, DiagnosticId, Severity, Span};
use ruff_db::files::{File, system_path_to_file};
@@ -625,11 +487,11 @@ mod tests {
db.write_file("/src/test.py", source).unwrap();
let file = system_path_to_file(&db, "/src/test.py").unwrap();
let diagnostics: Vec<Diagnostic> = expected_diagnostics
let check_outputs: Vec<CheckOutput> = expected_diagnostics
.into_iter()
.map(|diagnostic| diagnostic.into_diagnostic(file))
.map(|diagnostic| CheckOutput::Diagnostic(diagnostic.into_diagnostic(file)))
.collect();
super::match_file(&db, file, &diagnostics)
super::match_file(&db, file, &check_outputs)
}
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {