Skip to content

Commit ef91b4f

Browse files
committed
Recursively visit deferred AST nodes
1 parent da275b8 commit ef91b4f

File tree

10 files changed

+92
-48
lines changed

10 files changed

+92
-48
lines changed

crates/ruff_linter/resources/test/fixtures/pyflakes/F401_21.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
"""Test: ensure that we visit type definitions and lambdas recursively."""
2+
13
from __future__ import annotations
24

35
from collections import Counter
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
"""Test: ensure that we visit type definitions and lambdas recursively."""
2+
3+
from __future__ import annotations
4+
5+
import re
6+
import os
7+
from typing import cast
8+
9+
cast(lambda: re.match, 1)
10+
11+
cast(lambda: cast(lambda: os.path, 1), 1)

crates/ruff_linter/src/checkers/ast/analyze/deferred_for_loops.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::rules::{flake8_bugbear, flake8_simplify, perflint, pyupgrade, refurb}
66

77
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
88
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
9-
while !checker.deferred.for_loops.is_empty() {
10-
let for_loops = std::mem::take(&mut checker.deferred.for_loops);
9+
while !checker.analyze.for_loops.is_empty() {
10+
let for_loops = std::mem::take(&mut checker.analyze.for_loops);
1111
for snapshot in for_loops {
1212
checker.semantic.restore(snapshot);
1313

crates/ruff_linter/src/checkers/ast/analyze/deferred_lambdas.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ use crate::rules::{flake8_pie, pylint, refurb};
66

77
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
88
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
9-
while !checker.deferred.lambdas.is_empty() {
10-
let lambdas = std::mem::take(&mut checker.deferred.lambdas);
9+
while !checker.analyze.lambdas.is_empty() {
10+
let lambdas = std::mem::take(&mut checker.analyze.lambdas);
1111
for snapshot in lambdas {
1212
checker.semantic.restore(snapshot);
1313

crates/ruff_linter/src/checkers/ast/analyze/deferred_scopes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
7777
};
7878

7979
let mut diagnostics: Vec<Diagnostic> = vec![];
80-
for scope_id in checker.deferred.scopes.iter().rev().copied() {
80+
for scope_id in checker.analyze.scopes.iter().rev().copied() {
8181
let scope = &checker.semantic.scopes[scope_id];
8282

8383
if checker.enabled(Rule::UndefinedLocal) {

crates/ruff_linter/src/checkers/ast/analyze/statement.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1264,7 +1264,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
12641264
Rule::UnusedLoopControlVariable,
12651265
Rule::YieldInForLoop,
12661266
]) {
1267-
checker.deferred.for_loops.push(checker.semantic.snapshot());
1267+
checker.analyze.for_loops.push(checker.semantic.snapshot());
12681268
}
12691269
if checker.enabled(Rule::LoopVariableOverridesIterator) {
12701270
flake8_bugbear::rules::loop_variable_overrides_iterator(checker, target, iter);

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,34 @@ use ruff_python_ast::Expr;
22
use ruff_python_semantic::{ScopeId, Snapshot};
33
use ruff_text_size::TextRange;
44

5-
/// A collection of AST nodes that are deferred for later analysis.
6-
/// Used to, e.g., store functions, whose bodies shouldn't be analyzed until all
7-
/// module-level definitions have been analyzed.
5+
/// A collection of AST nodes that are deferred for later visitation. Used to, e.g., store
6+
/// functions, whose bodies shouldn't be visited until all module-level definitions have been
7+
/// visited.
88
#[derive(Debug, Default)]
9-
pub(crate) struct Deferred<'a> {
10-
pub(crate) scopes: Vec<ScopeId>,
9+
pub(crate) struct Visit<'a> {
1110
pub(crate) string_type_definitions: Vec<(TextRange, &'a str, Snapshot)>,
1211
pub(crate) future_type_definitions: Vec<(&'a Expr, Snapshot)>,
1312
pub(crate) type_param_definitions: Vec<(&'a Expr, Snapshot)>,
1413
pub(crate) functions: Vec<Snapshot>,
1514
pub(crate) lambdas: Vec<Snapshot>,
15+
}
16+
17+
impl Visit<'_> {
18+
/// Returns `true` if there are no deferred nodes.
19+
pub(crate) fn is_empty(&self) -> bool {
20+
self.string_type_definitions.is_empty()
21+
&& self.future_type_definitions.is_empty()
22+
&& self.type_param_definitions.is_empty()
23+
&& self.functions.is_empty()
24+
&& self.lambdas.is_empty()
25+
}
26+
}
27+
28+
/// A collection of AST nodes to be analyzed after the AST traversal. Used to, e.g., store
29+
/// all `for` loops, so that they can be analyzed after the entire AST has been visited.
30+
#[derive(Debug, Default)]
31+
pub(crate) struct Analyze {
32+
pub(crate) scopes: Vec<ScopeId>,
33+
pub(crate) lambdas: Vec<Snapshot>,
1634
pub(crate) for_loops: Vec<Snapshot>,
1735
}

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

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,13 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
5252
use ruff_python_semantic::analyze::{imports, typing, visibility};
5353
use ruff_python_semantic::{
5454
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
55-
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
56-
StarImport, SubmoduleImport,
55+
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
56+
SubmoduleImport,
5757
};
5858
use ruff_python_stdlib::builtins::{IPYTHON_BUILTINS, MAGIC_GLOBALS, PYTHON_BUILTINS};
5959
use ruff_source_file::{Locator, OneIndexed, SourceRow};
6060

6161
use crate::checkers::ast::annotation::AnnotationContext;
62-
use crate::checkers::ast::deferred::Deferred;
6362
use crate::docstrings::extraction::ExtractionTarget;
6463
use crate::importer::Importer;
6564
use crate::noqa::NoqaMapping;
@@ -105,8 +104,10 @@ pub(crate) struct Checker<'a> {
105104
importer: Importer<'a>,
106105
/// The [`SemanticModel`], built up over the course of the AST traversal.
107106
semantic: SemanticModel<'a>,
108-
/// A set of deferred nodes to be processed after the current traversal (e.g., function bodies).
109-
deferred: Deferred<'a>,
107+
/// A set of deferred nodes to be visited after the current traversal (e.g., function bodies).
108+
visit: deferred::Visit<'a>,
109+
/// A set of deferred nodes to be analyzed after the AST traversal (e.g., `for` loops).
110+
analyze: deferred::Analyze,
110111
/// The cumulative set of diagnostics computed across all lint rules.
111112
pub(crate) diagnostics: Vec<Diagnostic>,
112113
/// The list of names already seen by flake8-bugbear diagnostics, to avoid duplicate violations..
@@ -145,7 +146,8 @@ impl<'a> Checker<'a> {
145146
indexer,
146147
importer,
147148
semantic: SemanticModel::new(&settings.typing_modules, path, module),
148-
deferred: Deferred::default(),
149+
visit: deferred::Visit::default(),
150+
analyze: deferred::Analyze::default(),
149151
diagnostics: Vec::default(),
150152
flake8_bugbear_seen: Vec::default(),
151153
cell_offsets,
@@ -596,7 +598,7 @@ where
596598
self.semantic.push_scope(ScopeKind::Function(function_def));
597599
self.semantic.flags -= SemanticModelFlags::EXCEPTION_HANDLER;
598600

599-
self.deferred.functions.push(self.semantic.snapshot());
601+
self.visit.functions.push(self.semantic.snapshot());
600602

601603
// Extract any global bindings from the function body.
602604
if let Some(globals) = Globals::from_body(body) {
@@ -652,7 +654,7 @@ where
652654
if let Some(type_params) = type_params {
653655
self.visit_type_params(type_params);
654656
}
655-
self.deferred
657+
self.visit
656658
.type_param_definitions
657659
.push((value, self.semantic.snapshot()));
658660
self.semantic.pop_scope();
@@ -785,7 +787,7 @@ where
785787
match stmt {
786788
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
787789
let scope_id = self.semantic.scope_id;
788-
self.deferred.scopes.push(scope_id);
790+
self.analyze.scopes.push(scope_id);
789791
self.semantic.pop_scope(); // Function scope
790792
self.semantic.pop_definition();
791793
self.semantic.pop_scope(); // Type parameter scope
@@ -798,7 +800,7 @@ where
798800
}
799801
Stmt::ClassDef(ast::StmtClassDef { name, .. }) => {
800802
let scope_id = self.semantic.scope_id;
801-
self.deferred.scopes.push(scope_id);
803+
self.analyze.scopes.push(scope_id);
802804
self.semantic.pop_scope(); // Class scope
803805
self.semantic.pop_definition();
804806
self.semantic.pop_scope(); // Type parameter scope
@@ -835,13 +837,13 @@ where
835837
&& self.semantic.future_annotations()
836838
{
837839
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = expr {
838-
self.deferred.string_type_definitions.push((
840+
self.visit.string_type_definitions.push((
839841
expr.range(),
840842
value.to_str(),
841843
self.semantic.snapshot(),
842844
));
843845
} else {
844-
self.deferred
846+
self.visit
845847
.future_type_definitions
846848
.push((expr, self.semantic.snapshot()));
847849
}
@@ -945,7 +947,8 @@ where
945947
}
946948

947949
self.semantic.push_scope(ScopeKind::Lambda(lambda));
948-
self.deferred.lambdas.push(self.semantic.snapshot());
950+
self.visit.lambdas.push(self.semantic.snapshot());
951+
self.analyze.lambdas.push(self.semantic.snapshot());
949952
}
950953
Expr::IfExp(ast::ExprIfExp {
951954
test,
@@ -1252,7 +1255,7 @@ where
12521255
}
12531256
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
12541257
if self.semantic.in_type_definition() && !self.semantic.in_typing_literal() {
1255-
self.deferred.string_type_definitions.push((
1258+
self.visit.string_type_definitions.push((
12561259
expr.range(),
12571260
value.to_str(),
12581261
self.semantic.snapshot(),
@@ -1273,7 +1276,7 @@ where
12731276
| Expr::ListComp(_)
12741277
| Expr::DictComp(_)
12751278
| Expr::SetComp(_) => {
1276-
self.deferred.scopes.push(self.semantic.scope_id);
1279+
self.analyze.scopes.push(self.semantic.scope_id);
12771280
self.semantic.pop_scope();
12781281
}
12791282
_ => {}
@@ -1447,7 +1450,7 @@ where
14471450
bound: Some(bound), ..
14481451
}) = type_param
14491452
{
1450-
self.deferred
1453+
self.visit
14511454
.type_param_definitions
14521455
.push((bound, self.semantic.snapshot()));
14531456
}
@@ -1809,8 +1812,8 @@ impl<'a> Checker<'a> {
18091812

18101813
fn visit_deferred_future_type_definitions(&mut self) {
18111814
let snapshot = self.semantic.snapshot();
1812-
while !self.deferred.future_type_definitions.is_empty() {
1813-
let type_definitions = std::mem::take(&mut self.deferred.future_type_definitions);
1815+
while !self.visit.future_type_definitions.is_empty() {
1816+
let type_definitions = std::mem::take(&mut self.visit.future_type_definitions);
18141817
for (expr, snapshot) in type_definitions {
18151818
self.semantic.restore(snapshot);
18161819

@@ -1824,8 +1827,8 @@ impl<'a> Checker<'a> {
18241827

18251828
fn visit_deferred_type_param_definitions(&mut self) {
18261829
let snapshot = self.semantic.snapshot();
1827-
while !self.deferred.type_param_definitions.is_empty() {
1828-
let type_params = std::mem::take(&mut self.deferred.type_param_definitions);
1830+
while !self.visit.type_param_definitions.is_empty() {
1831+
let type_params = std::mem::take(&mut self.visit.type_param_definitions);
18291832
for (type_param, snapshot) in type_params {
18301833
self.semantic.restore(snapshot);
18311834

@@ -1839,8 +1842,8 @@ impl<'a> Checker<'a> {
18391842

18401843
fn visit_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena<Expr>) {
18411844
let snapshot = self.semantic.snapshot();
1842-
while !self.deferred.string_type_definitions.is_empty() {
1843-
let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions);
1845+
while !self.visit.string_type_definitions.is_empty() {
1846+
let type_definitions = std::mem::take(&mut self.visit.string_type_definitions);
18441847
for (range, value, snapshot) in type_definitions {
18451848
if let Ok((expr, kind)) =
18461849
parse_type_annotation(value, range, self.locator.contents())
@@ -1887,8 +1890,8 @@ impl<'a> Checker<'a> {
18871890

18881891
fn visit_deferred_functions(&mut self) {
18891892
let snapshot = self.semantic.snapshot();
1890-
while !self.deferred.functions.is_empty() {
1891-
let deferred_functions = std::mem::take(&mut self.deferred.functions);
1893+
while !self.visit.functions.is_empty() {
1894+
let deferred_functions = std::mem::take(&mut self.visit.functions);
18921895
for snapshot in deferred_functions {
18931896
self.semantic.restore(snapshot);
18941897

@@ -1906,11 +1909,12 @@ impl<'a> Checker<'a> {
19061909
self.semantic.restore(snapshot);
19071910
}
19081911

1912+
/// Visit all deferred lambdas. Returns a list of snapshots, such that the caller can restore
1913+
/// the semantic model to the state it was in before visiting the deferred lambdas.
19091914
fn visit_deferred_lambdas(&mut self) {
19101915
let snapshot = self.semantic.snapshot();
1911-
let mut deferred: Vec<Snapshot> = Vec::with_capacity(self.deferred.lambdas.len());
1912-
while !self.deferred.lambdas.is_empty() {
1913-
let lambdas = std::mem::take(&mut self.deferred.lambdas);
1916+
while !self.visit.lambdas.is_empty() {
1917+
let lambdas = std::mem::take(&mut self.visit.lambdas);
19141918
for snapshot in lambdas {
19151919
self.semantic.restore(snapshot);
19161920

@@ -1927,15 +1931,23 @@ impl<'a> Checker<'a> {
19271931
self.visit_parameters(parameters);
19281932
}
19291933
self.visit_expr(body);
1930-
1931-
deferred.push(snapshot);
19321934
}
19331935
}
1934-
// Reset the deferred lambdas, so we can analyze them later on.
1935-
self.deferred.lambdas = deferred;
19361936
self.semantic.restore(snapshot);
19371937
}
19381938

1939+
/// Recursively visit all deferred AST nodes, including lambdas, functions, and type
1940+
/// annotations.
1941+
fn visit_deferred(&mut self, allocator: &'a typed_arena::Arena<Expr>) {
1942+
while !self.visit.is_empty() {
1943+
self.visit_deferred_functions();
1944+
self.visit_deferred_type_param_definitions();
1945+
self.visit_deferred_lambdas();
1946+
self.visit_deferred_future_type_definitions();
1947+
self.visit_deferred_string_type_definitions(allocator);
1948+
}
1949+
}
1950+
19391951
/// Run any lint rules that operate over the module exports (i.e., members of `__all__`).
19401952
fn visit_exports(&mut self) {
19411953
let snapshot = self.semantic.snapshot();
@@ -2043,12 +2055,8 @@ pub(crate) fn check_ast(
20432055
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
20442056
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
20452057
// function can add a deferred lambda, but the opposite is not true.
2046-
checker.visit_deferred_functions();
2047-
checker.visit_deferred_type_param_definitions();
2048-
checker.visit_deferred_lambdas();
2049-
checker.visit_deferred_future_type_definitions();
20502058
let allocator = typed_arena::Arena::new();
2051-
checker.visit_deferred_string_type_definitions(&allocator);
2059+
checker.visit_deferred(&allocator);
20522060
checker.visit_exports();
20532061

20542062
// Check docstrings, bindings, and unresolved references.
@@ -2060,7 +2068,7 @@ pub(crate) fn check_ast(
20602068

20612069
// Reset the scope to module-level, and check all consumed scopes.
20622070
checker.semantic.scope_id = ScopeId::global();
2063-
checker.deferred.scopes.push(ScopeId::global());
2071+
checker.analyze.scopes.push(ScopeId::global());
20642072
analyze::deferred_scopes(&mut checker);
20652073

20662074
checker.diagnostics

crates/ruff_linter/src/rules/pyflakes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ mod tests {
5454
#[test_case(Rule::UnusedImport, Path::new("F401_19.py"))]
5555
#[test_case(Rule::UnusedImport, Path::new("F401_20.py"))]
5656
#[test_case(Rule::UnusedImport, Path::new("F401_21.py"))]
57+
#[test_case(Rule::UnusedImport, Path::new("F401_22.py"))]
5758
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.py"))]
5859
#[test_case(Rule::ImportShadowedByLoopVar, Path::new("F402.ipynb"))]
5960
#[test_case(Rule::UndefinedLocalWithImportStar, Path::new("F403.py"))]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
3+
---
4+

0 commit comments

Comments
 (0)