Skip to content

Commit

Permalink
Add mdtest support for files with invalid syntax (#14126)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser authored Nov 6, 2024
1 parent 4ece8e5 commit a56ee92
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 71 deletions.
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ repos:
- id: blacken-docs
args: ["--pyi", "--line-length", "130"]
files: '^crates/.*/resources/mdtest/.*\.md'
exclude: |
(?x)^(
.*?invalid(_.+)_syntax.md
)$
additional_dependencies:
- black==24.10.0

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Exception Handling

## Invalid syntax

```py
from typing_extensions import reveal_type

try:
print
except as e: # error: [invalid-syntax]
reveal_type(e) # revealed: Unknown

```
2 changes: 1 addition & 1 deletion crates/red_knot_python_semantic/src/types/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::Arc;
use crate::types::{ClassLiteralType, Type};
use crate::Db;

#[derive(Debug, Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct TypeCheckDiagnostic {
// TODO: Don't use string keys for rules
pub(super) rule: String,
Expand Down
23 changes: 1 addition & 22 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5061,27 +5061,6 @@ mod tests {
Ok(())
}

#[test]
fn exception_handler_with_invalid_syntax() -> anyhow::Result<()> {
let mut db = setup_db();

db.write_dedented(
"src/a.py",
"
from typing_extensions import reveal_type
try:
print
except as e:
reveal_type(e)
",
)?;

assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `Unknown`"]);

Ok(())
}

#[test]
fn basic_comprehension() -> anyhow::Result<()> {
let mut db = setup_db();
Expand Down Expand Up @@ -5424,7 +5403,7 @@ mod tests {
return 42
class Iterable:
def __iter__(self) -> Iterator:
def __iter__(self) -> Iterator: ...
x = [*NotIterable()]
y = [*Iterable()]
Expand Down
1 change: 1 addition & 0 deletions crates/red_knot_test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ red_knot_python_semantic = { workspace = true }
red_knot_vendored = { workspace = true }
ruff_db = { workspace = true }
ruff_index = { workspace = true }
ruff_python_parser = { workspace = true }
ruff_python_trivia = { workspace = true }
ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true }
Expand Down
93 changes: 86 additions & 7 deletions crates/red_knot_test/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,63 @@
//!
//! We don't assume that we will get the diagnostics in source order.

use red_knot_python_semantic::types::TypeCheckDiagnostic;
use ruff_python_parser::ParseError;
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::Ranged;
use ruff_text_size::{Ranged, TextRange};
use std::borrow::Cow;
use std::ops::{Deref, Range};

pub(super) trait Diagnostic: std::fmt::Debug {
fn rule(&self) -> &str;

fn message(&self) -> Cow<str>;

fn range(&self) -> TextRange;
}

impl Diagnostic for TypeCheckDiagnostic {
fn rule(&self) -> &str {
TypeCheckDiagnostic::rule(self)
}

fn message(&self) -> Cow<str> {
TypeCheckDiagnostic::message(self).into()
}

fn range(&self) -> TextRange {
Ranged::range(self)
}
}

impl Diagnostic for ParseError {
fn rule(&self) -> &str {
"invalid-syntax"
}

fn message(&self) -> Cow<str> {
self.error.to_string().into()
}

fn range(&self) -> TextRange {
self.location
}
}

impl Diagnostic for Box<dyn Diagnostic> {
fn rule(&self) -> &str {
(**self).rule()
}

fn message(&self) -> Cow<str> {
(**self).message()
}

fn range(&self) -> TextRange {
(**self).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
Expand All @@ -19,13 +72,13 @@ pub(crate) struct SortedDiagnostics<T> {

impl<T> SortedDiagnostics<T>
where
T: Ranged + Clone,
T: Diagnostic,
{
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
let mut diagnostics: Vec<_> = diagnostics
.into_iter()
.map(|diagnostic| DiagnosticWithLine {
line_number: line_index.line_index(diagnostic.start()),
line_number: line_index.line_index(diagnostic.range().start()),
diagnostic,
})
.collect();
Expand Down Expand Up @@ -94,7 +147,7 @@ pub(crate) struct LineDiagnosticsIterator<'a, T> {

impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
where
T: Ranged + Clone,
T: Diagnostic,
{
type Item = LineDiagnostics<'a, T>;

Expand All @@ -110,7 +163,7 @@ where
}
}

impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Diagnostic {}

/// All diagnostics that start on a single line of source code in one embedded Python file.
#[derive(Debug)]
Expand Down Expand Up @@ -139,11 +192,13 @@ struct DiagnosticWithLine<T> {
#[cfg(test)]
mod tests {
use crate::db::Db;
use crate::diagnostic::Diagnostic;
use ruff_db::files::system_path_to_file;
use ruff_db::source::line_index;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_source_file::OneIndexed;
use ruff_text_size::{TextRange, TextSize};
use std::borrow::Cow;

#[test]
fn sort_and_group() {
Expand All @@ -152,13 +207,18 @@ mod tests {
let file = system_path_to_file(&db, "/src/test.py").unwrap();
let lines = line_index(&db, file);

let ranges = vec![
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 sorted = super::SortedDiagnostics::new(&ranges, &lines);
let diagnostics: Vec<_> = ranges
.into_iter()
.map(|range| DummyDiagnostic { range })
.collect();

let sorted = super::SortedDiagnostics::new(diagnostics, &lines);
let grouped = sorted.iter_lines().collect::<Vec<_>>();

let [line1, line2] = &grouped[..] else {
Expand All @@ -170,4 +230,23 @@ mod tests {
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
assert_eq!(line2.diagnostics.len(), 1);
}

#[derive(Debug)]
struct DummyDiagnostic {
range: TextRange,
}

impl Diagnostic for DummyDiagnostic {
fn rule(&self) -> &str {
"dummy"
}

fn message(&self) -> Cow<str> {
"dummy".into()
}

fn range(&self) -> TextRange {
self.range
}
}
}
29 changes: 19 additions & 10 deletions crates/red_knot_test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::diagnostic::Diagnostic;
use colored::Colorize;
use parser as test_parser;
use red_knot_python_semantic::types::check_types;
Expand All @@ -7,6 +8,7 @@ use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_source_file::LineIndex;
use ruff_text_size::TextSize;
use std::path::Path;
use std::sync::Arc;

mod assertion;
mod db;
Expand Down Expand Up @@ -87,16 +89,23 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
.filter_map(|test_file| {
let parsed = parsed_module(db, test_file.file);

// TODO allow testing against code with syntax errors
assert!(
parsed.errors().is_empty(),
"Python syntax errors in {}, {}: {:?}",
test.name(),
test_file.file.path(db),
parsed.errors()
);

match matcher::match_file(db, test_file.file, check_types(db, test_file.file)) {
let mut diagnostics: Vec<Box<_>> = parsed
.errors()
.iter()
.cloned()
.map(|error| {
let diagnostic: Box<dyn Diagnostic> = Box::new(error);
diagnostic
})
.collect();

let type_diagnostics = check_types(db, test_file.file);
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
let diagnostic: Box<dyn Diagnostic> = Box::new(Arc::unwrap_or_clone(diagnostic));
diagnostic
}));

match matcher::match_file(db, test_file.file, diagnostics) {
Ok(()) => None,
Err(line_failures) => Some(FileFailures {
backtick_offset: test_file.backtick_offset,
Expand Down
43 changes: 12 additions & 31 deletions crates/red_knot_test/src/matcher.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
//! Match [`Diagnostic`]s against [`Assertion`]s and produce test failure messages for any
//! mismatches.
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
use crate::db::Db;
use crate::diagnostic::SortedDiagnostics;
use crate::diagnostic::{Diagnostic, SortedDiagnostics};
use colored::Colorize;
use red_knot_python_semantic::types::TypeCheckDiagnostic;
use ruff_db::files::File;
use ruff_db::source::{line_index, source_text, SourceText};
use ruff_source_file::{LineIndex, OneIndexed};
use ruff_text_size::Ranged;
use std::cmp::Ordering;
use std::ops::Range;
use std::sync::Arc;

#[derive(Debug, Default)]
pub(super) struct FailuresByLine {
Expand Down Expand Up @@ -55,7 +52,7 @@ pub(super) fn match_file<T>(
diagnostics: impl IntoIterator<Item = T>,
) -> Result<(), FailuresByLine>
where
T: Diagnostic + Clone,
T: Diagnostic,
{
// Parse assertions from comments in the file, and get diagnostics from the file; both
// ordered by line number.
Expand Down Expand Up @@ -126,22 +123,6 @@ where
}
}

pub(super) trait Diagnostic: Ranged {
fn rule(&self) -> &str;

fn message(&self) -> &str;
}

impl Diagnostic for Arc<TypeCheckDiagnostic> {
fn rule(&self) -> &str {
self.as_ref().rule()
}

fn message(&self) -> &str {
self.as_ref().message()
}
}

trait Unmatched {
fn unmatched(&self) -> String;
}
Expand Down Expand Up @@ -253,9 +234,9 @@ impl Matcher {
}
}

fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
fn column<T: Diagnostic>(&self, diagnostic: &T) -> OneIndexed {
self.line_index
.source_location(ranged.start(), &self.source)
.source_location(diagnostic.range().start(), &self.source)
.column
}

Expand Down Expand Up @@ -323,11 +304,13 @@ impl Matcher {
#[cfg(test)]
mod tests {
use super::FailuresByLine;
use crate::diagnostic::Diagnostic;
use ruff_db::files::system_path_to_file;
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::OneIndexed;
use ruff_text_size::{Ranged, TextRange};
use ruff_text_size::TextRange;
use std::borrow::Cow;

#[derive(Clone, Debug)]
struct TestDiagnostic {
Expand All @@ -347,18 +330,16 @@ mod tests {
}
}

impl super::Diagnostic for TestDiagnostic {
impl Diagnostic for TestDiagnostic {
fn rule(&self) -> &str {
self.rule
}

fn message(&self) -> &str {
self.message
fn message(&self) -> Cow<str> {
self.message.into()
}
}

impl Ranged for TestDiagnostic {
fn range(&self) -> ruff_text_size::TextRange {
fn range(&self) -> TextRange {
self.range
}
}
Expand Down

0 comments on commit a56ee92

Please sign in to comment.