Skip to content

Commit dfcc911

Browse files
committed
Allow passing ParseOptions to inline tests
The changes here are based on the similar behavior in biome's [`collect_tests`](https://github.com/biomejs/biome/blob/b9f8ffea9967b098ec4c8bf74fa96826a879f043/xtask/codegen/src/parser_tests.rs#L159) function, which allows a syntax for inline test headers like ``` label language name [options] ``` Before this PR, we only allowed `label name`, where `label` is either `test_err` or `test_ok`. This PR adds support for an optional, trailing `options` field, corresponding to JSON-serialized `ParseOptions`. These get written to a `*.options.json` file alongside the inline test script and read when that test is run. This is currently stacked on #16090 so that I had something to test.
1 parent 76d507b commit dfcc911

13 files changed

+206
-13
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff_python_parser/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ license = { workspace = true }
1313
[lib]
1414

1515
[dependencies]
16-
ruff_python_ast = { workspace = true }
16+
ruff_python_ast = { workspace = true, features = ["serde"] }
1717
ruff_python_trivia = { workspace = true }
1818
ruff_text_size = { workspace = true }
1919

@@ -22,6 +22,7 @@ bstr = { workspace = true }
2222
compact_str = { workspace = true }
2323
memchr = { workspace = true }
2424
rustc-hash = { workspace = true }
25+
serde = { workspace = true }
2526
static_assertions = { workspace = true }
2627
unicode-ident = { workspace = true }
2728
unicode_names2 = { workspace = true }
@@ -33,6 +34,7 @@ ruff_source_file = { workspace = true }
3334

3435
anyhow = { workspace = true }
3536
insta = { workspace = true, features = ["glob"] }
37+
serde_json = { workspace = true }
3638
walkdir = { workspace = true }
3739

3840
[lints]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "target_version": "3.9" }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
match 2:
2+
case 1:
3+
pass
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{ "target_version": "3.10" }
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
match 2:
2+
case 1:
3+
pass

crates/ruff_python_parser/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -660,7 +660,8 @@ impl FusedIterator for TokenIterWithContext<'_> {}
660660
/// Control in the different modes by which a source file can be parsed.
661661
///
662662
/// The mode argument specifies in what way code must be parsed.
663-
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
663+
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, serde::Deserialize)]
664+
#[serde(rename_all = "lowercase")]
664665
pub enum Mode {
665666
/// The code consists of a sequence of statements.
666667
Module,

crates/ruff_python_parser/src/parser/options.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,13 @@ use crate::{AsMode, Mode};
2020
///
2121
/// let options = ParseOptions::from(PySourceType::Python);
2222
/// ```
23-
#[derive(Clone, Debug)]
23+
#[derive(Clone, Debug, serde::Deserialize)]
2424
pub struct ParseOptions {
2525
/// Specify the mode in which the code will be parsed.
26+
#[serde(default = "default_mode")]
2627
pub(crate) mode: Mode,
2728
/// Target version for detecting version-related syntax errors.
29+
#[serde(default)]
2830
pub(crate) target_version: PythonVersion,
2931
}
3032

@@ -53,3 +55,7 @@ impl From<PySourceType> for ParseOptions {
5355
}
5456
}
5557
}
58+
59+
fn default_mode() -> Mode {
60+
Mode::Module
61+
}

crates/ruff_python_parser/src/parser/statement.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2265,6 +2265,16 @@ impl<'src> Parser<'src> {
22652265

22662266
let cases = self.parse_match_body();
22672267

2268+
// test_err match_before_py310 { "target_version": "3.9" }
2269+
// match 2:
2270+
// case 1:
2271+
// pass
2272+
2273+
// test_ok match_after_py310 { "target_version": "3.10" }
2274+
// match 2:
2275+
// case 1:
2276+
// pass
2277+
22682278
if self.options.target_version < PythonVersion::PY310 {
22692279
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
22702280
kind: UnsupportedSyntaxErrorKind::MatchBeforePy310,

crates/ruff_python_parser/tests/fixtures.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::path::Path;
55

66
use ruff_annotate_snippets::{Level, Renderer, Snippet};
77
use ruff_python_ast::visitor::source_order::{walk_module, SourceOrderVisitor, TraversalSignal};
8-
use ruff_python_ast::{AnyNodeRef, Mod};
8+
use ruff_python_ast::{AnyNodeRef, Mod, PythonVersion};
99
use ruff_python_parser::{parse_unchecked, Mode, ParseErrorType, ParseOptions, Token};
1010
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
1111
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
@@ -33,8 +33,13 @@ fn inline_err() {
3333
/// Asserts that the parser generates no syntax errors for a valid program.
3434
/// Snapshots the AST.
3535
fn test_valid_syntax(input_path: &Path) {
36+
let options_path = input_path.with_extension("options.json");
37+
let options = fs::read_to_string(options_path)
38+
.ok()
39+
.and_then(|s| serde_json::from_str(&s).ok())
40+
.unwrap_or_else(|| ParseOptions::from(Mode::Module));
3641
let source = fs::read_to_string(input_path).expect("Expected test file to exist");
37-
let parsed = parse_unchecked(&source, ParseOptions::from(Mode::Module));
42+
let parsed = parse_unchecked(&source, options);
3843

3944
if !parsed.is_valid() {
4045
let line_index = LineIndex::from_source_text(&source);
@@ -77,11 +82,20 @@ fn test_valid_syntax(input_path: &Path) {
7782
/// Assert that the parser generates at least one syntax error for the given input file.
7883
/// Snapshots the AST and the error messages.
7984
fn test_invalid_syntax(input_path: &Path) {
85+
let options_path = input_path.with_extension("options.json");
86+
let options = fs::read_to_string(options_path)
87+
.ok()
88+
.and_then(|s| serde_json::from_str(&s).ok())
89+
.unwrap_or_else(|| {
90+
ParseOptions::from(Mode::Module).with_target_version(PythonVersion::PY313)
91+
});
8092
let source = fs::read_to_string(input_path).expect("Expected test file to exist");
81-
let parsed = parse_unchecked(&source, ParseOptions::from(Mode::Module));
93+
let parsed = parse_unchecked(&source, options);
94+
95+
let is_valid = parsed.is_valid() && parsed.unsupported_syntax_errors().is_empty();
8296

8397
assert!(
84-
!parsed.is_valid(),
98+
!is_valid,
8599
"{input_path:?}: Expected parser to generate at least one syntax error for a program containing syntax errors."
86100
);
87101

@@ -110,6 +124,23 @@ fn test_invalid_syntax(input_path: &Path) {
110124
.unwrap();
111125
}
112126

127+
if !parsed.unsupported_syntax_errors().is_empty() {
128+
writeln!(&mut output, "## Unsupported Syntax Errors\n").unwrap();
129+
}
130+
131+
for error in parsed.unsupported_syntax_errors() {
132+
writeln!(
133+
&mut output,
134+
"{}\n",
135+
CodeFrame {
136+
range: error.range,
137+
error: &ParseErrorType::OtherError(error.to_string()),
138+
source_code: &source_code,
139+
}
140+
)
141+
.unwrap();
142+
}
143+
113144
insta::with_settings!({
114145
omit_expression => true,
115146
input_file => input_path,

0 commit comments

Comments
 (0)