Start detecting version-related syntax errors in the parser (#16090)

## Summary

This PR builds on the changes in #16220 to pass a target Python version
to the parser. It also adds the `Parser::unsupported_syntax_errors` field, which
collects version-related syntax errors while parsing. These syntax
errors are then turned into `Message`s in ruff (in preview mode).

This PR only detects one syntax error (`match` statement before Python
3.10), but it has been pretty quick to extend to several other simple
errors (see #16308 for example).

## Test Plan

The current tests are CLI tests in the linter crate, but these could be
supplemented with inline parser tests after #16357.

I also tested the display of these syntax errors in VS Code:


![image](https://github.com/user-attachments/assets/062b4441-740e-46c3-887c-a954049ef26e)

![image](https://github.com/user-attachments/assets/101f55b8-146c-4d59-b6b0-922f19bcd0fa)

---------

Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
Brent Westbrook 2025-02-25 23:03:48 -05:00 committed by GitHub
parent b39a4ad01d
commit 78806361fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 356 additions and 37 deletions

View file

@ -16,7 +16,7 @@ use ruff_notebook::NotebookError;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::ParseError;
use ruff_python_parser::{ParseError, ParseOptions};
use ruff_python_trivia::textwrap::dedent;
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::Ranged;
@ -110,7 +110,11 @@ pub(crate) fn test_contents<'a>(
settings: &LinterSettings,
) -> (Vec<Message>, Cow<'a, SourceKind>) {
let source_type = PySourceType::from(path);
let parsed = ruff_python_parser::parse_unchecked_source(source_kind.source_code(), source_type);
let target_version = settings.resolve_target_version(path);
let options = ParseOptions::from(source_type).with_target_version(target_version);
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), options.clone())
.try_into_module()
.expect("PySourceType always parses into a module");
let locator = Locator::new(source_kind.source_code());
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
let indexer = Indexer::from_tokens(parsed.tokens(), locator.contents());
@ -134,6 +138,7 @@ pub(crate) fn test_contents<'a>(
source_kind,
source_type,
&parsed,
target_version,
);
let source_has_errors = !parsed.is_valid();
@ -174,7 +179,9 @@ pub(crate) fn test_contents<'a>(
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
let parsed =
ruff_python_parser::parse_unchecked_source(transformed.source_code(), source_type);
ruff_python_parser::parse_unchecked(transformed.source_code(), options.clone())
.try_into_module()
.expect("PySourceType always parses into a module");
let locator = Locator::new(transformed.source_code());
let stylist = Stylist::from_tokens(parsed.tokens(), locator.contents());
let indexer = Indexer::from_tokens(parsed.tokens(), locator.contents());
@ -197,6 +204,7 @@ pub(crate) fn test_contents<'a>(
&transformed,
source_type,
&parsed,
target_version,
);
if !parsed.is_valid() && !source_has_errors {