[syntax-errors] Start detecting compile-time syntax errors (#16106)

## Summary

This PR implements the "greeter" approach for checking the AST for
syntax errors emitted by the CPython compiler. It introduces two main
infrastructural changes to support all of the compile-time errors:
1. Adds a new `semantic_errors` module to the parser crate with public
`SemanticSyntaxChecker` and `SemanticSyntaxError` types
2. Embeds a `SemanticSyntaxChecker` in the `ruff_linter::Checker` for
checking these errors in ruff

As a proof of concept, it also implements detection of two syntax
errors:
1. A reimplementation of
[`late-future-import`](https://docs.astral.sh/ruff/rules/late-future-import/)
(`F404`)
2. Detection of rebound comprehension iteration variables
(https://github.com/astral-sh/ruff/issues/14395)

## Test plan
Existing F404 tests, new inline tests in the `ruff_python_parser` crate,
and a linter CLI test showing an example of the `Message` output.

I also tested in VS Code, where `preview = false` and turning off syntax
errors both disable the new errors:


![image](https://github.com/user-attachments/assets/cf453d95-04f7-484b-8440-cb812f29d45e)

And on the playground, where `preview = false` also disables the errors:


![image](https://github.com/user-attachments/assets/a97570c4-1efa-439f-9d99-a54487dd6064)


Fixes #14395

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
Brent Westbrook 2025-03-21 14:45:25 -04:00 committed by GitHub
parent b1deab83d9
commit 2baaedda6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1601 additions and 93 deletions

View file

@ -1965,11 +1965,6 @@ impl<'a> SemanticModel<'a> {
self.flags.intersects(SemanticModelFlags::IMPORT_BOUNDARY)
}
/// Return `true` if the model has traverse past the `__future__` import boundary.
pub const fn seen_futures_boundary(&self) -> bool {
self.flags.intersects(SemanticModelFlags::FUTURES_BOUNDARY)
}
/// Return `true` if the model has traversed past the module docstring boundary.
pub const fn seen_module_docstring_boundary(&self) -> bool {
self.flags
@ -2338,21 +2333,6 @@ bitflags! {
/// ```
const IMPORT_BOUNDARY = 1 << 13;
/// The model has traversed past the `__future__` import boundary.
///
/// For example, the model could be visiting `x` in:
/// ```python
/// from __future__ import annotations
///
/// import os
///
/// x: int = 1
/// ```
///
/// Python considers it a syntax error to import from `__future__` after
/// any other non-`__future__`-importing statements.
const FUTURES_BOUNDARY = 1 << 14;
/// The model is in a file that has `from __future__ import annotations`
/// at the top of the module.
///
@ -2364,10 +2344,10 @@ bitflags! {
/// def f(x: int) -> int:
/// ...
/// ```
const FUTURE_ANNOTATIONS = 1 << 15;
const FUTURE_ANNOTATIONS = 1 << 14;
/// The model is in a Python stub file (i.e., a `.pyi` file).
const STUB_FILE = 1 << 16;
const STUB_FILE = 1 << 15;
/// `__future__`-style type annotations are enabled in this model.
/// That could be because it's a stub file,
@ -2383,7 +2363,7 @@ bitflags! {
///
/// x: int = 1
/// ```
const MODULE_DOCSTRING_BOUNDARY = 1 << 17;
const MODULE_DOCSTRING_BOUNDARY = 1 << 16;
/// The model is in a (deferred) [type parameter definition].
///
@ -2407,7 +2387,7 @@ bitflags! {
/// not when we "pass by" it when initially traversing the source tree.
///
/// [type parameter definition]: https://docs.python.org/3/reference/executionmodel.html#annotation-scopes
const TYPE_PARAM_DEFINITION = 1 << 18;
const TYPE_PARAM_DEFINITION = 1 << 17;
/// The model is in a named expression assignment.
///
@ -2415,7 +2395,7 @@ bitflags! {
/// ```python
/// if (x := 1): ...
/// ```
const NAMED_EXPRESSION_ASSIGNMENT = 1 << 19;
const NAMED_EXPRESSION_ASSIGNMENT = 1 << 18;
/// The model is in a docstring as described in [PEP 257].
///
@ -2436,7 +2416,7 @@ bitflags! {
/// ```
///
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const PEP_257_DOCSTRING = 1 << 20;
const PEP_257_DOCSTRING = 1 << 19;
/// The model is visiting the r.h.s. of a module-level `__all__` definition.
///
@ -2448,7 +2428,7 @@ bitflags! {
/// __all__ = ("bar",)
/// __all__ += ("baz,")
/// ```
const DUNDER_ALL_DEFINITION = 1 << 21;
const DUNDER_ALL_DEFINITION = 1 << 20;
/// The model is in an f-string replacement field.
///
@ -2457,7 +2437,7 @@ bitflags! {
/// ```python
/// f"first {x} second {y}"
/// ```
const F_STRING_REPLACEMENT_FIELD = 1 << 22;
const F_STRING_REPLACEMENT_FIELD = 1 << 21;
/// The model is visiting the bases tuple of a class.
///
@ -2467,11 +2447,11 @@ bitflags! {
/// class Baz(Foo, Bar):
/// pass
/// ```
const CLASS_BASE = 1 << 23;
const CLASS_BASE = 1 << 22;
/// The model is visiting a class base that was initially deferred
/// while traversing the AST. (This only happens in stub files.)
const DEFERRED_CLASS_BASE = 1 << 24;
const DEFERRED_CLASS_BASE = 1 << 23;
/// The model is in an attribute docstring.
///
@ -2496,7 +2476,7 @@ bitflags! {
/// static-analysis tools.
///
/// [PEP 257]: https://peps.python.org/pep-0257/#what-is-a-docstring
const ATTRIBUTE_DOCSTRING = 1 << 25;
const ATTRIBUTE_DOCSTRING = 1 << 24;
/// The model is in the value expression of a [PEP 613] explicit type alias.
///
@ -2508,7 +2488,7 @@ bitflags! {
/// ```
///
/// [PEP 613]: https://peps.python.org/pep-0613/
const ANNOTATED_TYPE_ALIAS = 1 << 27;
const ANNOTATED_TYPE_ALIAS = 1 << 25;
/// The model is in the value expression of a [PEP 695] type statement.
///
@ -2518,7 +2498,7 @@ bitflags! {
/// ```
///
/// [PEP 695]: https://peps.python.org/pep-0695/#generic-type-alias
const DEFERRED_TYPE_ALIAS = 1 << 28;
const DEFERRED_TYPE_ALIAS = 1 << 26;
/// The model is visiting an `assert` statement.
///
@ -2526,7 +2506,7 @@ bitflags! {
/// ```python
/// assert (y := x**2) > 42, y
/// ```
const ASSERT_STATEMENT = 1 << 29;
const ASSERT_STATEMENT = 1 << 27;
/// The model is in a [`@no_type_check`] context.
///
@ -2543,7 +2523,7 @@ bitflags! {
///
/// [no_type_check]: https://docs.python.org/3/library/typing.html#typing.no_type_check
/// [#13824]: https://github.com/astral-sh/ruff/issues/13824
const NO_TYPE_CHECK = 1 << 30;
const NO_TYPE_CHECK = 1 << 28;
/// The context is in any type annotation.
const ANNOTATION = Self::TYPING_ONLY_ANNOTATION.bits() | Self::RUNTIME_EVALUATED_ANNOTATION.bits() | Self::RUNTIME_REQUIRED_ANNOTATION.bits();