11use 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 } ;
36use ruff_python_ast:: PythonVersion ;
47use ruff_python_ast:: { self as ast, Expr , name:: Name , parenthesize:: parenthesized_range} ;
58use ruff_python_codegen:: Generator ;
69use ruff_python_semantic:: { BindingId , ResolvedReference , SemanticModel } ;
710use 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()`.
1313pub ( 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