Skip to content
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

Allow passing ParseOptions to inline tests #16357

Merged
merged 70 commits into from
Feb 27, 2025
Merged
Show file tree
Hide file tree
Changes from 66 commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
e871194
add unused Parser::syntax_errors field
ntBre Feb 10, 2025
591063f
add hard-coded python version and detect `match`
ntBre Feb 10, 2025
e628e66
make PythonVersion more public, convert SyntaxError in red-knot
ntBre Feb 10, 2025
36ee9cd
process SyntaxErrors in ruff
ntBre Feb 10, 2025
611bda9
pass target version at the very end
ntBre Feb 10, 2025
bfb3bb5
pass tests
ntBre Feb 10, 2025
a6b8e34
add ruff test case
ntBre Feb 10, 2025
288821d
detect late future imports in the parser
ntBre Feb 10, 2025
ed67b01
pass f404 tests
ntBre Feb 10, 2025
fbf8765
check if rules are enabled before converting to diagnostics
ntBre Feb 10, 2025
5768f71
add a todo about duplicate diagnostics
ntBre Feb 10, 2025
2ecf3f2
clippy
ntBre Feb 10, 2025
ef858bf
update to use ast::PythonVersion and Span
ntBre Feb 14, 2025
d817289
remove LateFutureImport detection from the parser
ntBre Feb 15, 2025
6e4b956
tidy up
ntBre Feb 15, 2025
1805a65
clean up after rebase
ntBre Feb 19, 2025
cf77087
add ParseOptions::target_version
ntBre Feb 19, 2025
3c67b03
filter syntax errors when parsing, store version on SyntaxError
ntBre Feb 19, 2025
09ccf65
make ParseOptions Clone
ntBre Feb 19, 2025
a6bc0db
pass ParseOptions::target_version in the linter
ntBre Feb 19, 2025
49a48f2
pass ParseOptions::target_version in red-knot
ntBre Feb 19, 2025
79e6322
ignore version-related syntax errors when fuzzing
ntBre Feb 19, 2025
12d04b0
Red-knot no longer panics!
AlexWaygood Feb 21, 2025
efb9c2a
Fix fuzzing script and fix Rust formatting
AlexWaygood Feb 21, 2025
ee30486
Revert "ignore version-related syntax errors when fuzzing"
ntBre Feb 21, 2025
c8a22c5
pass check_file_skips_type_checking_when by initializing settings
ntBre Feb 21, 2025
380aaff
fix clippy lint about &impl ToString
ntBre Feb 21, 2025
80be7f7
pass version to new parsed_module call
ntBre Feb 21, 2025
6b69bb5
use LinterSettings::resolve_target_version
ntBre Feb 24, 2025
cd02f61
add ok test for match
ntBre Feb 24, 2025
cd90966
gate syntax error reporting behind preview
ntBre Feb 24, 2025
db77d9e
add docs for SyntaxError
ntBre Feb 24, 2025
1210a2f
use PythonVersion Display
ntBre Feb 24, 2025
8c4cf8c
impl Display instead of separate `message` method
ntBre Feb 24, 2025
a2f70cd
remove unused match
ntBre Feb 24, 2025
5a7906d
rename version to minimum_version
ntBre Feb 24, 2025
63e4a5a
mdtest version-related errors in red-knot
ntBre Feb 24, 2025
f3e5636
pass existing tests using `match`
ntBre Feb 24, 2025
3b93520
just use InvalidSyntax variant for version errors
ntBre Feb 25, 2025
16f1ae2
use SyntaxError::minimum_version
ntBre Feb 25, 2025
3dbb7f7
delete unused SyntaxErrorKind::as_str
ntBre Feb 25, 2025
be1a6fd
SyntaxError -> UnsupportedSyntaxError
ntBre Feb 25, 2025
1714b60
SyntaxErrorKind -> UnsupportedSyntaxErrorKind
ntBre Feb 25, 2025
418dd0f
rename fields and methods too
ntBre Feb 25, 2025
d78463c
move target_version out of loop
ntBre Feb 25, 2025
c672d22
only mark `match` for diagnostic
ntBre Feb 25, 2025
1a363ab
update is_valid docs
ntBre Feb 25, 2025
8035b13
update as_result and into_result docs too
ntBre Feb 25, 2025
bbb4bc4
always include unsupported_syntax_errors
ntBre Feb 25, 2025
18d9b6c
check for unsupported syntax errors in ruff_server
ntBre Feb 25, 2025
9980a6a
pass target_version to check_path
ntBre Feb 25, 2025
45c3b67
revert red-knot changes
ntBre Feb 25, 2025
8b14483
new -> added
ntBre Feb 25, 2025
76d507b
add checkpoint for unsupported_syntax_errors
ntBre Feb 25, 2025
dfcc911
Allow passing `ParseOptions` to inline tests
ntBre Feb 24, 2025
a0cb2e8
clippy
ntBre Feb 24, 2025
5f935e3
use an options pragma
ntBre Feb 25, 2025
4fe6788
revert test installation changes
ntBre Feb 25, 2025
e3b770b
don't print empty Errors section, update snap
ntBre Feb 25, 2025
72d9ea1
update snap after rebase
ntBre Feb 25, 2025
ef8c37e
activate serde with a feature
ntBre Feb 25, 2025
ae800ec
fix is_valid check for test_valid_syntax
ntBre Feb 25, 2025
d76ce1a
fix error printing for test_valid_syntax
ntBre Feb 25, 2025
8304f75
default to latest python to avoid breaking existing tests
ntBre Feb 25, 2025
45101ce
use latest instead of 3.13
ntBre Feb 25, 2025
e2db37d
Merge branch 'main' into brent/parser-tests
ntBre Feb 26, 2025
e82b83a
delete duplicate diagnostics.chain
ntBre Feb 26, 2025
f854172
use kebab-case
ntBre Feb 26, 2025
7329128
delete outdated comment
ntBre Feb 26, 2025
62b43a7
move serde to dev-dependency, work around with Json* types
ntBre Feb 26, 2025
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

