mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
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:   --------- Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
This commit is contained in:
parent
b39a4ad01d
commit
78806361fd
14 changed files with 356 additions and 37 deletions
|
@ -10,10 +10,10 @@ use rustc_hash::FxHashMap;
|
|||
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::{ModModule, PySourceType};
|
||||
use ruff_python_ast::{ModModule, PySourceType, PythonVersion};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_index::Indexer;
|
||||
use ruff_python_parser::{ParseError, Parsed};
|
||||
use ruff_python_parser::{ParseError, ParseOptions, Parsed, UnsupportedSyntaxError};
|
||||
use ruff_source_file::SourceFileBuilder;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
|
@ -71,6 +71,7 @@ pub fn check_path(
|
|||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
parsed: &Parsed<ModModule>,
|
||||
target_version: PythonVersion,
|
||||
) -> Vec<Diagnostic> {
|
||||
// Aggregate all diagnostics.
|
||||
let mut diagnostics = vec![];
|
||||
|
@ -104,8 +105,6 @@ pub fn check_path(
|
|||
));
|
||||
}
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
// Run the filesystem-based rules.
|
||||
if settings
|
||||
.rules
|
||||
|
@ -335,7 +334,8 @@ pub fn add_noqa_to_path(
|
|||
settings: &LinterSettings,
|
||||
) -> Result<usize> {
|
||||
// Parse once.
|
||||
let parsed = ruff_python_parser::parse_unchecked_source(source_kind.source_code(), source_type);
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
let parsed = parse_unchecked_source(source_kind, source_type, target_version);
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(source_kind.source_code());
|
||||
|
@ -367,6 +367,7 @@ pub fn add_noqa_to_path(
|
|||
source_kind,
|
||||
source_type,
|
||||
&parsed,
|
||||
target_version,
|
||||
);
|
||||
|
||||
// Add any missing `# noqa` pragmas.
|
||||
|
@ -393,7 +394,8 @@ pub fn lint_only(
|
|||
source_type: PySourceType,
|
||||
source: ParseSource,
|
||||
) -> LinterResult {
|
||||
let parsed = source.into_parsed(source_kind, source_type);
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
let parsed = source.into_parsed(source_kind, source_type, target_version);
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(source_kind.source_code());
|
||||
|
@ -425,12 +427,20 @@ pub fn lint_only(
|
|||
source_kind,
|
||||
source_type,
|
||||
&parsed,
|
||||
target_version,
|
||||
);
|
||||
|
||||
let syntax_errors = if settings.preview.is_enabled() {
|
||||
parsed.unsupported_syntax_errors()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
LinterResult {
|
||||
messages: diagnostics_to_messages(
|
||||
diagnostics,
|
||||
parsed.errors(),
|
||||
syntax_errors,
|
||||
path,
|
||||
&locator,
|
||||
&directives,
|
||||
|
@ -443,6 +453,7 @@ pub fn lint_only(
|
|||
fn diagnostics_to_messages(
|
||||
diagnostics: Vec<Diagnostic>,
|
||||
parse_errors: &[ParseError],
|
||||
unsupported_syntax_errors: &[UnsupportedSyntaxError],
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
directives: &Directives,
|
||||
|
@ -461,6 +472,9 @@ fn diagnostics_to_messages(
|
|||
parse_errors
|
||||
.iter()
|
||||
.map(|parse_error| Message::from_parse_error(parse_error, locator, file.deref().clone()))
|
||||
.chain(unsupported_syntax_errors.iter().map(|syntax_error| {
|
||||
Message::from_unsupported_syntax_error(syntax_error, file.deref().clone())
|
||||
}))
|
||||
.chain(diagnostics.into_iter().map(|diagnostic| {
|
||||
let noqa_offset = directives.noqa_line_for.resolve(diagnostic.start());
|
||||
Message::from_diagnostic(diagnostic, file.deref().clone(), noqa_offset)
|
||||
|
@ -491,11 +505,12 @@ pub fn lint_fix<'a>(
|
|||
// Track whether the _initial_ source code is valid syntax.
|
||||
let mut is_valid_syntax = false;
|
||||
|
||||
let target_version = settings.resolve_target_version(path);
|
||||
|
||||
// Continuously fix until the source code stabilizes.
|
||||
loop {
|
||||
// Parse once.
|
||||
let parsed =
|
||||
ruff_python_parser::parse_unchecked_source(transformed.source_code(), source_type);
|
||||
let parsed = parse_unchecked_source(&transformed, source_type, target_version);
|
||||
|
||||
// Map row and column locations to byte slices (lazily).
|
||||
let locator = Locator::new(transformed.source_code());
|
||||
|
@ -527,6 +542,7 @@ pub fn lint_fix<'a>(
|
|||
&transformed,
|
||||
source_type,
|
||||
&parsed,
|
||||
target_version,
|
||||
);
|
||||
|
||||
if iterations == 0 {
|
||||
|
@ -573,11 +589,18 @@ pub fn lint_fix<'a>(
|
|||
report_failed_to_converge_error(path, transformed.source_code(), &diagnostics);
|
||||
}
|
||||
|
||||
let syntax_errors = if settings.preview.is_enabled() {
|
||||
parsed.unsupported_syntax_errors()
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
|
||||
return Ok(FixerResult {
|
||||
result: LinterResult {
|
||||
messages: diagnostics_to_messages(
|
||||
diagnostics,
|
||||
parsed.errors(),
|
||||
syntax_errors,
|
||||
path,
|
||||
&locator,
|
||||
&directives,
|
||||
|
@ -680,16 +703,35 @@ pub enum ParseSource {
|
|||
impl ParseSource {
|
||||
/// Consumes the [`ParseSource`] and returns the parsed [`Parsed`], parsing the source code if
|
||||
/// necessary.
|
||||
fn into_parsed(self, source_kind: &SourceKind, source_type: PySourceType) -> Parsed<ModModule> {
|
||||
fn into_parsed(
|
||||
self,
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
target_version: PythonVersion,
|
||||
) -> Parsed<ModModule> {
|
||||
match self {
|
||||
ParseSource::None => {
|
||||
ruff_python_parser::parse_unchecked_source(source_kind.source_code(), source_type)
|
||||
}
|
||||
ParseSource::None => parse_unchecked_source(source_kind, source_type, target_version),
|
||||
ParseSource::Precomputed(parsed) => parsed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`ruff_python_parser::parse_unchecked_source`] but with an additional [`PythonVersion`]
|
||||
/// argument.
|
||||
fn parse_unchecked_source(
|
||||
source_kind: &SourceKind,
|
||||
source_type: PySourceType,
|
||||
target_version: PythonVersion,
|
||||
) -> Parsed<ModModule> {
|
||||
let options = ParseOptions::from(source_type).with_target_version(target_version);
|
||||
// SAFETY: Safe because `PySourceType` always parses to a `ModModule`. See
|
||||
// `ruff_python_parser::parse_unchecked_source`. We use `parse_unchecked` (and thus
|
||||
// have to unwrap) in order to pass the `PythonVersion` via `ParseOptions`.
|
||||
ruff_python_parser::parse_unchecked(source_kind.source_code(), options)
|
||||
.try_into_module()
|
||||
.expect("PySourceType always parses into a module")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::Path;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue