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

@ -24,7 +24,7 @@ use ruff_linter::{
use ruff_notebook::Notebook;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::ParseError;
use ruff_python_parser::{ParseError, ParseOptions, UnsupportedSyntaxError};
use ruff_source_file::LineIndex;
use ruff_text_size::{Ranged, TextRange};
@ -94,8 +94,18 @@ pub(crate) fn check(
let source_type = query.source_type();
let target_version = if let Some(path) = &document_path {
settings.linter.resolve_target_version(path)
} else {
settings.linter.unresolved_target_version
};
let parse_options = ParseOptions::from(source_type).with_target_version(target_version);
// Parse once.
let parsed = ruff_python_parser::parse_unchecked_source(source_kind.source_code(), source_type);
let parsed = ruff_python_parser::parse_unchecked(source_kind.source_code(), parse_options)
.try_into_module()
.expect("PySourceType always parses to a ModModule");
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
@ -122,6 +132,7 @@ pub(crate) fn check(
&source_kind,
source_type,
&parsed,
target_version,
);
let noqa_edits = generate_noqa_edits(
@ -164,14 +175,25 @@ pub(crate) fn check(
let lsp_diagnostics = lsp_diagnostics.chain(
show_syntax_errors
.then(|| {
parsed.errors().iter().map(|parse_error| {
parse_error_to_lsp_diagnostic(
parse_error,
&source_kind,
locator.to_index(),
encoding,
)
})
parsed
.errors()
.iter()
.map(|parse_error| {
parse_error_to_lsp_diagnostic(
parse_error,
&source_kind,
locator.to_index(),
encoding,
)
})
.chain(parsed.unsupported_syntax_errors().iter().map(|error| {
unsupported_syntax_error_to_lsp_diagnostic(
error,
&source_kind,
locator.to_index(),
encoding,
)
}))
})
.into_iter()
.flatten(),
@ -350,6 +372,45 @@ fn parse_error_to_lsp_diagnostic(
)
}
fn unsupported_syntax_error_to_lsp_diagnostic(
unsupported_syntax_error: &UnsupportedSyntaxError,
source_kind: &SourceKind,
index: &LineIndex,
encoding: PositionEncoding,
) -> (usize, lsp_types::Diagnostic) {
let range: lsp_types::Range;
let cell: usize;
if let Some(notebook_index) = source_kind.as_ipy_notebook().map(Notebook::index) {
NotebookRange { cell, range } = unsupported_syntax_error.range.to_notebook_range(
source_kind.source_code(),
index,
notebook_index,
encoding,
);
} else {
cell = usize::default();
range = unsupported_syntax_error
.range
.to_range(source_kind.source_code(), index, encoding);
}
(
cell,
lsp_types::Diagnostic {
range,
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
tags: None,
code: None,
code_description: None,
source: Some(DIAGNOSTIC_NAME.into()),
message: format!("SyntaxError: {unsupported_syntax_error}"),
related_information: None,
data: None,
},
)
}
fn diagnostic_edit_range(
range: TextRange,
source_kind: &SourceKind,