Skip to content

Commit 072bb07

Browse files
committed
refactor: common implementation and use previously implemented functions with qualified name resolution
Signed-off-by: 11happy <[email protected]>
1 parent 04faf2d commit 072bb07

File tree

7 files changed

+320
-441
lines changed

7 files changed

+320
-441
lines changed

crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from pathlib import Path
12
def foo():
23
...
34

crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool {
6969
}
7070

7171
/// Returns `true` if an expression resolves to a call to `pathlib.Path.open`.
72-
fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
72+
pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool {
7373
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else {
7474
return false;
7575
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mod async_zero_sleep;
1818
mod blocking_http_call;
1919
mod blocking_http_call_httpx;
2020
mod blocking_input;
21-
mod blocking_open_call;
21+
pub(crate) mod blocking_open_call;
2222
mod blocking_path_methods;
2323
mod blocking_process_invocation;
2424
mod blocking_sleep;

crates/ruff_linter/src/rules/refurb/helpers.rs

Lines changed: 62 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
use std::borrow::Cow;
22

3+
use crate::checkers::ast::Checker;
4+
use crate::rules::flake8_async::rules::blocking_open_call::is_open_call_from_pathlib;
5+
use crate::{Applicability, Edit, Fix};
36
use ruff_python_ast::PythonVersion;
47
use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range};
58
use ruff_python_codegen::Generator;
69
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
710
use ruff_text_size::{Ranged, TextRange};
811

9-
use crate::checkers::ast::Checker;
10-
use crate::{Applicability, Edit, Fix};
11-
1212
/// Format a code snippet to call `name.method()`.
1313
pub(super) fn generate_method_call(name: Name, method: &str, generator: Generator) -> String {
1414
// Construct `name`.
@@ -155,44 +155,16 @@ pub(super) fn find_file_opens<'a>(
155155
.collect()
156156
}
157157

158-
/// Find `open` operation in the given `with` item.
159-
fn find_file_open<'a>(
158+
fn resolve_file_open<'a>(
160159
item: &'a ast::WithItem,
161160
with: &'a ast::StmtWith,
162161
semantic: &'a SemanticModel<'a>,
163162
read_mode: bool,
164-
python_version: PythonVersion,
163+
filename: &'a Expr,
164+
mode: OpenMode,
165+
keywords: Vec<&'a ast::Keyword>,
166+
path_obj: Option<&'a Expr>,
165167
) -> Option<FileOpen<'a>> {
166-
// We want to match `open(...) as var`.
167-
let ast::ExprCall {
168-
func,
169-
arguments: ast::Arguments { args, keywords, .. },
170-
..
171-
} = item.context_expr.as_call_expr()?;
172-
173-
let var = item.optional_vars.as_deref()?.as_name_expr()?;
174-
175-
// Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`,
176-
// it could be a match; but in all other cases, the call _could_ contain unsupported keyword
177-
// arguments, like `buffering`.
178-
if args.iter().any(Expr::is_starred_expr)
179-
|| keywords.iter().any(|keyword| keyword.arg.is_none())
180-
{
181-
return None;
182-
}
183-
184-
if !semantic.match_builtin_expr(func, "open") {
185-
return None;
186-
}
187-
188-
// Match positional arguments, get filename and mode.
189-
let (filename, pos_mode) = match_open_args(args)?;
190-
191-
// Match keyword arguments, get keyword arguments to forward and possibly mode.
192-
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
193-
194-
let mode = kw_mode.unwrap_or(pos_mode);
195-
196168
match mode {
197169
OpenMode::ReadText | OpenMode::ReadBytes => {
198170
if !read_mode {
@@ -206,33 +178,25 @@ fn find_file_open<'a>(
206178
}
207179
}
208180

209-
// Path.read_bytes and Path.write_bytes do not support any kwargs.
210181
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
211182
return None;
212183
}
213184

214-
// Now we need to find what is this variable bound to...
185+
let var = item.optional_vars.as_deref()?.as_name_expr()?;
215186
let scope = semantic.current_scope();
216-
let bindings: Vec<BindingId> = scope.get_all(var.id.as_str()).collect();
217187

218-
let binding = bindings
219-
.iter()
220-
.map(|id| semantic.binding(*id))
221-
// We might have many bindings with the same name, but we only care
222-
// for the one we are looking at right now.
223-
.find(|binding| binding.range() == var.range())?;
188+
let binding = scope.get_all(var.id.as_str()).find_map(|id| {
189+
let b = semantic.binding(id);
190+
(b.range() == var.range()).then_some(b)
191+
})?;
224192

225-
// Since many references can share the same binding, we can limit our attention span
226-
// exclusively to the body of the current `with` statement.
227193
let references: Vec<&ResolvedReference> = binding
228194
.references
229195
.iter()
230196
.map(|id| semantic.reference(*id))
231197
.filter(|reference| with.range().contains_range(reference.range()))
232198
.collect();
233199

