[red-knot] Error out when an mdtest code block is unterminated (#14965)

## Summary

Resolves #14934.

## Test Plan

Added a unit test.
This commit is contained in:
InSync 2024-12-14 12:51:21 +07:00 committed by GitHub
parent 224c8438bd
commit a80e934838
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,7 +7,8 @@ use rustc_hash::{FxHashMap, FxHashSet};
use ruff_index::{newtype_index, IndexVec}; use ruff_index::{newtype_index, IndexVec};
use ruff_python_trivia::Cursor; use ruff_python_trivia::Cursor;
use ruff_text_size::{TextLen, TextSize}; use ruff_source_file::LineRanges;
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::config::MarkdownTestConfig; use crate::config::MarkdownTestConfig;
@ -156,8 +157,14 @@ static HEADER_RE: LazyLock<Regex> =
/// Matches a code block fenced by triple backticks, possibly with language and `key=val` /// Matches a code block fenced by triple backticks, possibly with language and `key=val`
/// configuration items following the opening backticks (in the "tag string" of the code block). /// configuration items following the opening backticks (in the "tag string" of the code block).
static CODE_RE: LazyLock<Regex> = LazyLock::new(|| { static CODE_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^```(?<lang>(?-u:\w)+)?(?<config>(?: +\S+)*)\s*\n(?<code>(?:.|\n)*?)\n?```\s*\n?") Regex::new(
.unwrap() r"(?x)
^```(?<lang>(?-u:\w)+)?(?<config>(?:\x20+\S+)*)\s*\n
(?<code>(?:.|\n)*?)\n?
(?<end>```|\z)
",
)
.unwrap()
}); });
#[derive(Debug)] #[derive(Debug)]
@ -202,6 +209,7 @@ struct Parser<'s> {
/// The unparsed remainder of the Markdown source. /// The unparsed remainder of the Markdown source.
cursor: Cursor<'s>, cursor: Cursor<'s>,
source: &'s str,
source_len: TextSize, source_len: TextSize,
/// Stack of ancestor sections. /// Stack of ancestor sections.
@ -225,6 +233,7 @@ impl<'s> Parser<'s> {
}); });
Self { Self {
sections, sections,
source,
files: IndexVec::default(), files: IndexVec::default(),
cursor: Cursor::new(source), cursor: Cursor::new(source),
source_len: source.text_len(), source_len: source.text_len(),
@ -328,6 +337,13 @@ impl<'s> Parser<'s> {
// We never pop the implicit root section. // We never pop the implicit root section.
let section = self.stack.top(); let section = self.stack.top();
if captures.name("end").unwrap().is_empty() {
let code_block_start = self.cursor.token_len();
let line = self.source.count_lines(TextRange::up_to(code_block_start)) + 1;
return Err(anyhow::anyhow!("Unterminated code block at line {line}."));
}
let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default(); let mut config: FxHashMap<&'s str, &'s str> = FxHashMap::default();
if let Some(config_match) = captures.name("config") { if let Some(config_match) = captures.name("config") {
@ -664,6 +680,38 @@ mod tests {
assert_eq!(file.code, "x = 10"); assert_eq!(file.code, "x = 10");
} }
#[test]
fn unterminated_code_block_1() {
let source = dedent(
"
```
x = 1
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Unterminated code block at line 2.");
}
#[test]
fn unterminated_code_block_2() {
let source = dedent(
"
## A well-fenced block
```
y = 2
```
## A not-so-well-fenced block
```
x = 1
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(err.to_string(), "Unterminated code block at line 10.");
}
#[test] #[test]
fn no_header_inside_test() { fn no_header_inside_test() {
let source = dedent( let source = dedent(