Skip to content

Commit 1b79ae9

Browse files
committed
[ruff-0.7] Stabilise the expansion of open-file-with-context-handler to work with other standard-library IO modules (SIM115) (#13680)
Closes #7313.
1 parent 2b87587 commit 1b79ae9

4 files changed

+284
-383
lines changed

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

-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ mod tests {
5858
}
5959

6060
#[test_case(Rule::IfElseBlockInsteadOfIfExp, Path::new("SIM108.py"))]
61-
#[test_case(Rule::OpenFileWithContextHandler, Path::new("SIM115.py"))]
6261
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
6362
let snapshot = format!(
6463
"preview__{}_{}",

crates/ruff_linter/src/rules/flake8_simplify/rules/open_file_with_context_handler.rs

+11-48
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,9 @@ use crate::checkers::ast::Checker;
1414
/// ## Why is this bad?
1515
/// If a file is opened without a context manager, it is not guaranteed that
1616
/// the file will be closed (e.g., if an exception is raised), which can cause
17-
/// resource leaks.
18-
///
19-
/// ## Preview-mode behavior
20-
/// If [preview] mode is enabled, this rule will detect a wide array of IO calls where
21-
/// context managers could be used, such as `tempfile.TemporaryFile()` or
22-
/// `tarfile.TarFile(...).gzopen()`. If preview mode is not enabled, only `open()`,
23-
/// `builtins.open()` and `pathlib.Path(...).open()` are detected.
17+
/// resource leaks. The rule detects a wide array of IO calls where context managers
18+
/// could be used, such as `open`, `pathlib.Path(...).open()`, `tempfile.TemporaryFile()`
19+
/// or`tarfile.TarFile(...).gzopen()`.
2420
///
2521
/// ## Example
2622
/// ```python
@@ -118,36 +114,9 @@ fn match_exit_stack(semantic: &SemanticModel) -> bool {
118114
false
119115
}
120116

121-
/// Return `true` if `func` is the builtin `open` or `pathlib.Path(...).open`.
122-
fn is_open(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
123-
// Ex) `open(...)`
124-
if semantic.match_builtin_expr(&call.func, "open") {
125-
return true;
126-
}
127-
128-
// Ex) `pathlib.Path(...).open()`
129-
let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = &*call.func else {
130-
return false;
131-
};
132-
133-
if attr != "open" {
134-
return false;
135-
}
136-
137-
let Expr::Call(ast::ExprCall {
138-
func: value_func, ..
139-
}) = &**value
140-
else {
141-
return false;
142-
};
143-
144-
semantic
145-
.resolve_qualified_name(value_func)
146-
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["pathlib", "Path"]))
147-
}
148-
149-
/// Return `true` if the expression is an `open` call or temporary file constructor.
150-
fn is_open_preview(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
117+
/// Return `true` if the expression is a call to `open()`,
118+
/// or a call to some other standard-library function that opens a file.
119+
fn is_open_call(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
151120
let func = &*call.func;
152121

153122
// Ex) `open(...)`
@@ -203,8 +172,8 @@ fn is_open_preview(semantic: &SemanticModel, call: &ast::ExprCall) -> bool {
203172
)
204173
}
205174

206-
/// Return `true` if the current expression is followed by a `close` call.
207-
fn is_closed(semantic: &SemanticModel) -> bool {
175+
/// Return `true` if the current expression is immediately followed by a `.close()` call.
176+
fn is_immediately_closed(semantic: &SemanticModel) -> bool {
208177
let Some(expr) = semantic.current_expression_grandparent() else {
209178
return false;
210179
};
@@ -231,18 +200,12 @@ fn is_closed(semantic: &SemanticModel) -> bool {
231200
pub(crate) fn open_file_with_context_handler(checker: &mut Checker, call: &ast::ExprCall) {
232201
let semantic = checker.semantic();
233202

234-
if checker.settings.preview.is_disabled() {
235-
if !is_open(semantic, call) {
236-
return;
237-
}
238-
} else {
239-
if !is_open_preview(semantic, call) {
240-
return;
241-
}
203+
if !is_open_call(semantic, call) {
204+
return;
242205
}
243206

244207
// Ex) `open("foo.txt").close()`
245-
if is_closed(semantic) {
208+
if is_immediately_closed(semantic) {
246209
return;
247210
}
248211

crates/ruff_linter/src/rules/flake8_simplify/snapshots/ruff_linter__rules__flake8_simplify__tests__SIM115_SIM115.py.snap

+273
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,276 @@ SIM115.py:39:9: SIM115 Use a context manager for opening files
5959
40 |
6060
41 | # OK
6161
|
62+
63+
SIM115.py:80:5: SIM115 Use a context manager for opening files
64+
|
65+
78 | import fileinput
66+
79 |
67+
80 | f = tempfile.NamedTemporaryFile()
68+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
69+
81 | f = tempfile.TemporaryFile()
70+
82 | f = tempfile.SpooledTemporaryFile()
71+
|
72+
73+
SIM115.py:81:5: SIM115 Use a context manager for opening files
74+
|
75+
80 | f = tempfile.NamedTemporaryFile()
76+
81 | f = tempfile.TemporaryFile()
77+
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
78+
82 | f = tempfile.SpooledTemporaryFile()
79+
83 | f = tarfile.open("foo.tar")
80+
|
81+
82+
SIM115.py:82:5: SIM115 Use a context manager for opening files
83+
|
84+
80 | f = tempfile.NamedTemporaryFile()
85+
81 | f = tempfile.TemporaryFile()
86+
82 | f = tempfile.SpooledTemporaryFile()
87+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
88+
83 | f = tarfile.open("foo.tar")
89+
84 | f = TarFile("foo.tar").open()
90+
|
91+
92+
SIM115.py:83:5: SIM115 Use a context manager for opening files
93+
|
94+
81 | f = tempfile.TemporaryFile()
95+
82 | f = tempfile.SpooledTemporaryFile()
96+
83 | f = tarfile.open("foo.tar")
97+
| ^^^^^^^^^^^^ SIM115
98+
84 | f = TarFile("foo.tar").open()
99+
85 | f = tarfile.TarFile("foo.tar").open()
100+
|
101+
102+
SIM115.py:84:5: SIM115 Use a context manager for opening files
103+
|
104+
82 | f = tempfile.SpooledTemporaryFile()
105+
83 | f = tarfile.open("foo.tar")
106+
84 | f = TarFile("foo.tar").open()
107+
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
108+
85 | f = tarfile.TarFile("foo.tar").open()
109+
86 | f = tarfile.TarFile().open()
110+
|
111+
112+
SIM115.py:85:5: SIM115 Use a context manager for opening files
113+
|
114+
83 | f = tarfile.open("foo.tar")
115+
84 | f = TarFile("foo.tar").open()
116+
85 | f = tarfile.TarFile("foo.tar").open()
117+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
118+
86 | f = tarfile.TarFile().open()
119+
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
120+
|
121+
122+
SIM115.py:86:5: SIM115 Use a context manager for opening files
123+
|
124+
84 | f = TarFile("foo.tar").open()
125+
85 | f = tarfile.TarFile("foo.tar").open()
126+
86 | f = tarfile.TarFile().open()
127+
| ^^^^^^^^^^^^^^^^^^^^^^ SIM115
128+
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
129+
88 | f = io.open("foo.txt")
130+
|
131+
132+
SIM115.py:87:5: SIM115 Use a context manager for opening files
133+
|
134+
85 | f = tarfile.TarFile("foo.tar").open()
135+
86 | f = tarfile.TarFile().open()
136+
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
137+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM115
138+
88 | f = io.open("foo.txt")
139+
89 | f = io.open_code("foo.txt")
140+
|
141+
142+
SIM115.py:88:5: SIM115 Use a context manager for opening files
143+
|
144+
86 | f = tarfile.TarFile().open()
145+
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
146+
88 | f = io.open("foo.txt")
147+
| ^^^^^^^ SIM115
148+
89 | f = io.open_code("foo.txt")
149+
90 | f = codecs.open("foo.txt")
150+
|
151+
152+
SIM115.py:89:5: SIM115 Use a context manager for opening files
153+
|
154+
87 | f = zipfile.ZipFile("foo.zip").open("foo.txt")
155+
88 | f = io.open("foo.txt")
156+
89 | f = io.open_code("foo.txt")
157+
| ^^^^^^^^^^^^ SIM115
158+
90 | f = codecs.open("foo.txt")
159+
91 | f = bz2.open("foo.txt")
160+
|
161+
162+
SIM115.py:90:5: SIM115 Use a context manager for opening files
163+
|
164+
88 | f = io.open("foo.txt")
165+
89 | f = io.open_code("foo.txt")
166+
90 | f = codecs.open("foo.txt")
167+
| ^^^^^^^^^^^ SIM115
168+
91 | f = bz2.open("foo.txt")
169+
92 | f = gzip.open("foo.txt")
170+
|
171+
172+
SIM115.py:91:5: SIM115 Use a context manager for opening files
173+
|
174+
89 | f = io.open_code("foo.txt")
175+
90 | f = codecs.open("foo.txt")
176+
91 | f = bz2.open("foo.txt")
177+
| ^^^^^^^^ SIM115
178+
92 | f = gzip.open("foo.txt")
179+
93 | f = dbm.open("foo.db")
180+
|
181+
182+
SIM115.py:92:5: SIM115 Use a context manager for opening files
183+
|
184+
90 | f = codecs.open("foo.txt")
185+
91 | f = bz2.open("foo.txt")
186+
92 | f = gzip.open("foo.txt")
187+
| ^^^^^^^^^ SIM115
188+
93 | f = dbm.open("foo.db")
189+
94 | f = dbm.gnu.open("foo.db")
190+
|
191+
192+
SIM115.py:93:5: SIM115 Use a context manager for opening files
193+
|
194+
91 | f = bz2.open("foo.txt")
195+
92 | f = gzip.open("foo.txt")
196+
93 | f = dbm.open("foo.db")
197+
| ^^^^^^^^ SIM115
198+
94 | f = dbm.gnu.open("foo.db")
199+
95 | f = dbm.ndbm.open("foo.db")
200+
|
201+
202+
SIM115.py:94:5: SIM115 Use a context manager for opening files
203+
|
204+
92 | f = gzip.open("foo.txt")
205+
93 | f = dbm.open("foo.db")
206+
94 | f = dbm.gnu.open("foo.db")
207+
| ^^^^^^^^^^^^ SIM115
208+
95 | f = dbm.ndbm.open("foo.db")
209+
96 | f = dbm.dumb.open("foo.db")
210+
|
211+
212+
SIM115.py:95:5: SIM115 Use a context manager for opening files
213+
|
214+
93 | f = dbm.open("foo.db")
215+
94 | f = dbm.gnu.open("foo.db")
216+
95 | f = dbm.ndbm.open("foo.db")
217+
| ^^^^^^^^^^^^^ SIM115
218+
96 | f = dbm.dumb.open("foo.db")
219+
97 | f = lzma.open("foo.xz")
220+
|
221+
222+
SIM115.py:96:5: SIM115 Use a context manager for opening files
223+
|
224+
94 | f = dbm.gnu.open("foo.db")
225+
95 | f = dbm.ndbm.open("foo.db")
226+
96 | f = dbm.dumb.open("foo.db")
227+
| ^^^^^^^^^^^^^ SIM115
228+
97 | f = lzma.open("foo.xz")
229+
98 | f = lzma.LZMAFile("foo.xz")
230+
|
231+
232+
SIM115.py:97:5: SIM115 Use a context manager for opening files
233+
|
234+
95 | f = dbm.ndbm.open("foo.db")
235+
96 | f = dbm.dumb.open("foo.db")
236+
97 | f = lzma.open("foo.xz")
237+
| ^^^^^^^^^ SIM115
238+
98 | f = lzma.LZMAFile("foo.xz")
239+
99 | f = shelve.open("foo.db")
240+
|
241+
242+
SIM115.py:98:5: SIM115 Use a context manager for opening files
243+
|
244+
96 | f = dbm.dumb.open("foo.db")
245+
97 | f = lzma.open("foo.xz")
246+
98 | f = lzma.LZMAFile("foo.xz")
247+
| ^^^^^^^^^^^^^ SIM115
248+
99 | f = shelve.open("foo.db")
249+
100 | f = tokenize.open("foo.py")
250+
|
251+
252+
SIM115.py:99:5: SIM115 Use a context manager for opening files
253+
|
254+
97 | f = lzma.open("foo.xz")
255+
98 | f = lzma.LZMAFile("foo.xz")
256+
99 | f = shelve.open("foo.db")
257+
| ^^^^^^^^^^^ SIM115
258+
100 | f = tokenize.open("foo.py")
259+
101 | f = wave.open("foo.wav")
260+
|
261+
262+
SIM115.py:100:5: SIM115 Use a context manager for opening files
263+
|
264+
98 | f = lzma.LZMAFile("foo.xz")
265+
99 | f = shelve.open("foo.db")
266+
100 | f = tokenize.open("foo.py")
267+
| ^^^^^^^^^^^^^ SIM115
268+
101 | f = wave.open("foo.wav")
269+
102 | f = tarfile.TarFile.taropen("foo.tar")
270+
|
271+
272+
SIM115.py:101:5: SIM115 Use a context manager for opening files
273+
|
274+
99 | f = shelve.open("foo.db")
275+
100 | f = tokenize.open("foo.py")
276+
101 | f = wave.open("foo.wav")
277+
| ^^^^^^^^^ SIM115
278+
102 | f = tarfile.TarFile.taropen("foo.tar")
279+
103 | f = fileinput.input("foo.txt")
280+
|
281+
282+
SIM115.py:102:5: SIM115 Use a context manager for opening files
283+
|
284+
100 | f = tokenize.open("foo.py")
285+
101 | f = wave.open("foo.wav")
286+
102 | f = tarfile.TarFile.taropen("foo.tar")
287+
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM115
288+
103 | f = fileinput.input("foo.txt")
289+
104 | f = fileinput.FileInput("foo.txt")
290+
|
291+
292+
SIM115.py:103:5: SIM115 Use a context manager for opening files
293+
|
294+
101 | f = wave.open("foo.wav")
295+
102 | f = tarfile.TarFile.taropen("foo.tar")
296+
103 | f = fileinput.input("foo.txt")
297+
| ^^^^^^^^^^^^^^^ SIM115
298+
104 | f = fileinput.FileInput("foo.txt")
299+
|
300+
301+
SIM115.py:104:5: SIM115 Use a context manager for opening files
302+
|
303+
102 | f = tarfile.TarFile.taropen("foo.tar")
304+
103 | f = fileinput.input("foo.txt")
305+
104 | f = fileinput.FileInput("foo.txt")
306+
| ^^^^^^^^^^^^^^^^^^^ SIM115
307+
105 |
308+
106 | with contextlib.suppress(Exception):
309+
|
310+
311+
SIM115.py:240:9: SIM115 Use a context manager for opening files
312+
|
313+
238 | def aliased():
314+
239 | from shelve import open as open_shelf
315+
240 | x = open_shelf("foo.dbm")
316+
| ^^^^^^^^^^ SIM115
317+
241 | x.close()
318+
|
319+
320+
SIM115.py:244:9: SIM115 Use a context manager for opening files
321+
|
322+
243 | from tarfile import TarFile as TF
323+
244 | f = TF("foo").open()
324+
| ^^^^^^^^^^^^^^ SIM115
325+
245 | f.close()
326+
|
327+
328+
SIM115.py:257:5: SIM115 Use a context manager for opening files
329+
|
330+
256 | # SIM115
331+
257 | f = dbm.sqlite3.open("foo.db")
332+
| ^^^^^^^^^^^^^^^^ SIM115
333+
258 | f.close()
334+
|

0 commit comments

Comments
 (0)