diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_0.py similarity index 100% rename from crates/ruff_linter/resources/test/fixtures/refurb/FURB103.py rename to crates/ruff_linter/resources/test/fixtures/refurb/FURB103_0.py diff --git a/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py new file mode 100644 index 0000000000..f4b9cb1dec --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/refurb/FURB103_1.py @@ -0,0 +1,26 @@ +from pathlib import Path + +with Path("file.txt").open("w") as f: + f.write("test") + +with Path("file.txt").open("wb") as f: + f.write(b"test") + +with Path("file.txt").open(mode="w") as f: + f.write("test") + +with Path("file.txt").open("w", encoding="utf8") as f: + f.write("test") + +with Path("file.txt").open("w", errors="ignore") as f: + f.write("test") + +with Path(foo()).open("w") as f: + f.write("test") + +p = Path("file.txt") +with p.open("w") as f: + f.write("test") + +with Path("foo", "bar", "baz").open("w") as f: + f.write("test") \ No newline at end of file diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs index 0b1427c781..01bea3def4 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/blocking_open_call.rs @@ -70,7 +70,7 @@ fn is_open_call(func: &Expr, semantic: &SemanticModel) -> bool { } /// Returns `true` if an expression resolves to a call to `pathlib.Path.open`. -fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool { +pub(crate) fn is_open_call_from_pathlib(func: &Expr, semantic: &SemanticModel) -> bool { let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func else { return false; }; diff --git a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs index 3e12ea360c..ecdddaeda6 100644 --- a/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_async/rules/mod.rs @@ -18,7 +18,7 @@ mod async_zero_sleep; mod blocking_http_call; mod blocking_http_call_httpx; mod blocking_input; -mod blocking_open_call; +pub(crate) mod blocking_open_call; mod blocking_path_methods; mod blocking_process_invocation; mod blocking_sleep; diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index a6871f0497..3b1d20e784 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -3,10 +3,11 @@ use std::borrow::Cow; use ruff_python_ast::PythonVersion; use ruff_python_ast::{self as ast, Expr, name::Name, token::parenthesized_range}; use ruff_python_codegen::Generator; -use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; +use ruff_python_semantic::{ResolvedReference, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; +use crate::rules::flake8_async::rules::blocking_open_call::is_open_call_from_pathlib; use crate::{Applicability, Edit, Fix}; /// Format a code snippet to call `name.method()`. @@ -119,14 +120,13 @@ impl OpenMode { pub(super) struct FileOpen<'a> { /// With item where the open happens, we use it for the reporting range. pub(super) item: &'a ast::WithItem, - /// Filename expression used as the first argument in `open`, we use it in the diagnostic message. - pub(super) filename: &'a Expr, /// The file open mode. pub(super) mode: OpenMode, /// The file open keywords. pub(super) keywords: Vec<&'a ast::Keyword>, /// We only check `open` operations whose file handles are used exactly once. pub(super) reference: &'a ResolvedReference, + pub(super) argument: OpenArgument<'a>, } impl FileOpen<'_> { @@ -137,6 +137,45 @@ impl FileOpen<'_> { } } +#[derive(Debug, Clone, Copy)] +pub(super) enum OpenArgument<'a> { + /// The filename argument to `open`, e.g. "foo.txt" in: + /// + /// ```py + /// f = open("foo.txt") + /// ``` + Builtin { filename: &'a Expr }, + /// The `Path` receiver of a `pathlib.Path.open` call, e.g. the `p` in the + /// context manager in: + /// + /// ```py + /// p = Path("foo.txt") + /// with p.open() as f: ... + /// ``` + /// + /// or `Path("foo.txt")` in + /// + /// ```py + /// with Path("foo.txt").open() as f: ... + /// ``` + Pathlib { path: &'a Expr }, +} + +impl OpenArgument<'_> { + pub(super) fn display<'src>(&self, source: &'src str) -> &'src str { + &source[self.range()] + } +} + +impl Ranged for OpenArgument<'_> { + fn range(&self) -> TextRange { + match self { + OpenArgument::Builtin { filename } => filename.range(), + OpenArgument::Pathlib { path } => path.range(), + } + } +} + /// Find and return all `open` operations in the given `with` statement. pub(super) fn find_file_opens<'a>( with: &'a ast::StmtWith, @@ -146,10 +185,65 @@ pub(super) fn find_file_opens<'a>( ) -> Vec> { with.items .iter() - .filter_map(|item| find_file_open(item, with, semantic, read_mode, python_version)) + .filter_map(|item| { + find_file_open(item, with, semantic, read_mode, python_version) + .or_else(|| find_path_open(item, with, semantic, read_mode, python_version)) + }) .collect() } +fn resolve_file_open<'a>( + item: &'a ast::WithItem, + with: &'a ast::StmtWith, + semantic: &'a SemanticModel<'a>, + read_mode: bool, + mode: OpenMode, + keywords: Vec<&'a ast::Keyword>, + argument: OpenArgument<'a>, +) -> Option> { + match mode { + OpenMode::ReadText | OpenMode::ReadBytes => { + if !read_mode { + return None; + } + } + OpenMode::WriteText | OpenMode::WriteBytes => { + if read_mode { + return None; + } + } + } + + if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() { + return None; + } + let var = item.optional_vars.as_deref()?.as_name_expr()?; + let scope = semantic.current_scope(); + + let binding = scope.get_all(var.id.as_str()).find_map(|id| { + let b = semantic.binding(id); + (b.range() == var.range()).then_some(b) + })?; + let references: Vec<&ResolvedReference> = binding + .references + .iter() + .map(|id| semantic.reference(*id)) + .filter(|reference| with.range().contains_range(reference.range())) + .collect(); + + let [reference] = references.as_slice() else { + return None; + }; + + Some(FileOpen { + item, + mode, + keywords, + reference, + argument, + }) +} + /// Find `open` operation in the given `with` item. fn find_file_open<'a>( item: &'a ast::WithItem, @@ -165,8 +259,6 @@ fn find_file_open<'a>( .. } = item.context_expr.as_call_expr()?; - let var = item.optional_vars.as_deref()?.as_name_expr()?; - // Ignore calls with `*args` and `**kwargs`. In the exact case of `open(*filename, mode="w")`, // it could be a match; but in all other cases, the call _could_ contain unsupported keyword // arguments, like `buffering`. @@ -187,58 +279,57 @@ fn find_file_open<'a>( let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?; let mode = kw_mode.unwrap_or(pos_mode); - - match mode { - OpenMode::ReadText | OpenMode::ReadBytes => { - if !read_mode { - return None; - } - } - OpenMode::WriteText | OpenMode::WriteBytes => { - if read_mode { - return None; - } - } - } - - // Path.read_bytes and Path.write_bytes do not support any kwargs. - if matches!(mode, OpenMode::ReadBytes | OpenMode::WriteBytes) && !keywords.is_empty() { - return None; - } - - // Now we need to find what is this variable bound to... - let scope = semantic.current_scope(); - let bindings: Vec = scope.get_all(var.id.as_str()).collect(); - - let binding = bindings - .iter() - .map(|id| semantic.binding(*id)) - // We might have many bindings with the same name, but we only care - // for the one we are looking at right now. - .find(|binding| binding.range() == var.range())?; - - // Since many references can share the same binding, we can limit our attention span - // exclusively to the body of the current `with` statement. - let references: Vec<&ResolvedReference> = binding - .references - .iter() - .map(|id| semantic.reference(*id)) - .filter(|reference| with.range().contains_range(reference.range())) - .collect(); - - // And even with all these restrictions, if the file handle gets used not exactly once, - // it doesn't fit the bill. - let [reference] = references.as_slice() else { - return None; - }; - - Some(FileOpen { + resolve_file_open( item, - filename, + with, + semantic, + read_mode, mode, keywords, - reference, - }) + OpenArgument::Builtin { filename }, + ) +} + +fn find_path_open<'a>( + item: &'a ast::WithItem, + with: &'a ast::StmtWith, + semantic: &'a SemanticModel<'a>, + read_mode: bool, + python_version: PythonVersion, +) -> Option> { + let ast::ExprCall { + func, + arguments: ast::Arguments { args, keywords, .. }, + .. + } = item.context_expr.as_call_expr()?; + if args.iter().any(Expr::is_starred_expr) + || keywords.iter().any(|keyword| keyword.arg.is_none()) + { + return None; + } + if !is_open_call_from_pathlib(func, semantic) { + return None; + } + let attr = func.as_attribute_expr()?; + let mode = if args.is_empty() { + OpenMode::ReadText + } else { + match_open_mode(args.first()?)? + }; + + let (keywords, kw_mode) = match_open_keywords(keywords, read_mode, python_version)?; + let mode = kw_mode.unwrap_or(mode); + resolve_file_open( + item, + with, + semantic, + read_mode, + mode, + keywords, + OpenArgument::Pathlib { + path: attr.value.as_ref(), + }, + ) } /// Match positional arguments. Return expression for the file name and open mode. diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 9187853141..2c7f389eae 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -46,7 +46,8 @@ mod tests { #[test_case(Rule::MetaClassABCMeta, Path::new("FURB180.py"))] #[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))] #[test_case(Rule::ListReverseCopy, Path::new("FURB187.py"))] - #[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))] + #[test_case(Rule::WriteWholeFile, Path::new("FURB103_0.py"))] + #[test_case(Rule::WriteWholeFile, Path::new("FURB103_1.py"))] #[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))] #[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))] #[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))] @@ -65,7 +66,7 @@ mod tests { #[test] fn write_whole_file_python_39() -> Result<()> { let diagnostics = test_path( - Path::new("refurb/FURB103.py"), + Path::new("refurb/FURB103_0.py"), &settings::LinterSettings::for_rule(Rule::WriteWholeFile) .with_target_version(PythonVersion::PY39), )?; diff --git a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs index 2b43af89a8..eb321cc4d0 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/read_whole_file.rs @@ -10,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; use crate::importer::ImportRequest; -use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; +use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens}; use crate::{FixAvailability, Violation}; /// ## What it does @@ -114,12 +114,11 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> { .position(|open| open.is_ref(read_from)) { let open = self.candidates.remove(open); + let filename_display = open.argument.display(self.checker.source()); let suggestion = make_suggestion(&open, self.checker.generator()); let mut diagnostic = self.checker.report_diagnostic( ReadWholeFile { - filename: SourceCodeSnippet::from_str( - &self.checker.generator().expr(open.filename), - ), + filename: SourceCodeSnippet::from_str(filename_display), suggestion: SourceCodeSnippet::from_str(&suggestion), }, open.item.range(), @@ -185,10 +184,11 @@ fn generate_fix( if with_stmt.items.len() != 1 { return None; } - + let OpenArgument::Builtin { filename } = open.argument else { + return None; + }; let locator = checker.locator(); - - let filename_code = locator.slice(open.filename.range()); + let filename_code = locator.slice(filename.range()); let (import_edit, binding) = checker .importer() diff --git a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs index 8e4c7319ba..23a53e1f3a 100644 --- a/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs +++ b/crates/ruff_linter/src/rules/refurb/rules/write_whole_file.rs @@ -9,7 +9,7 @@ use ruff_text_size::Ranged; use crate::checkers::ast::Checker; use crate::fix::snippet::SourceCodeSnippet; use crate::importer::ImportRequest; -use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; +use crate::rules::refurb::helpers::{FileOpen, OpenArgument, find_file_opens}; use crate::{FixAvailability, Locator, Violation}; /// ## What it does @@ -42,26 +42,40 @@ use crate::{FixAvailability, Locator, Violation}; /// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text) #[derive(ViolationMetadata)] #[violation_metadata(preview_since = "v0.3.6")] -pub(crate) struct WriteWholeFile { +pub(crate) struct WriteWholeFile<'a> { filename: SourceCodeSnippet, suggestion: SourceCodeSnippet, + argument: OpenArgument<'a>, } -impl Violation for WriteWholeFile { +impl Violation for WriteWholeFile<'_> { const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; #[derive_message_formats] fn message(&self) -> String { let filename = self.filename.truncated_display(); let suggestion = self.suggestion.truncated_display(); - format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`") + match self.argument { + OpenArgument::Pathlib { .. } => { + format!( + "`Path.open()` followed by `write()` can be replaced by `{filename}.{suggestion}`" + ) + } + OpenArgument::Builtin { .. } => { + format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`") + } + } } fn fix_title(&self) -> Option { - Some(format!( - "Replace with `Path({}).{}`", - self.filename.truncated_display(), - self.suggestion.truncated_display(), - )) + let filename = self.filename.truncated_display(); + let suggestion = self.suggestion.truncated_display(); + + match self.argument { + OpenArgument::Pathlib { .. } => Some(format!("Replace with `{filename}.{suggestion}`")), + OpenArgument::Builtin { .. } => { + Some(format!("Replace with `Path({filename}).{suggestion}`")) + } + } } } @@ -125,16 +139,15 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> { .position(|open| open.is_ref(write_to)) { let open = self.candidates.remove(open); - if self.loop_counter == 0 { + let filename_display = open.argument.display(self.checker.source()); let suggestion = make_suggestion(&open, content, self.checker.locator()); let mut diagnostic = self.checker.report_diagnostic( WriteWholeFile { - filename: SourceCodeSnippet::from_str( - &self.checker.generator().expr(open.filename), - ), + filename: SourceCodeSnippet::from_str(filename_display), suggestion: SourceCodeSnippet::from_str(&suggestion), + argument: open.argument, }, open.item.range(), ); @@ -198,7 +211,6 @@ fn generate_fix( } let locator = checker.locator(); - let filename_code = locator.slice(open.filename.range()); let (import_edit, binding) = checker .importer() @@ -209,7 +221,15 @@ fn generate_fix( ) .ok()?; - let replacement = format!("{binding}({filename_code}).{suggestion}"); + let target = match open.argument { + OpenArgument::Builtin { filename } => { + let filename_code = locator.slice(filename.range()); + format!("{binding}({filename_code})") + } + OpenArgument::Pathlib { path } => locator.slice(path.range()).to_string(), + }; + + let replacement = format!("{target}.{suggestion}"); let applicability = if checker.comment_ranges().intersects(with_stmt.range()) { Applicability::Unsafe diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap similarity index 96% rename from crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap rename to crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap index 81c35384b9..a22c150c6b 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_0.py.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/refurb/mod.rs --- FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")` - --> FURB103.py:12:6 + --> FURB103_0.py:12:6 | 11 | # FURB103 12 | with open("file.txt", "w") as f: @@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")` 16 | with open("file.txt", "wb") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` - --> FURB103.py:16:6 + --> FURB103_0.py:16:6 | 15 | # FURB103 16 | with open("file.txt", "wb") as f: @@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)` 20 | with open("file.txt", mode="wb") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")` - --> FURB103.py:20:6 + --> FURB103_0.py:20:6 | 19 | # FURB103 20 | with open("file.txt", mode="wb") as f: @@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")` 24 | with open("file.txt", "w", encoding="utf8") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")` - --> FURB103.py:24:6 + --> FURB103_0.py:24:6 | 23 | # FURB103 24 | with open("file.txt", "w", encoding="utf8") as f: @@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")` 28 | with open("file.txt", "w", errors="ignore") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")` - --> FURB103.py:28:6 + --> FURB103_0.py:28:6 | 27 | # FURB103 28 | with open("file.txt", "w", errors="ignore") as f: @@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")` 32 | with open("file.txt", mode="w") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` - --> FURB103.py:32:6 + --> FURB103_0.py:32:6 | 31 | # FURB103 32 | with open("file.txt", mode="w") as f: @@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)` 36 | with open(foo(), "wb") as f: FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` - --> FURB103.py:36:6 + --> FURB103_0.py:36:6 | 35 | # FURB103 36 | with open(foo(), "wb") as f: @@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar()) help: Replace with `Path(foo()).write_bytes(bar())` FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` - --> FURB103.py:44:6 + --> FURB103_0.py:44:6 | 43 | # FURB103 44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: @@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` help: Replace with `Path("a.txt").write_text(x)` FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` - --> FURB103.py:44:31 + --> FURB103_0.py:44:31 | 43 | # FURB103 44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: @@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` help: Replace with `Path("b.txt").write_bytes(y)` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))` - --> FURB103.py:49:18 + --> FURB103_0.py:49:18 | 48 | # FURB103 49 | with foo() as a, open("file.txt", "w") as b, foo() as c: @@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))` FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` - --> FURB103.py:58:6 + --> FURB103_0.py:58:6 | 57 | # FURB103 58 | with open("file.txt", "w", newline="\r\n") as f: @@ -214,7 +214,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` 62 | import builtins FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` - --> FURB103.py:66:6 + --> FURB103_0.py:66:6 | 65 | # FURB103 66 | with builtins.open("file.txt", "w", newline="\r\n") as f: @@ -237,7 +237,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` 70 | from builtins import open as o FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` - --> FURB103.py:74:6 + --> FURB103_0.py:74:6 | 73 | # FURB103 74 | with o("file.txt", "w", newline="\r\n") as f: @@ -260,7 +260,7 @@ help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` 78 | FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....` - --> FURB103.py:154:6 + --> FURB103_0.py:154:6 | 152 | data = {"price": 100} 153 | @@ -284,7 +284,7 @@ help: Replace with `Path("test.json")....` 158 | with open("tmp_path/pyproject.toml", "w") as f: FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....` - --> FURB103.py:158:6 + --> FURB103_0.py:158:6 | 157 | # See: https://github.com/astral-sh/ruff/issues/21381 158 | with open("tmp_path/pyproject.toml", "w") as f: diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap new file mode 100644 index 0000000000..d30974009c --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB103_FURB103_1.py.snap @@ -0,0 +1,157 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")` + --> FURB103_1.py:3:6 + | +1 | from pathlib import Path +2 | +3 | with Path("file.txt").open("w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +4 | f.write("test") + | +help: Replace with `Path("file.txt").write_text("test")` +1 | from pathlib import Path +2 | + - with Path("file.txt").open("w") as f: + - f.write("test") +3 + Path("file.txt").write_text("test") +4 | +5 | with Path("file.txt").open("wb") as f: +6 | f.write(b"test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_bytes(b"test")` + --> FURB103_1.py:6:6 + | +4 | f.write("test") +5 | +6 | with Path("file.txt").open("wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +7 | f.write(b"test") + | +help: Replace with `Path("file.txt").write_bytes(b"test")` +3 | with Path("file.txt").open("w") as f: +4 | f.write("test") +5 | + - with Path("file.txt").open("wb") as f: + - f.write(b"test") +6 + Path("file.txt").write_bytes(b"test") +7 | +8 | with Path("file.txt").open(mode="w") as f: +9 | f.write("test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")` + --> FURB103_1.py:9:6 + | + 7 | f.write(b"test") + 8 | + 9 | with Path("file.txt").open(mode="w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +10 | f.write("test") + | +help: Replace with `Path("file.txt").write_text("test")` +6 | with Path("file.txt").open("wb") as f: +7 | f.write(b"test") +8 | + - with Path("file.txt").open(mode="w") as f: + - f.write("test") +9 + Path("file.txt").write_text("test") +10 | +11 | with Path("file.txt").open("w", encoding="utf8") as f: +12 | f.write("test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", encoding="utf8")` + --> FURB103_1.py:12:6 + | +10 | f.write("test") +11 | +12 | with Path("file.txt").open("w", encoding="utf8") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +13 | f.write("test") + | +help: Replace with `Path("file.txt").write_text("test", encoding="utf8")` +9 | with Path("file.txt").open(mode="w") as f: +10 | f.write("test") +11 | + - with Path("file.txt").open("w", encoding="utf8") as f: + - f.write("test") +12 + Path("file.txt").write_text("test", encoding="utf8") +13 | +14 | with Path("file.txt").open("w", errors="ignore") as f: +15 | f.write("test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", errors="ignore")` + --> FURB103_1.py:15:6 + | +13 | f.write("test") +14 | +15 | with Path("file.txt").open("w", errors="ignore") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +16 | f.write("test") + | +help: Replace with `Path("file.txt").write_text("test", errors="ignore")` +12 | with Path("file.txt").open("w", encoding="utf8") as f: +13 | f.write("test") +14 | + - with Path("file.txt").open("w", errors="ignore") as f: + - f.write("test") +15 + Path("file.txt").write_text("test", errors="ignore") +16 | +17 | with Path(foo()).open("w") as f: +18 | f.write("test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path(foo()).write_text("test")` + --> FURB103_1.py:18:6 + | +16 | f.write("test") +17 | +18 | with Path(foo()).open("w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +19 | f.write("test") + | +help: Replace with `Path(foo()).write_text("test")` +15 | with Path("file.txt").open("w", errors="ignore") as f: +16 | f.write("test") +17 | + - with Path(foo()).open("w") as f: + - f.write("test") +18 + Path(foo()).write_text("test") +19 | +20 | p = Path("file.txt") +21 | with p.open("w") as f: + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `p.write_text("test")` + --> FURB103_1.py:22:6 + | +21 | p = Path("file.txt") +22 | with p.open("w") as f: + | ^^^^^^^^^^^^^^^^ +23 | f.write("test") + | +help: Replace with `p.write_text("test")` +19 | f.write("test") +20 | +21 | p = Path("file.txt") + - with p.open("w") as f: + - f.write("test") +22 + p.write_text("test") +23 | +24 | with Path("foo", "bar", "baz").open("w") as f: +25 | f.write("test") + +FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("foo", "bar", "baz").write_text("test")` + --> FURB103_1.py:25:6 + | +23 | f.write("test") +24 | +25 | with Path("foo", "bar", "baz").open("w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +26 | f.write("test") + | +help: Replace with `Path("foo", "bar", "baz").write_text("test")` +22 | with p.open("w") as f: +23 | f.write("test") +24 | + - with Path("foo", "bar", "baz").open("w") as f: + - f.write("test") +25 + Path("foo", "bar", "baz").write_text("test") diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap index 7a5ae0c747..eaa3066929 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__write_whole_file_python_39.snap @@ -2,7 +2,7 @@ source: crates/ruff_linter/src/rules/refurb/mod.rs --- FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text("test")` - --> FURB103.py:12:6 + --> FURB103_0.py:12:6 | 11 | # FURB103 12 | with open("file.txt", "w") as f: @@ -26,7 +26,7 @@ help: Replace with `Path("file.txt").write_text("test")` 16 | with open("file.txt", "wb") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` - --> FURB103.py:16:6 + --> FURB103_0.py:16:6 | 15 | # FURB103 16 | with open("file.txt", "wb") as f: @@ -50,7 +50,7 @@ help: Replace with `Path("file.txt").write_bytes(foobar)` 20 | with open("file.txt", mode="wb") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")` - --> FURB103.py:20:6 + --> FURB103_0.py:20:6 | 19 | # FURB103 20 | with open("file.txt", mode="wb") as f: @@ -74,7 +74,7 @@ help: Replace with `Path("file.txt").write_bytes(b"abc")` 24 | with open("file.txt", "w", encoding="utf8") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")` - --> FURB103.py:24:6 + --> FURB103_0.py:24:6 | 23 | # FURB103 24 | with open("file.txt", "w", encoding="utf8") as f: @@ -98,7 +98,7 @@ help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")` 28 | with open("file.txt", "w", errors="ignore") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")` - --> FURB103.py:28:6 + --> FURB103_0.py:28:6 | 27 | # FURB103 28 | with open("file.txt", "w", errors="ignore") as f: @@ -122,7 +122,7 @@ help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")` 32 | with open("file.txt", mode="w") as f: FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` - --> FURB103.py:32:6 + --> FURB103_0.py:32:6 | 31 | # FURB103 32 | with open("file.txt", mode="w") as f: @@ -146,7 +146,7 @@ help: Replace with `Path("file.txt").write_text(foobar)` 36 | with open(foo(), "wb") as f: FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` - --> FURB103.py:36:6 + --> FURB103_0.py:36:6 | 35 | # FURB103 36 | with open(foo(), "wb") as f: @@ -157,7 +157,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar()) help: Replace with `Path(foo()).write_bytes(bar())` FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` - --> FURB103.py:44:6 + --> FURB103_0.py:44:6 | 43 | # FURB103 44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: @@ -168,7 +168,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` help: Replace with `Path("a.txt").write_text(x)` FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` - --> FURB103.py:44:31 + --> FURB103_0.py:44:31 | 43 | # FURB103 44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: @@ -179,7 +179,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` help: Replace with `Path("b.txt").write_bytes(y)` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(bar(bar(a + x)))` - --> FURB103.py:49:18 + --> FURB103_0.py:49:18 | 48 | # FURB103 49 | with foo() as a, open("file.txt", "w") as b, foo() as c: @@ -190,7 +190,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))` FURB103 [*] `open` and `write` should be replaced by `Path("test.json")....` - --> FURB103.py:154:6 + --> FURB103_0.py:154:6 | 152 | data = {"price": 100} 153 | @@ -214,7 +214,7 @@ help: Replace with `Path("test.json")....` 158 | with open("tmp_path/pyproject.toml", "w") as f: FURB103 [*] `open` and `write` should be replaced by `Path("tmp_path/pyproject.toml")....` - --> FURB103.py:158:6 + --> FURB103_0.py:158:6 | 157 | # See: https://github.com/astral-sh/ruff/issues/21381 158 | with open("tmp_path/pyproject.toml", "w") as f: