mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-31 00:43:47 +00:00
Add mdtest support for files with invalid syntax (#14126)
This commit is contained in:
parent
4ece8e5c1e
commit
a56ee9268e
9 changed files with 137 additions and 70 deletions
|
@ -51,6 +51,10 @@ repos:
|
||||||
- id: blacken-docs
|
- id: blacken-docs
|
||||||
args: ["--pyi", "--line-length", "130"]
|
args: ["--pyi", "--line-length", "130"]
|
||||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||||
|
exclude: |
|
||||||
|
(?x)^(
|
||||||
|
.*?invalid(_.+)_syntax.md
|
||||||
|
)$
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- black==24.10.0
|
- black==24.10.0
|
||||||
|
|
||||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2173,6 +2173,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"ruff_db",
|
"ruff_db",
|
||||||
"ruff_index",
|
"ruff_index",
|
||||||
|
"ruff_python_parser",
|
||||||
"ruff_python_trivia",
|
"ruff_python_trivia",
|
||||||
"ruff_source_file",
|
"ruff_source_file",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
```
|
|
@ -8,7 +8,7 @@ use std::sync::Arc;
|
||||||
use crate::types::{ClassLiteralType, Type};
|
use crate::types::{ClassLiteralType, Type};
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||||
pub struct TypeCheckDiagnostic {
|
pub struct TypeCheckDiagnostic {
|
||||||
// TODO: Don't use string keys for rules
|
// TODO: Don't use string keys for rules
|
||||||
pub(super) rule: String,
|
pub(super) rule: String,
|
||||||
|
|
|
@ -5061,27 +5061,6 @@ mod tests {
|
||||||
Ok(())
|
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]
|
#[test]
|
||||||
fn basic_comprehension() -> anyhow::Result<()> {
|
fn basic_comprehension() -> anyhow::Result<()> {
|
||||||
let mut db = setup_db();
|
let mut db = setup_db();
|
||||||
|
@ -5424,7 +5403,7 @@ mod tests {
|
||||||
return 42
|
return 42
|
||||||
|
|
||||||
class Iterable:
|
class Iterable:
|
||||||
def __iter__(self) -> Iterator:
|
def __iter__(self) -> Iterator: ...
|
||||||
|
|
||||||
x = [*NotIterable()]
|
x = [*NotIterable()]
|
||||||
y = [*Iterable()]
|
y = [*Iterable()]
|
||||||
|
|
|
@ -15,6 +15,7 @@ red_knot_python_semantic = { workspace = true }
|
||||||
red_knot_vendored = { workspace = true }
|
red_knot_vendored = { workspace = true }
|
||||||
ruff_db = { workspace = true }
|
ruff_db = { workspace = true }
|
||||||
ruff_index = { workspace = true }
|
ruff_index = { workspace = true }
|
||||||
|
ruff_python_parser = { workspace = true }
|
||||||
ruff_python_trivia = { workspace = true }
|
ruff_python_trivia = { workspace = true }
|
||||||
ruff_source_file = { workspace = true }
|
ruff_source_file = { workspace = true }
|
||||||
ruff_text_size = { workspace = true }
|
ruff_text_size = { workspace = true }
|
||||||
|
|
|
@ -2,10 +2,63 @@
|
||||||
//!
|
//!
|
||||||
//! We don't assume that we will get the diagnostics in source order.
|
//! 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_source_file::{LineIndex, OneIndexed};
|
||||||
use ruff_text_size::Ranged;
|
use ruff_text_size::{Ranged, TextRange};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::ops::{Deref, Range};
|
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.
|
/// 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
|
/// The diagnostics are kept in a flat vector, sorted by line number. A separate vector of
|
||||||
|
@ -19,13 +72,13 @@ pub(crate) struct SortedDiagnostics<T> {
|
||||||
|
|
||||||
impl<T> SortedDiagnostics<T>
|
impl<T> SortedDiagnostics<T>
|
||||||
where
|
where
|
||||||
T: Ranged + Clone,
|
T: Diagnostic,
|
||||||
{
|
{
|
||||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||||
let mut diagnostics: Vec<_> = diagnostics
|
let mut diagnostics: Vec<_> = diagnostics
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|diagnostic| DiagnosticWithLine {
|
.map(|diagnostic| DiagnosticWithLine {
|
||||||
line_number: line_index.line_index(diagnostic.start()),
|
line_number: line_index.line_index(diagnostic.range().start()),
|
||||||
diagnostic,
|
diagnostic,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
@ -94,7 +147,7 @@ pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||||
|
|
||||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||||
where
|
where
|
||||||
T: Ranged + Clone,
|
T: Diagnostic,
|
||||||
{
|
{
|
||||||
type Item = LineDiagnostics<'a, T>;
|
type Item = LineDiagnostics<'a, T>;
|
||||||
|
|
||||||
|
@ -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.
|
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -139,11 +192,13 @@ struct DiagnosticWithLine<T> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
|
use crate::diagnostic::Diagnostic;
|
||||||
use ruff_db::files::system_path_to_file;
|
use ruff_db::files::system_path_to_file;
|
||||||
use ruff_db::source::line_index;
|
use ruff_db::source::line_index;
|
||||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||||
use ruff_source_file::OneIndexed;
|
use ruff_source_file::OneIndexed;
|
||||||
use ruff_text_size::{TextRange, TextSize};
|
use ruff_text_size::{TextRange, TextSize};
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sort_and_group() {
|
fn sort_and_group() {
|
||||||
|
@ -152,13 +207,18 @@ mod tests {
|
||||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||||
let lines = line_index(&db, file);
|
let lines = line_index(&db, file);
|
||||||
|
|
||||||
let ranges = vec![
|
let ranges = [
|
||||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
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 grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||||
|
|
||||||
let [line1, line2] = &grouped[..] else {
|
let [line1, line2] = &grouped[..] else {
|
||||||
|
@ -170,4 +230,23 @@ mod tests {
|
||||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||||
assert_eq!(line2.diagnostics.len(), 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::diagnostic::Diagnostic;
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use parser as test_parser;
|
use parser as test_parser;
|
||||||
use red_knot_python_semantic::types::check_types;
|
use red_knot_python_semantic::types::check_types;
|
||||||
|
@ -7,6 +8,7 @@ use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||||
use ruff_source_file::LineIndex;
|
use ruff_source_file::LineIndex;
|
||||||
use ruff_text_size::TextSize;
|
use ruff_text_size::TextSize;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
mod assertion;
|
mod assertion;
|
||||||
mod db;
|
mod db;
|
||||||
|
@ -87,16 +89,23 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||||
.filter_map(|test_file| {
|
.filter_map(|test_file| {
|
||||||
let parsed = parsed_module(db, test_file.file);
|
let parsed = parsed_module(db, test_file.file);
|
||||||
|
|
||||||
// TODO allow testing against code with syntax errors
|
let mut diagnostics: Vec<Box<_>> = parsed
|
||||||
assert!(
|
.errors()
|
||||||
parsed.errors().is_empty(),
|
.iter()
|
||||||
"Python syntax errors in {}, {}: {:?}",
|
.cloned()
|
||||||
test.name(),
|
.map(|error| {
|
||||||
test_file.file.path(db),
|
let diagnostic: Box<dyn Diagnostic> = Box::new(error);
|
||||||
parsed.errors()
|
diagnostic
|
||||||
);
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
match matcher::match_file(db, test_file.file, check_types(db, test_file.file)) {
|
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,
|
Ok(()) => None,
|
||||||
Err(line_failures) => Some(FileFailures {
|
Err(line_failures) => Some(FileFailures {
|
||||||
backtick_offset: test_file.backtick_offset,
|
backtick_offset: test_file.backtick_offset,
|
||||||
|
|
|
@ -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.
|
//! mismatches.
|
||||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||||
use crate::db::Db;
|
use crate::db::Db;
|
||||||
use crate::diagnostic::SortedDiagnostics;
|
use crate::diagnostic::{Diagnostic, SortedDiagnostics};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
|
||||||
use ruff_db::files::File;
|
use ruff_db::files::File;
|
||||||
use ruff_db::source::{line_index, source_text, SourceText};
|
use ruff_db::source::{line_index, source_text, SourceText};
|
||||||
use ruff_source_file::{LineIndex, OneIndexed};
|
use ruff_source_file::{LineIndex, OneIndexed};
|
||||||
use ruff_text_size::Ranged;
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub(super) struct FailuresByLine {
|
pub(super) struct FailuresByLine {
|
||||||
|
@ -55,7 +52,7 @@ pub(super) fn match_file<T>(
|
||||||
diagnostics: impl IntoIterator<Item = T>,
|
diagnostics: impl IntoIterator<Item = T>,
|
||||||
) -> Result<(), FailuresByLine>
|
) -> Result<(), FailuresByLine>
|
||||||
where
|
where
|
||||||
T: Diagnostic + Clone,
|
T: Diagnostic,
|
||||||
{
|
{
|
||||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||||
// ordered by line number.
|
// ordered by line number.
|
||||||
|
@ -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 {
|
trait Unmatched {
|
||||||
fn unmatched(&self) -> String;
|
fn unmatched(&self) -> String;
|
||||||
}
|
}
|
||||||
|
@ -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
|
self.line_index
|
||||||
.source_location(ranged.start(), &self.source)
|
.source_location(diagnostic.range().start(), &self.source)
|
||||||
.column
|
.column
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,11 +304,13 @@ impl Matcher {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::FailuresByLine;
|
use super::FailuresByLine;
|
||||||
|
use crate::diagnostic::Diagnostic;
|
||||||
use ruff_db::files::system_path_to_file;
|
use ruff_db::files::system_path_to_file;
|
||||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||||
use ruff_python_trivia::textwrap::dedent;
|
use ruff_python_trivia::textwrap::dedent;
|
||||||
use ruff_source_file::OneIndexed;
|
use ruff_source_file::OneIndexed;
|
||||||
use ruff_text_size::{Ranged, TextRange};
|
use ruff_text_size::TextRange;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
struct TestDiagnostic {
|
struct TestDiagnostic {
|
||||||
|
@ -347,18 +330,16 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::Diagnostic for TestDiagnostic {
|
impl Diagnostic for TestDiagnostic {
|
||||||
fn rule(&self) -> &str {
|
fn rule(&self) -> &str {
|
||||||
self.rule
|
self.rule
|
||||||
}
|
}
|
||||||
|
|
||||||
fn message(&self) -> &str {
|
fn message(&self) -> Cow<str> {
|
||||||
self.message
|
self.message.into()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Ranged for TestDiagnostic {
|
fn range(&self) -> TextRange {
|
||||||
fn range(&self) -> ruff_text_size::TextRange {
|
|
||||||
self.range
|
self.range
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue