Skip to content

Commit 144484d

Browse files
authored
Refactor semantic syntax error scope handling (#17314)
## Summary Based on the discussion in #17298 (comment), we decided to move the scope handling out of the `SemanticSyntaxChecker` and into the `SemanticSyntaxContext` trait. This PR implements that refactor by: - Reverting all of the `Checkpoint` and `in_async_context` code in the `SemanticSyntaxChecker` - Adding four new methods to the `SemanticSyntaxContext` trait - `in_async_context`: matches `SemanticModel::in_async_context` and only detects the nearest enclosing function - `in_sync_comprehension`: uses the new `is_async` tracking on `Generator` scopes to detect any enclosing sync comprehension - `in_module_scope`: reports whether we're at the top-level scope - `in_notebook`: reports whether we're in a Jupyter notebook - In-lining the `TestContext` directly into the `SemanticSyntaxCheckerVisitor` - This allows modifying the context as the visitor traverses the AST, which wasn't possible before One potential question here is "why not add a single method returning a `Scope` or `Scopes` to the context?" The main reason is that the `Scope` type is defined in the `ruff_python_semantic` crate, which is not currently a dependency of the parser. It also doesn't appear to be used in red-knot. So it seemed best to use these more granular methods instead of trying to access `Scope` in `ruff_python_parser` (and red-knot). ## Test Plan Existing parser and linter tests.
1 parent c87e3cc commit 144484d

File tree

7 files changed

+253
-236
lines changed

7 files changed

+253
-236
lines changed

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ use std::path::Path;
2727
use itertools::Itertools;
2828
use log::debug;
2929
use ruff_python_parser::semantic_errors::{
30-
Checkpoint, SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError,
31-
SemanticSyntaxErrorKind,
30+
SemanticSyntaxChecker, SemanticSyntaxContext, SemanticSyntaxError, SemanticSyntaxErrorKind,
3231
};
3332
use rustc_hash::{FxHashMap, FxHashSet};
3433

@@ -283,7 +282,7 @@ impl<'a> Checker<'a> {
283282
last_stmt_end: TextSize::default(),
284283
docstring_state: DocstringState::default(),
285284
target_version,
286-
semantic_checker: SemanticSyntaxChecker::new(source_type),
285+
semantic_checker: SemanticSyntaxChecker::new(),
287286
semantic_errors: RefCell::default(),
288287
}
289288
}
@@ -526,14 +525,10 @@ impl<'a> Checker<'a> {
526525
self.target_version
527526
}
528527

529-
fn with_semantic_checker(
530-
&mut self,
531-
f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker) -> Checkpoint,
532-
) -> Checkpoint {
528+
fn with_semantic_checker(&mut self, f: impl FnOnce(&mut SemanticSyntaxChecker, &Checker)) {
533529
let mut checker = std::mem::take(&mut self.semantic_checker);
534-
let checkpoint = f(&mut checker, self);
530+
f(&mut checker, self);
535531
self.semantic_checker = checker;
536-
checkpoint
537532
}
538533
}
539534

@@ -597,17 +592,43 @@ impl SemanticSyntaxContext for Checker<'_> {
597592
fn future_annotations_or_stub(&self) -> bool {
598593
self.semantic.future_annotations_or_stub()
599594
}
595+
596+
fn in_async_context(&self) -> bool {
597+
self.semantic.in_async_context()
598+
}
599+
600+
fn in_sync_comprehension(&self) -> bool {
601+
for scope in self.semantic.current_scopes() {
602+
if let ScopeKind::Generator {
603+
kind:
604+
GeneratorKind::ListComprehension
605+
| GeneratorKind::DictComprehension
606+
| GeneratorKind::SetComprehension,
607+
is_async: false,
608+
} = scope.kind
609+
{
610+
return true;
611+
}
612+
}
613+
false
614+
}
615+
616+
fn in_module_scope(&self) -> bool {
617+
self.semantic.current_scope().kind.is_module()
618+
}
619+
620+
fn in_notebook(&self) -> bool {
621+
self.source_type.is_ipynb()
622+
}
600623
}
601624