5 changes: 5 additions & 0 deletions crates/ruff_python_parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ bstr = { workspace = true }
compact_str = { workspace = true }
memchr = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true, optional = true }
static_assertions = { workspace = true }
unicode-ident = { workspace = true }
unicode_names2 = { workspace = true }
Expand All @@ -33,7 +34,11 @@ ruff_source_file = { workspace = true }

anyhow = { workspace = true }
insta = { workspace = true, features = ["glob"] }
serde_json = { workspace = true }
walkdir = { workspace = true }

[features]
serde = ["dep:serde", "ruff_python_ast/serde"]

[lints]
workspace = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target_version": "3.9" }
match 2:
case 1:
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# parse_options: { "target_version": "3.10" }
match 2:
case 1:
pass
5 changes: 5 additions & 0 deletions crates/ruff_python_parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,11 @@ impl FusedIterator for TokenIterWithContext<'_> {}
///
/// The mode argument specifies in what way code must be parsed.
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serde",
derive(serde::Deserialize),
serde(rename_all = "lowercase")
)]
pub enum Mode {
/// The code consists of a sequence of statements.
Module,
Expand Down
8 changes: 8 additions & 0 deletions crates/ruff_python_parser/src/parser/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ use crate::{AsMode, Mode};
/// let options = ParseOptions::from(PySourceType::Python);
/// ```
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
pub struct ParseOptions {
/// Specify the mode in which the code will be parsed.
#[cfg_attr(feature = "serde", serde(default = "default_mode"))]
pub(crate) mode: Mode,
/// Target version for detecting version-related syntax errors.
#[cfg_attr(feature = "serde", serde(default))]
pub(crate) target_version: PythonVersion,
}

Expand Down Expand Up @@ -53,3 +56,8 @@ impl From<PySourceType> for ParseOptions {
}
}
}

