mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-04 10:48:32 +00:00
[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:  And on the playground, where `preview = false` also disables the errors:  Fixes #14395 --------- Co-authored-by: Micha Reiser <micha@reiser.io>
This commit is contained in:
parent
b1deab83d9
commit
2baaedda6c
17 changed files with 1601 additions and 93 deletions
|
@ -0,0 +1,9 @@
|
|||
[(a := 0) for a in range(0)]
|
||||
{(a := 0) for a in range(0)}
|
||||
{(a := 0): val for a in range(0)}
|
||||
{key: (a := 0) for a in range(0)}
|
||||
((a := 0) for a in range(0))
|
||||
[[(a := 0)] for a in range(0)]
|
||||
[(a := 0) for b in range (0) for a in range(0)]
|
||||
[(a := 0) for a in range (0) for b in range(0)]
|
||||
[((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
|
@ -0,0 +1 @@
|
|||
[a := 0 for x in range(0)]
|
|
@ -32,7 +32,7 @@ call((yield from x))
|
|||
|
||||
# Named expression
|
||||
call(x := 1)
|
||||
call(x := 1 for x in iter)
|
||||
call(x := 1 for i in iter)
|
||||
|
||||
# Starred expressions
|
||||
call(*x and y)
|
||||
|
|
|
@ -85,6 +85,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
|
|||
mod error;
|
||||
pub mod lexer;
|
||||
mod parser;
|
||||
pub mod semantic_errors;
|
||||
mod string;
|
||||
mod token;
|
||||
mod token_set;
|
||||
|
|
273
crates/ruff_python_parser/src/semantic_errors.rs
Normal file
273
crates/ruff_python_parser/src/semantic_errors.rs
Normal file
|
@ -0,0 +1,273 @@
|
|||
//! [`SemanticSyntaxChecker`] for AST-based syntax errors.
|
||||
//!
|
||||
//! This checker is not responsible for traversing the AST itself. Instead, its
|
||||
//! [`SemanticSyntaxChecker::visit_stmt`] and [`SemanticSyntaxChecker::visit_expr`] methods should
|
||||
//! be called in a parent `Visitor`'s `visit_stmt` and `visit_expr` methods, respectively.
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use ruff_python_ast::{
|
||||
self as ast,
|
||||
visitor::{walk_expr, Visitor},
|
||||
Expr, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
|
||||
};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SemanticSyntaxChecker {
|
||||
/// The checker has traversed past the `__future__` import boundary.
|
||||
///
|
||||
/// For example, the checker 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.
|
||||
seen_futures_boundary: bool,
|
||||
}
|
||||
|
||||
impl SemanticSyntaxChecker {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
seen_futures_boundary: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SemanticSyntaxChecker {
|
||||
fn add_error<Ctx: SemanticSyntaxContext>(
|
||||
context: &Ctx,
|
||||
kind: SemanticSyntaxErrorKind,
|
||||
range: TextRange,
|
||||
) {
|
||||
context.report_semantic_error(SemanticSyntaxError {
|
||||
kind,
|
||||
range,
|
||||
python_version: context.python_version(),
|
||||
});
|
||||
}
|
||||
|
||||
fn check_stmt<Ctx: SemanticSyntaxContext>(&self, stmt: &ast::Stmt, ctx: &Ctx) {
|
||||
if let Stmt::ImportFrom(StmtImportFrom { range, module, .. }) = stmt {
|
||||
if self.seen_futures_boundary && matches!(module.as_deref(), Some("__future__")) {
|
||||
Self::add_error(ctx, SemanticSyntaxErrorKind::LateFutureImport, *range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visit_stmt<Ctx: SemanticSyntaxContext>(&mut self, stmt: &ast::Stmt, ctx: &Ctx) {
|
||||
// update internal state
|
||||
match stmt {
|
||||
Stmt::Expr(StmtExpr { value, .. })
|
||||
if !ctx.seen_docstring_boundary() && value.is_string_literal_expr() => {}
|
||||
Stmt::ImportFrom(StmtImportFrom { module, .. }) => {
|
||||
// Allow __future__ imports until we see a non-__future__ import.
|
||||
if !matches!(module.as_deref(), Some("__future__")) {
|
||||
self.seen_futures_boundary = true;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.seen_futures_boundary = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check for errors
|
||||
self.check_stmt(stmt, ctx);
|
||||
}
|
||||
|
||||
pub fn visit_expr<Ctx: SemanticSyntaxContext>(&mut self, expr: &Expr, ctx: &Ctx) {
|
||||
match expr {
|
||||
Expr::ListComp(ast::ExprListComp {
|
||||
elt, generators, ..
|
||||
})
|
||||
| Expr::SetComp(ast::ExprSetComp {
|
||||
elt, generators, ..
|
||||
})
|
||||
| Expr::Generator(ast::ExprGenerator {
|
||||
elt, generators, ..
|
||||
}) => Self::check_generator_expr(elt, generators, ctx),
|
||||
Expr::DictComp(ast::ExprDictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
..
|
||||
}) => {
|
||||
Self::check_generator_expr(key, generators, ctx);
|
||||
Self::check_generator_expr(value, generators, ctx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a [`SyntaxErrorKind::ReboundComprehensionVariable`] if `expr` rebinds an iteration
|
||||
/// variable in `generators`.
|
||||
fn check_generator_expr<Ctx: SemanticSyntaxContext>(
|
||||
expr: &Expr,
|
||||
comprehensions: &[ast::Comprehension],
|
||||
ctx: &Ctx,
|
||||
) {
|
||||
let rebound_variables = {
|
||||
let mut visitor = ReboundComprehensionVisitor {
|
||||
comprehensions,
|
||||
rebound_variables: Vec::new(),
|
||||
};
|
||||
visitor.visit_expr(expr);
|
||||
visitor.rebound_variables
|
||||
};
|
||||
|
||||
// TODO(brent) with multiple diagnostic ranges, we could mark both the named expr (current)
|
||||
// and the name expr being rebound
|
||||
for range in rebound_variables {
|
||||
// test_err rebound_comprehension_variable
|
||||
// [(a := 0) for a in range(0)]
|
||||
// {(a := 0) for a in range(0)}
|
||||
// {(a := 0): val for a in range(0)}
|
||||
// {key: (a := 0) for a in range(0)}
|
||||
// ((a := 0) for a in range(0))
|
||||
// [[(a := 0)] for a in range(0)]
|
||||
// [(a := 0) for b in range (0) for a in range(0)]
|
||||
// [(a := 0) for a in range (0) for b in range(0)]
|
||||
// [((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
|
||||
// test_ok non_rebound_comprehension_variable
|
||||
// [a := 0 for x in range(0)]
|
||||
Self::add_error(
|
||||
ctx,
|
||||
SemanticSyntaxErrorKind::ReboundComprehensionVariable,
|
||||
range,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SemanticSyntaxChecker {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct SemanticSyntaxError {
|
||||
pub kind: SemanticSyntaxErrorKind,
|
||||
pub range: TextRange,
|
||||
pub python_version: PythonVersion,
|
||||
}
|
||||
|
||||
impl Display for SemanticSyntaxError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
SemanticSyntaxErrorKind::LateFutureImport => {
|
||||
f.write_str("__future__ imports must be at the top of the file")
|
||||
}
|
||||
SemanticSyntaxErrorKind::ReboundComprehensionVariable => {
|
||||
f.write_str("assignment expression cannot rebind comprehension variable")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum SemanticSyntaxErrorKind {
|
||||
/// Represents the use of a `__future__` import after the beginning of a file.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// from pathlib import Path
|
||||
///
|
||||
/// from __future__ import annotations
|
||||
/// ```
|
||||
///
|
||||
/// This corresponds to the [`late-future-import`] (`F404`) rule in ruff.
|
||||
///
|
||||
/// [`late-future-import`]: https://docs.astral.sh/ruff/rules/late-future-import/
|
||||
LateFutureImport,
|
||||
|
||||
/// Represents the rebinding of the iteration variable of a list, set, or dict comprehension or
|
||||
/// a generator expression.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```python
|
||||
/// [(a := 0) for a in range(0)]
|
||||
/// {(a := 0) for a in range(0)}
|
||||
/// {(a := 0): val for a in range(0)}
|
||||
/// {key: (a := 0) for a in range(0)}
|
||||
/// ((a := 0) for a in range(0))
|
||||
/// ```
|
||||
ReboundComprehensionVariable,
|
||||
}
|
||||
|
||||
/// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in
|
||||
/// a comprehension or generator expression.
|
||||
struct ReboundComprehensionVisitor<'a> {
|
||||
comprehensions: &'a [ast::Comprehension],
|
||||
rebound_variables: Vec<TextRange>,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for ReboundComprehensionVisitor<'_> {
|
||||
fn visit_expr(&mut self, expr: &Expr) {
|
||||
if let Expr::Named(ast::ExprNamed { target, .. }) = expr {
|
||||
if let Expr::Name(ast::ExprName { id, range, .. }) = &**target {
|
||||
if self.comprehensions.iter().any(|comp| {
|
||||
comp.target
|
||||
.as_name_expr()
|
||||
.is_some_and(|name| name.id == *id)
|
||||
}) {
|
||||
self.rebound_variables.push(*range);
|
||||
}
|
||||
};
|
||||
}
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SemanticSyntaxContext {
|
||||
/// Returns `true` if a module's docstring boundary has been passed.
|
||||
fn seen_docstring_boundary(&self) -> bool;
|
||||
|
||||
/// The target Python version for detecting backwards-incompatible syntax changes.
|
||||
fn python_version(&self) -> PythonVersion;
|
||||
|
||||
fn report_semantic_error(&self, error: SemanticSyntaxError);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct SemanticSyntaxCheckerVisitor<Ctx> {
|
||||
checker: SemanticSyntaxChecker,
|
||||
context: Ctx,
|
||||
}
|
||||
|
||||
impl<Ctx> SemanticSyntaxCheckerVisitor<Ctx> {
|
||||
pub fn new(context: Ctx) -> Self {
|
||||
Self {
|
||||
checker: SemanticSyntaxChecker::new(),
|
||||
context,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_context(self) -> Ctx {
|
||||
self.context
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx> Visitor<'_> for SemanticSyntaxCheckerVisitor<Ctx>
|
||||
where
|
||||
Ctx: SemanticSyntaxContext,
|
||||
{
|
||||
fn visit_stmt(&mut self, stmt: &'_ Stmt) {
|
||||
self.checker.visit_stmt(stmt, &self.context);
|
||||
ruff_python_ast::visitor::walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||
self.checker.visit_expr(expr, &self.context);
|
||||
ruff_python_ast::visitor::walk_expr(self, expr);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
use std::cell::RefCell;
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Formatter, Write};
|
||||
use std::fs;
|
||||
|
@ -5,7 +6,11 @@ use std::path::Path;
|
|||
|
||||
use ruff_annotate_snippets::{Level, Renderer, Snippet};
|
||||
use ruff_python_ast::visitor::source_order::{walk_module, SourceOrderVisitor, TraversalSignal};
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{AnyNodeRef, Mod, PythonVersion};
|
||||
use ruff_python_parser::semantic_errors::{
|
||||
SemanticSyntaxCheckerVisitor, SemanticSyntaxContext, SemanticSyntaxError,
|
||||
};
|
||||
use ruff_python_parser::{parse_unchecked, Mode, ParseErrorType, ParseOptions, Token};
|
||||
use ruff_source_file::{LineIndex, OneIndexed, SourceCode};
|
||||
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
||||
|
@ -81,6 +86,38 @@ fn test_valid_syntax(input_path: &Path) {
|
|||
writeln!(&mut output, "## AST").unwrap();
|
||||
writeln!(&mut output, "\n```\n{:#?}\n```", parsed.syntax()).unwrap();
|
||||
|
||||
let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");
|
||||
|
||||
let mut visitor = SemanticSyntaxCheckerVisitor::new(TestContext::default());
|
||||
|
||||
for stmt in parsed.suite() {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
|
||||
let semantic_syntax_errors = visitor.into_context().diagnostics.into_inner();
|
||||
|
||||
if !semantic_syntax_errors.is_empty() {
|
||||
let mut message = "Expected no semantic syntax errors for a valid program:\n".to_string();
|
||||
|
||||
let line_index = LineIndex::from_source_text(&source);
|
||||
let source_code = SourceCode::new(&source, &line_index);
|
||||
|
||||
for error in semantic_syntax_errors {
|
||||
writeln!(
|
||||
&mut message,
|
||||
"{}\n",
|
||||
CodeFrame {
|
||||
range: error.range,
|
||||
error: &ParseErrorType::OtherError(error.to_string()),
|
||||
source_code: &source_code,
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
panic!("{input_path:?}: {message}");
|
||||
}
|
||||
|
||||
insta::with_settings!({
|
||||
omit_expression => true,
|
||||
input_file => input_path,
|
||||
|
@ -99,11 +136,6 @@ fn test_invalid_syntax(input_path: &Path) {
|
|||
});
|
||||
let parsed = parse_unchecked(&source, options);
|
||||
|
||||
assert!(
|
||||
parsed.has_syntax_errors(),
|
||||
"{input_path:?}: Expected parser to generate at least one syntax error for a program containing syntax errors."
|
||||
);
|
||||
|
||||
validate_tokens(parsed.tokens(), source.text_len(), input_path);
|
||||
validate_ast(parsed.syntax(), source.text_len(), input_path);
|
||||
|
||||
|
@ -148,6 +180,38 @@ fn test_invalid_syntax(input_path: &Path) {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");
|
||||
|
||||
let mut visitor = SemanticSyntaxCheckerVisitor::new(TestContext::default());
|
||||
|
||||
for stmt in parsed.suite() {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
|
||||
let semantic_syntax_errors = visitor.into_context().diagnostics.into_inner();
|
||||
|
||||
assert!(
|
||||
parsed.has_syntax_errors() || !semantic_syntax_errors.is_empty(),
|
||||
"{input_path:?}: Expected parser to generate at least one syntax error for a program containing syntax errors."
|
||||
);
|
||||
|
||||
if !semantic_syntax_errors.is_empty() {
|
||||
writeln!(&mut output, "## Semantic Syntax Errors\n").unwrap();
|
||||
}
|
||||
|
||||
for error in semantic_syntax_errors {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{}\n",
|
||||
CodeFrame {
|
||||
range: error.range,
|
||||
error: &ParseErrorType::OtherError(error.to_string()),
|
||||
source_code: &source_code,
|
||||
}
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
insta::with_settings!({
|
||||
omit_expression => true,
|
||||
input_file => input_path,
|
||||
|
@ -393,3 +457,22 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
|
|||
self.previous = Some(node);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct TestContext {
|
||||
diagnostics: RefCell<Vec<SemanticSyntaxError>>,
|
||||
}
|
||||
|
||||
impl SemanticSyntaxContext for TestContext {
|
||||
fn seen_docstring_boundary(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn python_version(&self) -> PythonVersion {
|
||||
PythonVersion::default()
|
||||
}
|
||||
|
||||
fn report_semantic_error(&self, error: SemanticSyntaxError) {
|
||||
self.diagnostics.borrow_mut().push(error);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ fn project_root() -> PathBuf {
|
|||
|
||||
#[test]
|
||||
fn generate_inline_tests() -> Result<()> {
|
||||
let parser_dir = project_root().join("crates/ruff_python_parser/src/parser");
|
||||
let parser_dir = project_root().join("crates/ruff_python_parser/src/");
|
||||
let tests = TestCollection::try_from(parser_dir.as_path())?;
|
||||
|
||||
let mut test_files = TestFiles::default();
|
||||
|
|
|
@ -0,0 +1,903 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/err/rebound_comprehension_variable.py
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..342,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..28,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 0..28,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 2..8,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 2..3,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 7..8,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 10..27,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 14..15,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 19..27,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 19..24,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 24..27,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 25..26,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 29..57,
|
||||
value: SetComp(
|
||||
ExprSetComp {
|
||||
range: 29..57,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 31..37,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 31..32,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 36..37,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 39..56,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 43..44,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 48..56,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 48..53,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 53..56,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 54..55,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 58..91,
|
||||
value: DictComp(
|
||||
ExprDictComp {
|
||||
range: 58..91,
|
||||
key: Named(
|
||||
ExprNamed {
|
||||
range: 60..66,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 60..61,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 65..66,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
value: Name(
|
||||
ExprName {
|
||||
range: 69..72,
|
||||
id: Name("val"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 73..90,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 77..78,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 82..90,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 82..87,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 87..90,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 88..89,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 92..125,
|
||||
value: DictComp(
|
||||
ExprDictComp {
|
||||
range: 92..125,
|
||||
key: Name(
|
||||
ExprName {
|
||||
range: 93..96,
|
||||
id: Name("key"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
value: Named(
|
||||
ExprNamed {
|
||||
range: 99..105,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 99..100,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 104..105,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 107..124,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 111..112,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 116..124,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 116..121,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 121..124,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 122..123,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 126..154,
|
||||
value: Generator(
|
||||
ExprGenerator {
|
||||
range: 126..154,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 128..134,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 128..129,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 133..134,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 136..153,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 140..141,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 145..153,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 145..150,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 150..153,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 151..152,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 155..185,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 155..185,
|
||||
elt: List(
|
||||
ExprList {
|
||||
range: 156..166,
|
||||
elts: [
|
||||
Named(
|
||||
ExprNamed {
|
||||
range: 158..164,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 158..159,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 163..164,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 167..184,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 171..172,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 176..184,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 176..181,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 181..184,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 182..183,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 186..233,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 186..233,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 188..194,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 188..189,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 193..194,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 196..214,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 200..201,
|
||||
id: Name("b"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 205..214,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 205..210,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 211..214,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 212..213,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
Comprehension {
|
||||
range: 215..232,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 219..220,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 224..232,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 224..229,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 229..232,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 230..231,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 234..281,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 234..281,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 236..242,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 236..237,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 241..242,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 244..262,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 248..249,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 253..262,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 253..258,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 259..262,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 260..261,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
Comprehension {
|
||||
range: 263..280,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 267..268,
|
||||
id: Name("b"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 272..280,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 272..277,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 277..280,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 278..279,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 282..341,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 282..341,
|
||||
elt: Tuple(
|
||||
ExprTuple {
|
||||
range: 283..303,
|
||||
elts: [
|
||||
Named(
|
||||
ExprNamed {
|
||||
range: 285..291,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 285..286,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 290..291,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
Named(
|
||||
ExprNamed {
|
||||
range: 295..301,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 295..296,
|
||||
id: Name("b"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 300..301,
|
||||
value: Int(
|
||||
1,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
ctx: Load,
|
||||
parenthesized: true,
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 304..322,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 308..309,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 313..322,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 313..318,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 319..322,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 320..321,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
Comprehension {
|
||||
range: 323..340,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 327..328,
|
||||
id: Name("b"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 332..340,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 332..337,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 337..340,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 338..339,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
||||
## Semantic Syntax Errors
|
||||
|
||||
|
|
||||
1 | [(a := 0) for a in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
2 | {(a := 0) for a in range(0)}
|
||||
3 | {(a := 0): val for a in range(0)}
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | [(a := 0) for a in range(0)]
|
||||
2 | {(a := 0) for a in range(0)}
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
3 | {(a := 0): val for a in range(0)}
|
||||
4 | {key: (a := 0) for a in range(0)}
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
1 | [(a := 0) for a in range(0)]
|
||||
2 | {(a := 0) for a in range(0)}
|
||||
3 | {(a := 0): val for a in range(0)}
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
4 | {key: (a := 0) for a in range(0)}
|
||||
5 | ((a := 0) for a in range(0))
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
2 | {(a := 0) for a in range(0)}
|
||||
3 | {(a := 0): val for a in range(0)}
|
||||
4 | {key: (a := 0) for a in range(0)}
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
5 | ((a := 0) for a in range(0))
|
||||
6 | [[(a := 0)] for a in range(0)]
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
3 | {(a := 0): val for a in range(0)}
|
||||
4 | {key: (a := 0) for a in range(0)}
|
||||
5 | ((a := 0) for a in range(0))
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
6 | [[(a := 0)] for a in range(0)]
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
4 | {key: (a := 0) for a in range(0)}
|
||||
5 | ((a := 0) for a in range(0))
|
||||
6 | [[(a := 0)] for a in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
8 | [(a := 0) for a in range (0) for b in range(0)]
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
5 | ((a := 0) for a in range(0))
|
||||
6 | [[(a := 0)] for a in range(0)]
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
8 | [(a := 0) for a in range (0) for b in range(0)]
|
||||
9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
6 | [[(a := 0)] for a in range(0)]
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
8 | [(a := 0) for a in range (0) for b in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
8 | [(a := 0) for a in range (0) for b in range(0)]
|
||||
9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
|
|
||||
|
||||
|
||||
|
|
||||
7 | [(a := 0) for b in range (0) for a in range(0)]
|
||||
8 | [(a := 0) for a in range (0) for b in range(0)]
|
||||
9 | [((a := 0), (b := 1)) for a in range (0) for b in range(0)]
|
||||
| ^ Syntax Error: assignment expression cannot rebind comprehension variable
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/valid/expressions/arguments.py
|
||||
snapshot_kind: text
|
||||
---
|
||||
## AST
|
||||
|
||||
|
@ -1159,7 +1158,7 @@ Module(
|
|||
target: Name(
|
||||
ExprName {
|
||||
range: 562..563,
|
||||
id: Name("x"),
|
||||
id: Name("i"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
---
|
||||
source: crates/ruff_python_parser/tests/fixtures.rs
|
||||
input_file: crates/ruff_python_parser/resources/inline/ok/non_rebound_comprehension_variable.py
|
||||
---
|
||||
## AST
|
||||
|
||||
```
|
||||
Module(
|
||||
ModModule {
|
||||
range: 0..27,
|
||||
body: [
|
||||
Expr(
|
||||
StmtExpr {
|
||||
range: 0..26,
|
||||
value: ListComp(
|
||||
ExprListComp {
|
||||
range: 0..26,
|
||||
elt: Named(
|
||||
ExprNamed {
|
||||
range: 1..7,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 1..2,
|
||||
id: Name("a"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
value: NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 6..7,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
generators: [
|
||||
Comprehension {
|
||||
range: 8..25,
|
||||
target: Name(
|
||||
ExprName {
|
||||
range: 12..13,
|
||||
id: Name("x"),
|
||||
ctx: Store,
|
||||
},
|
||||
),
|
||||
iter: Call(
|
||||
ExprCall {
|
||||
range: 17..25,
|
||||
func: Name(
|
||||
ExprName {
|
||||
range: 17..22,
|
||||
id: Name("range"),
|
||||
ctx: Load,
|
||||
},
|
||||
),
|
||||
arguments: Arguments {
|
||||
range: 22..25,
|
||||
args: [
|
||||
NumberLiteral(
|
||||
ExprNumberLiteral {
|
||||
range: 23..24,
|
||||
value: Int(
|
||||
0,
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
keywords: [],
|
||||
},
|
||||
},
|
||||
),
|
||||
ifs: [],
|
||||
is_async: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
)
|
||||
```
|
Loading…
Add table
Add a link
Reference in a new issue