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

@ -5,6 +5,7 @@ use bitflags::bitflags;
use ruff_python_ast::{Mod, ModExpression, ModModule};
use ruff_text_size::{Ranged, TextRange, TextSize};
use crate::error::UnsupportedSyntaxError;
use crate::parser::expression::ExpressionContext;
use crate::parser::progress::{ParserProgress, TokenId};
use crate::token::TokenValue;
@ -35,6 +36,9 @@ pub(crate) struct Parser<'src> {
/// Stores all the syntax errors found during the parsing.
errors: Vec<ParseError>,
/// Stores non-fatal syntax errors found during parsing, such as version-related errors.
unsupported_syntax_errors: Vec<UnsupportedSyntaxError>,
/// Options for how the code will be parsed.
options: ParseOptions,
@ -70,6 +74,7 @@ impl<'src> Parser<'src> {
options,
source,
errors: Vec::new(),
unsupported_syntax_errors: Vec::new(),
tokens,
recovery_context: RecoveryContext::empty(),
prev_token_end: TextSize::new(0),
@ -166,6 +171,7 @@ impl<'src> Parser<'src> {
syntax,
tokens: Tokens::new(tokens),
errors: parse_errors,
unsupported_syntax_errors: self.unsupported_syntax_errors,
};
}
@ -197,6 +203,7 @@ impl<'src> Parser<'src> {
syntax,
tokens: Tokens::new(tokens),
errors: merged,
unsupported_syntax_errors: self.unsupported_syntax_errors,
}
}
@ -658,6 +665,7 @@ impl<'src> Parser<'src> {
ParserCheckpoint {
tokens: self.tokens.checkpoint(),
errors_position: self.errors.len(),
unsupported_syntax_errors_position: self.unsupported_syntax_errors.len(),
current_token_id: self.current_token_id,
prev_token_end: self.prev_token_end,
recovery_context: self.recovery_context,
@ -669,6 +677,7 @@ impl<'src> Parser<'src> {
let ParserCheckpoint {
tokens,
errors_position,
unsupported_syntax_errors_position,
current_token_id,
prev_token_end,
recovery_context,
@ -676,6 +685,8 @@ impl<'src> Parser<'src> {
self.tokens.rewind(tokens);
self.errors.truncate(errors_position);
self.unsupported_syntax_errors
.truncate(unsupported_syntax_errors_position);
self.current_token_id = current_token_id;
self.prev_token_end = prev_token_end;
self.recovery_context = recovery_context;
@ -685,6 +696,7 @@ impl<'src> Parser<'src> {
struct ParserCheckpoint {
tokens: TokenSourceCheckpoint,
errors_position: usize,
unsupported_syntax_errors_position: usize,
current_token_id: TokenId,
prev_token_end: TextSize,
recovery_context: RecoveryContext,

View file

@ -1,4 +1,4 @@
use ruff_python_ast::PySourceType;
use ruff_python_ast::{PySourceType, PythonVersion};
use crate::{AsMode, Mode};
@ -20,15 +20,28 @@ use crate::{AsMode, Mode};
///
/// let options = ParseOptions::from(PySourceType::Python);
/// ```
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct ParseOptions {
/// Specify the mode in which the code will be parsed.
pub(crate) mode: Mode,
/// Target version for detecting version-related syntax errors.
pub(crate) target_version: PythonVersion,
}
impl ParseOptions {
#[must_use]
pub fn with_target_version(mut self, target_version: PythonVersion) -> Self {
self.target_version = target_version;
self
}
}
impl From<Mode> for ParseOptions {
fn from(mode: Mode) -> Self {
Self { mode }
Self {
mode,
target_version: PythonVersion::default(),
}
}
}
@ -36,6 +49,7 @@ impl From<PySourceType> for ParseOptions {
fn from(source_type: PySourceType) -> Self {
Self {
mode: source_type.as_mode(),
target_version: PythonVersion::default(),
}
}
}

View file

@ -5,7 +5,8 @@ use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_python_ast::name::Name;
use ruff_python_ast::{
self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, Stmt, WithItem,
self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt,
WithItem,
};
use ruff_text_size::{Ranged, TextSize};
@ -16,7 +17,7 @@ use crate::parser::{
};
use crate::token::{TokenKind, TokenValue};
use crate::token_set::TokenSet;
use crate::{Mode, ParseErrorType};
use crate::{Mode, ParseErrorType, UnsupportedSyntaxError, UnsupportedSyntaxErrorKind};
use super::expression::ExpressionContext;
use super::Parenthesized;
@ -2257,11 +2258,21 @@ impl<'src> Parser<'src> {
let start = self.node_start();
self.bump(TokenKind::Match);
let match_range = self.node_range(start);
let subject = self.parse_match_subject_expression();
self.expect(TokenKind::Colon);
let cases = self.parse_match_body();
if self.options.target_version < PythonVersion::PY310 {
self.unsupported_syntax_errors.push(UnsupportedSyntaxError {
kind: UnsupportedSyntaxErrorKind::MatchBeforePy310,
range: match_range,
target_version: self.options.target_version,
});
}
ast::StmtMatch {
subject: Box::new(subject),
cases,