#[cfg(feature = "serde")]
fn default_mode() -> Mode {
Mode::Module
}
12 changes: 12 additions & 0 deletions crates/ruff_python_parser/src/parser/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2265,6 +2265,18 @@ impl<'src> Parser<'src> {

let cases = self.parse_match_body();

// test_err match_before_py310
// # parse_options: { "target_version": "3.9" }
// match 2:
// case 1:
// pass

// test_ok match_after_py310
// # parse_options: { "target_version": "3.10" }
// match 2:
// case 1:
// pass

if self.options.target_version < PythonVersion::PY310 {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
kind: UnsupportedSyntaxErrorKind::MatchBeforePy310,
Expand Down
70 changes: 63 additions & 7 deletions crates/ruff_python_parser/tests/fixtures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::path::Path;

use ruff_annotate_snippets::{Level, Renderer, Snippet};
use ruff_python_ast::visitor::source_order::{walk_module, SourceOrderVisitor, TraversalSignal};
use ruff_python_ast::{AnyNodeRef, Mod};
use ruff_python_ast::{AnyNodeRef, Mod, PythonVersion};
use ruff_python_parser::{parse_unchecked, Mode, ParseErrorType, ParseOptions, Token};
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
Expand Down Expand Up @@ -34,9 +34,14 @@ fn inline_err() {
/// Snapshots the AST.
fn test_valid_syntax(input_path: &Path) {
let source = fs::read_to_string(input_path).expect("Expected test file to exist");
let parsed = parse_unchecked(&source, ParseOptions::from(Mode::Module));
let options = extract_options(&source).unwrap_or_else(|| {
ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest())
});
let parsed = parse_unchecked(&source, options);

if !parsed.is_valid() {
let is_valid = parsed.is_valid() && parsed.unsupported_syntax_errors().is_empty();

if !is_valid {
let line_index = LineIndex::from_source_text(&source);
let source_code = SourceCode::new(&source, &line_index);

Expand All @@ -55,6 +60,19 @@ fn test_valid_syntax(input_path: &Path) {
.unwrap();
}

for error in parsed.unsupported_syntax_errors() {
writeln!(
&mut message,
"{}\n",
CodeFrame {
range: error.range,
error: &ParseErrorType::OtherError(error.to_string()),
source_code: &source_code,
}
)
.unwrap();
}

panic!("{input_path:?}: {message}");
}

Expand All @@ -78,10 +96,15 @@ fn test_valid_syntax(input_path: &Path) {
/// Snapshots the AST and the error messages.
fn test_invalid_syntax(input_path: &Path) {
let source = fs::read_to_string(input_path).expect("Expected test file to exist");
let parsed = parse_unchecked(&source, ParseOptions::from(Mode::Module));
let options = extract_options(&source).unwrap_or_else(|| {
ParseOptions::from(Mode::Module).with_target_version(PythonVersion::latest())
});
let parsed = parse_unchecked(&source, options);

let is_valid = parsed.is_valid() && parsed.unsupported_syntax_errors().is_empty();

assert!(
!parsed.is_valid(),
!is_valid,
"{input_path:?}: Expected parser to generate at least one syntax error for a program containing syntax errors."
);

Expand All @@ -92,11 +115,13 @@ fn test_invalid_syntax(input_path: &Path) {
writeln!(&mut output, "## AST").unwrap();
writeln!(&mut output, "\n```\n{:#?}\n```", parsed.syntax()).unwrap();

writeln!(&mut output, "## Errors\n").unwrap();

let line_index = LineIndex::from_source_text(&source);
let source_code = SourceCode::new(&source, &line_index);

if !parsed.errors().is_empty() {
writeln!(&mut output, "## Errors\n").unwrap();
}

for error in parsed.errors() {
writeln!(
&mut output,
Expand All @@ -110,6 +135,23 @@ fn test_invalid_syntax(input_path: &Path) {
.unwrap();
}

if !parsed.unsupported_syntax_errors().is_empty() {
writeln!(&mut output, "## Unsupported Syntax Errors\n").unwrap();
}

for error in parsed.unsupported_syntax_errors() {
writeln!(
&mut output,
"{}\n",
CodeFrame {
range: error.range,
error: &ParseErrorType::OtherError(error.to_string()),
source_code: &source_code,
}
)
.unwrap();
}

insta::with_settings!({
omit_expression => true,
input_file => input_path,
Expand All @@ -119,6 +161,20 @@ fn test_invalid_syntax(input_path: &Path) {
});
}

/// Extract [`ParseOptions`] from an initial pragma line, if present.
///
/// For example,
///
/// ```python
/// # parse_options: { "target_version": "3.10" }
/// def f(): ...
fn extract_options(source: &str) -> Option<ParseOptions> {
// extract options from pragma on the first line
let header = source.lines().next()?;
let (_label, options) = header.split_once("# parse_options: ")?;
serde_json::from_str(options.trim()).ok()
}

// Test that is intentionally ignored by default.
// Use it for quickly debugging a parser issue.
#[test]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/match_before_py310.py
---
## AST

```
Module(
ModModule {
range: 0..79,
body: [
Match(
StmtMatch {
range: 45..78,
subject: NumberLiteral(
ExprNumberLiteral {
range: 51..52,
value: Int(
2,
),
},
),
cases: [
MatchCase {
range: 58..78,
pattern: MatchValue(
PatternMatchValue {
range: 63..64,
value: NumberLiteral(
ExprNumberLiteral {
range: 63..64,
value: Int(
1,
),
},
),
},
),
guard: None,
body: [
Pass(
StmtPass {
range: 74..78,
},
),
],
},
],
},
),
],
},
)
```
## Unsupported Syntax Errors

|
1 | # parse_options: { "target_version": "3.9" }
2 | match 2:
| ^^^^^ Syntax Error: Cannot use `match` statement on Python 3.9 (syntax was added in Python 3.10)
3 | case 1:
4 | pass
|
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
---
source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/ok/match_after_py310.py
---
## AST

```
Module(
ModModule {
range: 0..80,
body: [
Match(
StmtMatch {
range: 46..79,
subject: NumberLiteral(
ExprNumberLiteral {
range: 52..53,
value: Int(
2,
),
},
),
cases: [
MatchCase {
range: 59..79,
pattern: MatchValue(
PatternMatchValue {
range: 64..65,
value: NumberLiteral(
ExprNumberLiteral {
range: 64..65,
value: Int(
1,
),
},
),
},
),
guard: None,
body: [
Pass(
StmtPass {
range: 75..79,
},
),
],
},
],
},
),
],
},
)
```
16 changes: 16 additions & 0 deletions crates/ruff_server/src/lint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,22 @@ pub(crate) fn check(
.flatten(),
);

let lsp_diagnostics = lsp_diagnostics.chain(
show_syntax_errors
.then(|| {
parsed.unsupported_syntax_errors().iter().map(|error| {
unsupported_syntax_error_to_lsp_diagnostic(
error,
&source_kind,
locator.to_index(),
encoding,
)
})
})
.into_iter()
.flatten(),
);

if let Some(notebook) = query.as_notebook() {
for (index, diagnostic) in lsp_diagnostics {
let Some(uri) = notebook.cell_uri_by_index(index) else {
Expand Down
Loading