Skip to content

[red-knot] Ignore surrounding whitespace when looking for <!-- snapshot-diagnostics --> directives in mdtests #16380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 38 additions & 14 deletions crates/red_knot_test/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -420,27 +420,26 @@ impl<'s> Parser<'s> {
}

fn parse_impl(&mut self) -> anyhow::Result<()> {
const SECTION_CONFIG_SNAPSHOT: &str = "<!-- snapshot-diagnostics -->";
const SECTION_CONFIG_SNAPSHOT: &str = "snapshot-diagnostics";
const CODE_BLOCK_END: &[u8] = b"```";
const HTML_COMMENT_END: &[u8] = b"-->";

while let Some(first) = self.cursor.bump() {
match first {
'<' => {
self.explicit_path = None;
self.preceding_blank_lines = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, should these have been removed? I think so, because now when this branch is taken, we either successfully parse an HTML comment (in which case, the previous state should probably be retained) or we report an error. It might be worth an added unit test for this? Although I wonder if it's actually impossible to observe. I'm thinking about something where you have

`some/file/path.py`:

<!-- snapshot-diagnostics -->

```py
print("frob")
```

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah, good question. I think you're right, I think removing these is probably best?

I'm not entirely sure how to easily add a useful unit test for this (and doesn't seem worth spending a lot of time on), but happy to add one as a followup if you can think of something 😄

// If we want to support more comment directives, then we should
// probably just parse the directive generically first. But it's
// not clear if we'll want to add more, since comments are hidden
// from GitHub Markdown rendering.
if self
.cursor
.as_str()
.starts_with(&SECTION_CONFIG_SNAPSHOT[1..])
'<' if self.cursor.eat_char3('!', '-', '-') => {
if let Some(position) =
memchr::memmem::find(self.cursor.as_bytes(), HTML_COMMENT_END)
{
self.cursor.skip_bytes(SECTION_CONFIG_SNAPSHOT.len() - 1);
self.process_snapshot_diagnostics()?;
let html_comment = self.cursor.as_str()[..position].trim();
if html_comment == SECTION_CONFIG_SNAPSHOT {
self.process_snapshot_diagnostics()?;
}
self.cursor.skip_bytes(position + HTML_COMMENT_END.len());
} else {
bail!("Unterminated HTML comment.");
}
}

'#' => {
self.explicit_path = None;
self.preceding_blank_lines = 0;
Expand Down Expand Up @@ -1729,6 +1728,31 @@ mod tests {
);
}

#[test]
fn snapshot_diagnostic_directive_detection_ignores_whitespace() {
// A bit weird, but the fact that the parser rejects this indicates that
// we have correctly recognised both of these HTML comments as a directive to have
// snapshotting enabled for the file, which is what we want (even though they both
// contain different amounts of whitespace to the "standard" snapshot directive).
let source = dedent(
"
# Some header

<!-- snapshot-diagnostics -->
<!--snapshot-diagnostics-->

```py
x = 1
```
",
);
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
assert_eq!(
err.to_string(),
"Section config to enable snapshotting diagnostics should appear at most once.",
);
}

#[test]
fn section_directive_must_appear_before_config() {
let source = dedent(
Expand Down
14 changes: 14 additions & 0 deletions crates/ruff_python_trivia/src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,20 @@ impl<'a> Cursor<'a> {
}
}

/// Eats the next two characters if they are `c1` and `c2`. Does not
/// consume any input otherwise, even if the first character matches.
pub fn eat_char3(&mut self, c1: char, c2: char, c3: char) -> bool {
let mut chars = self.chars.clone();
if chars.next() == Some(c1) && chars.next() == Some(c2) && chars.next() == Some(c3) {
self.bump();
self.bump();
self.bump();
true
} else {
false
}
}

pub fn eat_char_back(&mut self, c: char) -> bool {
if self.last() == c {
self.bump_back();
Expand Down
Loading