diff --git a/crates/ty_test/src/check_output.rs b/crates/ty_test/src/check_output.rs new file mode 100644 index 0000000000..3bebfbd92c --- /dev/null +++ b/crates/ty_test/src/check_output.rs @@ -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, + line_ranges: Vec, +} + +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, +} + +/// 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 { + 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::>(); + + 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); + } +} diff --git a/crates/ty_test/src/diagnostic.rs b/crates/ty_test/src/diagnostic.rs deleted file mode 100644 index f869f999ab..0000000000 --- a/crates/ty_test/src/diagnostic.rs +++ /dev/null @@ -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, -} - -impl<'a> SortedDiagnostics<'a> { - pub(crate) fn new( - diagnostics: impl IntoIterator, - 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, -} - -/// 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 { - 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::>(); - - 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); - } -} diff --git a/crates/ty_test/src/hover.rs b/crates/ty_test/src/hover.rs index 91b8c2f2e6..915f3ecc8b 100644 --- a/crates/ty_test/src/hover.rs +++ b/crates/ty_test/src/hover.rs @@ -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 { +) -> Vec { 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, }); diff --git a/crates/ty_test/src/lib.rs b/crates/ty_test/src/lib.rs index 5d50d06636..1d175298cb 100644 --- a/crates/ty_test/src/lib.rs +++ b/crates/ty_test/src/lib.rs @@ -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 = diagnostics + let mut check_outputs: Vec = 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 diff --git a/crates/ty_test/src/matcher.rs b/crates/ty_test/src/matcher.rs index 5b28c15fcb..4969be9f35 100644 --- a/crates/ty_test/src/matcher.rs +++ b/crates/ty_test/src/matcher.rs @@ -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, - line_ranges: Vec, -} - -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, -} - -/// 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 { - 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, @@ -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 = expected_diagnostics + let check_outputs: Vec = 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])]) {