602625
impl<'a> Visitor<'a> for Checker<'a> {
603626
fn visit_stmt(&mut self, stmt: &'a Stmt) {
604627
// For functions, defer semantic syntax error checks until the body of the function is
605628
// visited
606-
let checkpoint = if stmt.is_function_def_stmt() {
607-
None
608-
} else {
609-
Some(self.with_semantic_checker(|semantic, context| semantic.enter_stmt(stmt, context)))
610-
};
629+
if !stmt.is_function_def_stmt() {
630+
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
631+
}
611632

612633
// Step 0: Pre-processing
613634
self.semantic.push_node(stmt);
@@ -1210,10 +1231,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
12101231
self.semantic.flags = flags_snapshot;
12111232
self.semantic.pop_node();
12121233
self.last_stmt_end = stmt.end();
1213-
1214-
if let Some(checkpoint) = checkpoint {
1215-
self.semantic_checker.exit_stmt(checkpoint);
1216-
}
12171234
}
12181235

12191236
fn visit_annotation(&mut self, expr: &'a Expr) {
@@ -1224,8 +1241,7 @@ impl<'a> Visitor<'a> for Checker<'a> {
12241241
}
12251242

12261243
fn visit_expr(&mut self, expr: &'a Expr) {
1227-
let checkpoint =
1228-
self.with_semantic_checker(|semantic, context| semantic.enter_expr(expr, context));
1244+
self.with_semantic_checker(|semantic, context| semantic.visit_expr(expr, context));
12291245

12301246
// Step 0: Pre-processing
12311247
if self.source_type.is_stub()
@@ -1772,8 +1788,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
17721788
self.semantic.flags = flags_snapshot;
17731789
analyze::expression(expr, self);
17741790
self.semantic.pop_node();
1775-
1776-
self.semantic_checker.exit_expr(checkpoint);
17771791
}
17781792

17791793
fn visit_except_handler(&mut self, except_handler: &'a ExceptHandler) {
@@ -2012,7 +2026,10 @@ impl<'a> Checker<'a> {
20122026
// while all subsequent reads and writes are evaluated in the inner scope. In particular,
20132027
// `x` is local to `foo`, and the `T` in `y=T` skips the class scope when resolving.
20142028
self.visit_expr(&generator.iter);
2015-
self.semantic.push_scope(ScopeKind::Generator(kind));
2029+
self.semantic.push_scope(ScopeKind::Generator {
2030+
kind,
2031+
is_async: generators.iter().any(|gen| gen.is_async),
2032+
});
20162033

20172034
self.visit_expr(&generator.target);
20182035
self.semantic.flags = flags;
@@ -2618,15 +2635,12 @@ impl<'a> Checker<'a> {
26182635
unreachable!("Expected Stmt::FunctionDef")
26192636
};
26202637

2621-
let checkpoint = self
2622-
.with_semantic_checker(|semantic, context| semantic.enter_stmt(stmt, context));
2638+
self.with_semantic_checker(|semantic, context| semantic.visit_stmt(stmt, context));
26232639

26242640
self.visit_parameters(parameters);
26252641
// Set the docstring state before visiting the function body.
26262642
self.docstring_state = DocstringState::Expected(ExpectedDocstringKind::Function);
26272643
self.visit_body(body);
2628-
2629-
self.semantic_checker.exit_stmt(checkpoint);
26302644
}
26312645
}
26322646
self.semantic.restore(snapshot);

crates/ruff_linter/src/rules/pylint/rules/await_outside_async.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ pub(crate) fn await_outside_async<T: Ranged>(checker: &Checker, node: T) {
7373
// ```
7474
if matches!(
7575
checker.semantic().current_scope().kind,
76-
ScopeKind::Generator(GeneratorKind::Generator)
76+
ScopeKind::Generator {
77+
kind: GeneratorKind::Generator,
78+
..
79+
}
7780
) {
7881
return;
7982
}
Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
# parse_options: {"target-version": "3.10"}
2-
# this case fails if exit_expr doesn't run
32
async def f():
43
[_ for n in range(3)]
54
[_ async for n in range(3)]
6-
# and this fails without exit_stmt
75
async def f():
86
def g(): ...
97
[_ async for n in range(3)]

0 commit comments

Comments
 (0)