234-
// And even with all these restrictions, if the file handle gets used not exactly once,
235-
// it doesn't fit the bill.
236200
let [reference] = references.as_slice() else {
237201
return None;
238202
};
@@ -243,31 +207,67 @@ fn find_file_open<'a>(
243207
mode,
244208
keywords,
245209
reference,
246-
path_obj: None,
210+
path_obj,
247211
})
248212
}
249213

250-
fn find_path_open<'a>(
214+
/// Find `open` operation in the given `with` item.
215+
fn find_file_open<'a>(
251216
item: &'a ast::WithItem,
252217
with: &'a ast::StmtWith,
253218
semantic: &'a SemanticModel<'a>,
254219
read_mode: bool,
255220
python_version: PythonVersion,
256221
) -> Option<FileOpen<'a>> {
222+
// We want to match `open(...) as var`.
257223
let ast::ExprCall {
258224
func,
259225
arguments: ast::Arguments { args, keywords, .. },
260226
..
261227
} = item.context_expr.as_call_expr()?;
262-
let attr = func.as_attribute_expr()?;
263-
if attr.attr.as_str() != "open" {
228+
229+
// Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`,
230+
// it could be a match; but in all other cases, the call _could_ contain unsupported keyword
231+
// arguments, like `buffering`.
232+
if args.iter().any(Expr::is_starred_expr)
233+
|| keywords.iter().any(|keyword| keyword.arg.is_none())
234+
{
264235
return None;
265236
}
266-
let path_call = attr.value.as_call_expr()?;
267-
let path_func = path_call.func.as_name_expr()?;
268-
if path_func.id.as_str() != "Path" {
237+
238+
if !semantic.match_builtin_expr(func, "open") {
269239
return None;
270240
}
241+
242+
// Match positional arguments, get filename and mode.
243+
let (filename, pos_mode) = match_open_args(args)?;
244+
245+
// Match keyword arguments, get keyword arguments to forward and possibly mode.
246+
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
247+
248+
let mode = kw_mode.unwrap_or(pos_mode);
249+
resolve_file_open(
250+
item, with, semantic, read_mode, filename, mode, keywords, None,
251+
)
252+
}
253+
254+
fn find_path_open<'a>(
255+
item: &'a ast::WithItem,
256+
with: &'a ast::StmtWith,
257+
semantic: &'a SemanticModel<'a>,
258+
read_mode: bool,
259+
python_version: PythonVersion,
260+
) -> Option<FileOpen<'a>> {
261+
let ast::ExprCall {
262+
func,
263+
arguments: ast::Arguments { args, keywords, .. },
264+
..
265+
} = item.context_expr.as_call_expr()?;
266+
if !is_open_call_from_pathlib(func, semantic) {
267+
return None;
268+
}
269+
let attr = func.as_attribute_expr()?;
270+
let path_call = attr.value.as_call_expr()?;
271271
let filename = path_call.arguments.args.first()?;
272272
let mode = if args.is_empty() {
273273
OpenMode::ReadText
@@ -276,47 +276,16 @@ fn find_path_open<'a>(
276276
};
277277
let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?;
278278
let mode = kw_mode.unwrap_or(mode);
279-
match mode {
280-
OpenMode::ReadText | OpenMode::ReadBytes => {
281-
if !read_mode {
282-
return None;
283-
}
284-
}
285-
OpenMode::WriteText | OpenMode::WriteBytes => {
286-
if read_mode {
287-
return None;
288-
}
289-
}
290-
}
291-
if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() {
292-
return None;
293-
}
294-
let var = item.optional_vars.as_deref()?.as_name_expr()?;
295-
let scope = semantic.current_scope();
296-
let bindings: Vec<_> = scope.get_all(var.id.as_str()).collect();
297-
let binding = bindings
298-
.iter()
299-
.map(|id| semantic.binding(*id))
300-
.find(|binding| binding.range() == var.range())?;
301-
302-
let references: Vec<&ResolvedReference> = binding
303-
.references
304-
.iter()
305-
.map(|id| semantic.reference(*id))
306-
.filter(|reference| with.range().contains_range(reference.range()))
307-
.collect();
308-
309-
let [reference] = references.as_slice() else {
310-
return None;
311-
};
312-
Some(FileOpen {
279+
resolve_file_open(
313280
item,
281+
with,
282+
semantic,
283+
read_mode,
314284
filename,
315285
mode,
316286
keywords,
317-
reference,
318-
path_obj: Some(attr.value.as_ref()),
319-
})
287+
Some(attr.value.as_ref()),
288+
)
320289
}
321290

322291
/// Match positional arguments. Return expression for the file name and open mode.

0 commit comments

Comments
 (0)