[red-knot] Detect version-related syntax errors (#16379)

## Summary
This PR extends version-related syntax error detection to red-knot. The
main changes here are:

1. Passing `ParseOptions` specifying a `PythonVersion` to parser calls
2. Adding a `python_version` method to the `Db` trait to make this
possible
3. Converting `UnsupportedSyntaxError`s to `Diagnostic`s
4. Updating existing mdtests  to avoid unrelated syntax errors

My initial draft of (1) and (2) in #16090 instead tried passing a
`PythonVersion` down to every parser call, but @MichaReiser suggested
the `Db` approach instead
[here](https://github.com/astral-sh/ruff/pull/16090#discussion_r1969198407),
and I think it turned out much nicer.

All of the new `python_version` methods look like this:

```rust
fn python_version(&self) -> ruff_python_ast::PythonVersion {
    Program::get(self).python_version(self)
}
```

with the exception of the `TestDb` in `ruff_db`, which hard-codes
`PythonVersion::latest()`.

## Test Plan

Existing mdtests, plus a new mdtest to see at least one of the new
diagnostics.
This commit is contained in:
Brent Westbrook 2025-04-17 14:00:30 -04:00 committed by GitHub
parent d2ebfd6ed7
commit 9c47b6dbb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
43 changed files with 353 additions and 14 deletions

View file

@ -832,3 +832,16 @@ pub fn create_parse_diagnostic(file: File, err: &ruff_python_parser::ParseError)
diag.annotate(Annotation::primary(span).message(&err.error));
diag
}
/// Creates a `Diagnostic` from an unsupported syntax error.
///
/// See [`create_parse_diagnostic`] for more details.
pub fn create_unsupported_syntax_diagnostic(
file: File,
err: &ruff_python_parser::UnsupportedSyntaxError,
) -> Diagnostic {
let mut diag = Diagnostic::new(DiagnosticId::InvalidSyntax, Severity::Error, "");
let span = Span::from(file).with_range(err.range);
diag.annotate(Annotation::primary(span).message(err.to_string()));
diag
}

View file

@ -1,5 +1,6 @@
use std::hash::BuildHasherDefault;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHasher;
use crate::files::Files;
@ -27,6 +28,7 @@ pub trait Db: salsa::Database {
fn vendored(&self) -> &VendoredFileSystem;
fn system(&self) -> &dyn System;
fn files(&self) -> &Files;
fn python_version(&self) -> PythonVersion;
}
/// Trait for upcasting a reference to a base trait object.
@ -107,6 +109,10 @@ mod tests {
fn files(&self) -> &Files {
&self.files
}
fn python_version(&self) -> ruff_python_ast::PythonVersion {
ruff_python_ast::PythonVersion::latest()
}
}
impl DbWithTestSystem for TestDb {

View file

@ -3,7 +3,7 @@ use std::ops::Deref;
use std::sync::Arc;
use ruff_python_ast::ModModule;
use ruff_python_parser::{parse_unchecked_source, Parsed};
use ruff_python_parser::{parse_unchecked, ParseOptions, Parsed};
use crate::files::File;
use crate::source::source_text;
@ -27,7 +27,13 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
let source = source_text(db, file);
let ty = file.source_type(db);
ParsedModule::new(parse_unchecked_source(&source, ty))
let target_version = db.python_version();
let options = ParseOptions::from(ty).with_target_version(target_version);
let parsed = parse_unchecked(&source, options)
.try_into_module()
.expect("PySourceType always parses into a module");
ParsedModule::new(parsed)
}
/// Cheap cloneable wrapper around the parsed module.