diff --git a/crates/ruff_linter/src/preview.rs b/crates/ruff_linter/src/preview.rs index 8266a5b4a8596a..a5647d5ea06387 100644 --- a/crates/ruff_linter/src/preview.rs +++ b/crates/ruff_linter/src/preview.rs @@ -259,3 +259,13 @@ pub(crate) const fn is_b006_unsafe_fix_preserve_assignment_expr_enabled( ) -> bool { settings.preview.is_enabled() } + +// https://github.com/astral-sh/ruff/pull/20520 +pub(crate) const fn is_fix_read_whole_file_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} + +// https://github.com/astral-sh/ruff/pull/20520 +pub(crate) const fn is_fix_write_whole_file_enabled(settings: &LinterSettings) -> bool { + settings.preview.is_enabled() +} diff --git a/crates/ruff_linter/src/rules/refurb/helpers.rs b/crates/ruff_linter/src/rules/refurb/helpers.rs index 90a624a0553e8a..0a09d70aba8bb9 100644 --- a/crates/ruff_linter/src/rules/refurb/helpers.rs +++ b/crates/ruff_linter/src/rules/refurb/helpers.rs @@ -1,14 +1,13 @@ use std::borrow::Cow; -use ruff_python_ast::name::Name; -use ruff_python_ast::{self as ast, Expr, parenthesize::parenthesized_range}; +use ruff_python_ast::PythonVersion; +use ruff_python_ast::{self as ast, Expr, name::Name, parenthesize::parenthesized_range}; use ruff_python_codegen::Generator; use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; use crate::{Applicability, Edit, Fix}; -use ruff_python_ast::PythonVersion; /// Format a code snippet to call `name.method()`. pub(super) fn generate_method_call(name: Name, method: &str, generator: Generator) -> String { @@ -345,12 +344,8 @@ pub(super) fn parenthesize_loop_iter_if_necessary<'a>( let iter_in_source = locator.slice(iter); match iter { - ast::Expr::Tuple(tuple) if !tuple.parenthesized => { - Cow::Owned(format!("({iter_in_source})")) - } - ast::Expr::Lambda(_) | ast::Expr::If(_) - if matches!(location, IterLocation::Comprehension) => - { + Expr::Tuple(tuple) if !tuple.parenthesized => Cow::Owned(format!("({iter_in_source})")), + Expr::Lambda(_) | Expr::If(_) if matches!(location, IterLocation::Comprehension) => { Cow::Owned(format!("({iter_in_source})")) } _ => Cow::Borrowed(iter_in_source), diff --git a/crates/ruff_linter/src/rules/refurb/mod.rs b/crates/ruff_linter/src/rules/refurb/mod.rs index 918785314138da..97d9fae7a6d11e 100644 --- a/crates/ruff_linter/src/rules/refurb/mod.rs +++ b/crates/ruff_linter/src/rules/refurb/mod.rs @@ -12,6 +12,7 @@ mod tests { use test_case::test_case; use crate::registry::Rule; + use crate::settings::types::PreviewMode; use crate::test::test_path; use crate::{assert_diagnostics, settings}; @@ -62,6 +63,25 @@ mod tests { Ok(()) } + #[test_case(Rule::ReadWholeFile, Path::new("FURB101.py"))] + #[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))] + fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> { + let snapshot = format!( + "preview_{}_{}", + rule_code.noqa_code(), + path.to_string_lossy() + ); + let diagnostics = test_path( + Path::new("refurb").join(path).as_path(), + &settings::LinterSettings { + preview: PreviewMode::Enabled, + ..settings::LinterSettings::for_rule(rule_code) + }, + )?; + assert_diagnostics!(snapshot, diagnostics); + Ok(()) + } + #[test] fn write_whole_file_python_39() -> Result<()> { let diagnostics = test_path( 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 af21f4f2472d7e..365f9bf112302c 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 @@ -1,14 +1,17 @@ +use ruff_diagnostics::{Applicability, Edit, Fix}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::visitor::{self, Visitor}; -use ruff_python_ast::{self as ast, Expr}; +use ruff_python_ast::{ + self as ast, Expr, Stmt, + visitor::{self, Visitor}, +}; use ruff_python_codegen::Generator; use ruff_text_size::{Ranged, TextRange}; -use crate::Violation; 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::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of `open` and `read` that can be replaced by `pathlib` @@ -31,6 +34,8 @@ use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; /// /// contents = Path(filename).read_text() /// ``` +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. /// /// ## References /// - [Python documentation: `Path.read_bytes`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.read_bytes) @@ -42,12 +47,22 @@ pub(crate) struct ReadWholeFile { } impl Violation for ReadWholeFile { + 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 `read` 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(), + )) + } } /// FURB101 @@ -64,7 +79,7 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) { } // Then we need to match each `open` operation with exactly one `read` call. - let mut matcher = ReadMatcher::new(checker, candidates); + let mut matcher = ReadMatcher::new(checker, candidates, with); visitor::walk_body(&mut matcher, &with.body); } @@ -72,13 +87,19 @@ pub(crate) fn read_whole_file(checker: &Checker, with: &ast::StmtWith) { struct ReadMatcher<'a, 'b> { checker: &'a Checker<'b>, candidates: Vec>, + with_stmt: &'a ast::StmtWith, } impl<'a, 'b> ReadMatcher<'a, 'b> { - fn new(checker: &'a Checker<'b>, candidates: Vec>) -> Self { + fn new( + checker: &'a Checker<'b>, + candidates: Vec>, + with_stmt: &'a ast::StmtWith, + ) -> Self { Self { checker, candidates, + with_stmt, } } } @@ -92,15 +113,38 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> { .position(|open| open.is_ref(read_from)) { let open = self.candidates.remove(open); - self.checker.report_diagnostic( + 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), ), - suggestion: make_suggestion(&open, self.checker.generator()), + suggestion: SourceCodeSnippet::from_str(&suggestion), }, open.item.range(), ); + + if !crate::preview::is_fix_read_whole_file_enabled(self.checker.settings()) { + return; + } + + let target = match self.with_stmt.body.first() { + Some(Stmt::Assign(assign)) + if assign.value.range().contains_range(expr.range()) => + { + match assign.targets.first() { + Some(Expr::Name(name)) => Some(name.id.as_str()), + _ => None, + } + } + _ => None, + }; + + if let Some(fix) = + generate_fix(self.checker, &open, target, self.with_stmt, &suggestion) + { + diagnostic.set_fix(fix); + } } return; } @@ -125,7 +169,7 @@ fn match_read_call(expr: &Expr) -> Option<&Expr> { Some(&*attr.value) } -fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> SourceCodeSnippet { +fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> String { let name = ast::ExprName { id: open.mode.pathlib_method(), ctx: ast::ExprContext::Load, @@ -143,5 +187,46 @@ fn make_suggestion(open: &FileOpen<'_>, generator: Generator) -> SourceCodeSnipp range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; - SourceCodeSnippet::from_str(&generator.expr(&call.into())) + generator.expr(&call.into()) +} + +fn generate_fix( + checker: &Checker, + open: &FileOpen, + target: Option<&str>, + with_stmt: &ast::StmtWith, + suggestion: &str, +) -> Option { + if !(with_stmt.items.len() == 1 && matches!(with_stmt.body.as_slice(), [Stmt::Assign(_)])) { + return None; + } + + let locator = checker.locator(); + let filename_code = locator.slice(open.filename.range()); + + let (import_edit, binding) = checker + .importer() + .get_or_import_symbol( + &ImportRequest::import("pathlib", "Path"), + with_stmt.start(), + checker.semantic(), + ) + .ok()?; + + let replacement = match target { + Some(var) => format!("{var} = {binding}({filename_code}).{suggestion}"), + None => format!("{binding}({filename_code}).{suggestion}"), + }; + + let applicability = if checker.comment_ranges().intersects(with_stmt.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + Some(Fix::applicable_edits( + Edit::range_replacement(replacement, with_stmt.range()), + [import_edit], + applicability, + )) } 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 ab3a3f24806d2a..310f4babf5ecd7 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 @@ -1,15 +1,19 @@ +use ruff_diagnostics::{Applicability, Edit, Fix}; use ruff_macros::{ViolationMetadata, derive_message_formats}; -use ruff_python_ast::relocate::relocate_expr; -use ruff_python_ast::visitor::{self, Visitor}; -use ruff_python_ast::{self as ast, Expr, Stmt}; +use ruff_python_ast::{ + self as ast, Expr, Stmt, + relocate::relocate_expr, + visitor::{self, Visitor}, +}; + use ruff_python_codegen::Generator; use ruff_text_size::{Ranged, TextRange}; -use crate::Violation; 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::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of `open` and `write` that can be replaced by `pathlib` @@ -33,6 +37,9 @@ use crate::rules::refurb::helpers::{FileOpen, find_file_opens}; /// Path(filename).write_text(contents) /// ``` /// +/// ## Fix Safety +/// This rule's fix is marked as unsafe if the replacement would remove comments attached to the original expression. +/// /// ## References /// - [Python documentation: `Path.write_bytes`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_bytes) /// - [Python documentation: `Path.write_text`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.write_text) @@ -43,12 +50,21 @@ pub(crate) struct 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}`") } + fn fix_title(&self) -> Option { + Some(format!( + "Replace with `Path({}).{}`", + self.filename.truncated_display(), + self.suggestion.truncated_display(), + )) + } } /// FURB103 @@ -65,7 +81,7 @@ pub(crate) fn write_whole_file(checker: &Checker, with: &ast::StmtWith) { } // Then we need to match each `open` operation with exactly one `write` call. - let mut matcher = WriteMatcher::new(checker, candidates); + let mut matcher = WriteMatcher::new(checker, candidates, with); visitor::walk_body(&mut matcher, &with.body); } @@ -74,21 +90,27 @@ struct WriteMatcher<'a, 'b> { checker: &'a Checker<'b>, candidates: Vec>, loop_counter: u32, + with_stmt: &'a ast::StmtWith, } impl<'a, 'b> WriteMatcher<'a, 'b> { - fn new(checker: &'a Checker<'b>, candidates: Vec>) -> Self { + fn new( + checker: &'a Checker<'b>, + candidates: Vec>, + with_stmt: &'a ast::StmtWith, + ) -> Self { Self { checker, candidates, loop_counter: 0, + with_stmt, } } } impl<'a> Visitor<'a> for WriteMatcher<'a, '_> { fn visit_stmt(&mut self, stmt: &'a Stmt) { - if matches!(stmt, ast::Stmt::While(_) | ast::Stmt::For(_)) { + if matches!(stmt, Stmt::While(_) | Stmt::For(_)) { self.loop_counter += 1; visitor::walk_stmt(self, stmt); self.loop_counter -= 1; @@ -104,19 +126,30 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> { .iter() .position(|open| open.is_ref(write_to)) { + let open = self.candidates.remove(open); + if self.loop_counter == 0 { - let open = self.candidates.remove(open); - self.checker.report_diagnostic( + let suggestion = make_suggestion(&open, content, self.checker.generator()); + + let mut diagnostic = self.checker.report_diagnostic( WriteWholeFile { filename: SourceCodeSnippet::from_str( &self.checker.generator().expr(open.filename), ), - suggestion: make_suggestion(&open, content, self.checker.generator()), + suggestion: SourceCodeSnippet::from_str(&suggestion), }, open.item.range(), ); - } else { - self.candidates.remove(open); + + if !crate::preview::is_fix_write_whole_file_enabled(self.checker.settings()) { + return; + } + + if let Some(fix) = + generate_fix(self.checker, &open, self.with_stmt, &suggestion) + { + diagnostic.set_fix(fix); + } } } return; @@ -143,7 +176,7 @@ fn match_write_call(expr: &Expr) -> Option<(&Expr, &Expr)> { Some((&*attr.value, call.arguments.args.first()?)) } -fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> SourceCodeSnippet { +fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> String { let name = ast::ExprName { id: open.mode.pathlib_method(), ctx: ast::ExprContext::Load, @@ -163,5 +196,42 @@ fn make_suggestion(open: &FileOpen<'_>, arg: &Expr, generator: Generator) -> Sou range: TextRange::default(), node_index: ruff_python_ast::AtomicNodeIndex::NONE, }; - SourceCodeSnippet::from_str(&generator.expr(&call.into())) + generator.expr(&call.into()) +} + +fn generate_fix( + checker: &Checker, + open: &FileOpen, + with_stmt: &ast::StmtWith, + suggestion: &str, +) -> Option { + if !(with_stmt.items.len() == 1 && matches!(with_stmt.body.as_slice(), [Stmt::Expr(_)])) { + return None; + } + + let locator = checker.locator(); + let filename_code = locator.slice(open.filename.range()); + + let (import_edit, binding) = checker + .importer() + .get_or_import_symbol( + &ImportRequest::import("pathlib", "Path"), + with_stmt.start(), + checker.semantic(), + ) + .ok()?; + + let replacement = format!("{binding}({filename_code}).{suggestion}"); + + let applicability = if checker.comment_ranges().intersects(with_stmt.range()) { + Applicability::Unsafe + } else { + Applicability::Safe + }; + + Some(Fix::applicable_edits( + Edit::range_replacement(replacement, with_stmt.range()), + [import_edit], + applicability, + )) } diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap index c5a417db0dab78..3f851c3f128338 100644 --- a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__FURB101_FURB101.py.snap @@ -9,6 +9,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` | ^^^^^^^^^^^^^^^^^^^^^ 13 | x = f.read() | +help: Replace with `Path("file.txt").read_text()` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_bytes()` --> FURB101.py:16:6 @@ -18,6 +19,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_bytes()` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | x = f.read() | +help: Replace with `Path("file.txt").read_bytes()` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_bytes()` --> FURB101.py:20:6 @@ -27,6 +29,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_bytes()` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 | x = f.read() | +help: Replace with `Path("file.txt").read_bytes()` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf8")` --> FURB101.py:24:6 @@ -36,6 +39,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(enco | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | x = f.read() | +help: Replace with `Path("file.txt").read_text(encoding="utf8")` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(errors="ignore")` --> FURB101.py:28:6 @@ -45,6 +49,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text(erro | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | x = f.read() | +help: Replace with `Path("file.txt").read_text(errors="ignore")` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` --> FURB101.py:32:6 @@ -54,6 +59,7 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | x = f.read() | +help: Replace with `Path("file.txt").read_text()` FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()` --> FURB101.py:36:6 @@ -64,6 +70,7 @@ FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()` 37 | # The body of `with` is non-trivial, but the recommendation holds. 38 | bar("pre") | +help: Replace with `Path(foo()).read_bytes()` FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()` --> FURB101.py:44:6 @@ -74,6 +81,7 @@ FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()` 45 | x = a.read() 46 | y = b.read() | +help: Replace with `Path("a.txt").read_text()` FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()` --> FURB101.py:44:26 @@ -84,6 +92,7 @@ FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()` 45 | x = a.read() 46 | y = b.read() | +help: Replace with `Path("b.txt").read_bytes()` FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` --> FURB101.py:49:18 @@ -94,3 +103,4 @@ FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` 50 | # We have other things in here, multiple with items, but 51 | # the user reads the whole file and that bit they can replace. | +help: Replace with `Path("file.txt").read_text()` 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.py.snap index 908ac8f6f7dcb2..dfb111341ecbdd 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.py.snap @@ -9,6 +9,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text("t | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | f.write("test") | +help: Replace with `Path("file.txt").write_text("test")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` --> FURB103.py:16:6 @@ -18,6 +19,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(f | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | f.write(foobar) | +help: Replace with `Path("file.txt").write_bytes(foobar)` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")` --> FURB103.py:20:6 @@ -27,6 +29,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(b | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 | f.write(b"abc") | +help: Replace with `Path("file.txt").write_bytes(b"abc")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")` --> FURB103.py:24:6 @@ -36,6 +39,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")` --> FURB103.py:28:6 @@ -45,6 +49,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` --> FURB103.py:32:6 @@ -54,6 +59,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar)` FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` --> FURB103.py:36:6 @@ -64,6 +70,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar()) 37 | # The body of `with` is non-trivial, but the recommendation holds. 38 | bar("pre") | +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 @@ -74,6 +81,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` 45 | a.write(x) 46 | b.write(y) | +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 @@ -84,6 +92,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` 45 | a.write(x) 46 | b.write(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 @@ -94,6 +103,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba 50 | # We have other things in here, multiple with items, but the user 51 | # writes a single time to file and that bit they can replace. | +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 @@ -103,6 +113,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 59 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` --> FURB103.py:66:6 @@ -112,6 +123,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 67 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` --> FURB103.py:74:6 @@ -121,3 +133,4 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 75 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB101_FURB101.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB101_FURB101.py.snap new file mode 100644 index 00000000000000..4131499c0cf35a --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB101_FURB101.py.snap @@ -0,0 +1,191 @@ +--- +source: crates/ruff_linter/src/rules/refurb/mod.rs +--- +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()` + --> FURB101.py:12:6 + | +11 | # FURB101 +12 | with open("file.txt") as f: + | ^^^^^^^^^^^^^^^^^^^^^ +13 | x = f.read() + | +help: Replace with `Path("file.txt").read_text()` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +10 | # Errors. +11 | +12 | # FURB101 + - with open("file.txt") as f: + - x = f.read() +13 + x = pathlib.Path("file.txt").read_text() +14 | +15 | # FURB101 +16 | with open("file.txt", "rb") as f: + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()` + --> FURB101.py:16:6 + | +15 | # FURB101 +16 | with open("file.txt", "rb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | x = f.read() + | +help: Replace with `Path("file.txt").read_bytes()` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +14 | x = f.read() +15 | +16 | # FURB101 + - with open("file.txt", "rb") as f: + - x = f.read() +17 + x = pathlib.Path("file.txt").read_bytes() +18 | +19 | # FURB101 +20 | with open("file.txt", mode="rb") as f: + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_bytes()` + --> FURB101.py:20:6 + | +19 | # FURB101 +20 | with open("file.txt", mode="rb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | x = f.read() + | +help: Replace with `Path("file.txt").read_bytes()` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +18 | x = f.read() +19 | +20 | # FURB101 + - with open("file.txt", mode="rb") as f: + - x = f.read() +21 + x = pathlib.Path("file.txt").read_bytes() +22 | +23 | # FURB101 +24 | with open("file.txt", encoding="utf8") as f: + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(encoding="utf8")` + --> FURB101.py:24:6 + | +23 | # FURB101 +24 | with open("file.txt", encoding="utf8") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +25 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(encoding="utf8")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +22 | x = f.read() +23 | +24 | # FURB101 + - with open("file.txt", encoding="utf8") as f: + - x = f.read() +25 + x = pathlib.Path("file.txt").read_text(encoding="utf8") +26 | +27 | # FURB101 +28 | with open("file.txt", errors="ignore") as f: + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text(errors="ignore")` + --> FURB101.py:28:6 + | +27 | # FURB101 +28 | with open("file.txt", errors="ignore") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +29 | x = f.read() + | +help: Replace with `Path("file.txt").read_text(errors="ignore")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +26 | x = f.read() +27 | +28 | # FURB101 + - with open("file.txt", errors="ignore") as f: + - x = f.read() +29 + x = pathlib.Path("file.txt").read_text(errors="ignore") +30 | +31 | # FURB101 +32 | with open("file.txt", mode="r") as f: # noqa: FURB120 + +FURB101 [*] `open` and `read` should be replaced by `Path("file.txt").read_text()` + --> FURB101.py:32:6 + | +31 | # FURB101 +32 | with open("file.txt", mode="r") as f: # noqa: FURB120 + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +33 | x = f.read() + | +help: Replace with `Path("file.txt").read_text()` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +30 | x = f.read() +31 | +32 | # FURB101 + - with open("file.txt", mode="r") as f: # noqa: FURB120 + - x = f.read() +33 + x = pathlib.Path("file.txt").read_text() +34 | +35 | # FURB101 +36 | with open(foo(), "rb") as f: +note: This is an unsafe fix and may change runtime behavior + +FURB101 `open` and `read` should be replaced by `Path(foo()).read_bytes()` + --> FURB101.py:36:6 + | +35 | # FURB101 +36 | with open(foo(), "rb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^ +37 | # The body of `with` is non-trivial, but the recommendation holds. +38 | bar("pre") + | +help: Replace with `Path(foo()).read_bytes()` + +FURB101 `open` and `read` should be replaced by `Path("a.txt").read_text()` + --> FURB101.py:44:6 + | +43 | # FURB101 +44 | with open("a.txt") as a, open("b.txt", "rb") as b: + | ^^^^^^^^^^^^^^^^^^ +45 | x = a.read() +46 | y = b.read() + | +help: Replace with `Path("a.txt").read_text()` + +FURB101 `open` and `read` should be replaced by `Path("b.txt").read_bytes()` + --> FURB101.py:44:26 + | +43 | # FURB101 +44 | with open("a.txt") as a, open("b.txt", "rb") as b: + | ^^^^^^^^^^^^^^^^^^^^^^^^ +45 | x = a.read() +46 | y = b.read() + | +help: Replace with `Path("b.txt").read_bytes()` + +FURB101 `open` and `read` should be replaced by `Path("file.txt").read_text()` + --> FURB101.py:49:18 + | +48 | # FURB101 +49 | with foo() as a, open("file.txt") as b, foo() as c: + | ^^^^^^^^^^^^^^^^^^^^^ +50 | # We have other things in here, multiple with items, but +51 | # the user reads the whole file and that bit they can replace. + | +help: Replace with `Path("file.txt").read_text()` diff --git a/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap new file mode 100644 index 00000000000000..eef0992839565e --- /dev/null +++ b/crates/ruff_linter/src/rules/refurb/snapshots/ruff_linter__rules__refurb__tests__preview_FURB103_FURB103.py.snap @@ -0,0 +1,260 @@ +--- +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 + | +11 | # FURB103 +12 | with open("file.txt", "w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +13 | f.write("test") + | +help: Replace with `Path("file.txt").write_text("test")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +10 | # Errors. +11 | +12 | # FURB103 + - with open("file.txt", "w") as f: + - f.write("test") +13 + pathlib.Path("file.txt").write_text("test") +14 | +15 | # FURB103 +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 + | +15 | # FURB103 +16 | with open("file.txt", "wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +17 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_bytes(foobar)` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +14 | f.write("test") +15 | +16 | # FURB103 + - with open("file.txt", "wb") as f: + - f.write(foobar) +17 + pathlib.Path("file.txt").write_bytes(foobar) +18 | +19 | # FURB103 +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 + | +19 | # FURB103 +20 | with open("file.txt", mode="wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +21 | f.write(b"abc") + | +help: Replace with `Path("file.txt").write_bytes(b"abc")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +18 | f.write(foobar) +19 | +20 | # FURB103 + - with open("file.txt", mode="wb") as f: + - f.write(b"abc") +21 + pathlib.Path("file.txt").write_bytes(b"abc") +22 | +23 | # FURB103 +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 + | +23 | # FURB103 +24 | with open("file.txt", "w", encoding="utf8") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +25 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +22 | f.write(b"abc") +23 | +24 | # FURB103 + - with open("file.txt", "w", encoding="utf8") as f: + - f.write(foobar) +25 + pathlib.Path("file.txt").write_text(foobar, encoding="utf8") +26 | +27 | # FURB103 +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 + | +27 | # FURB103 +28 | with open("file.txt", "w", errors="ignore") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +29 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +26 | f.write(foobar) +27 | +28 | # FURB103 + - with open("file.txt", "w", errors="ignore") as f: + - f.write(foobar) +29 + pathlib.Path("file.txt").write_text(foobar, errors="ignore") +30 | +31 | # FURB103 +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 + | +31 | # FURB103 +32 | with open("file.txt", mode="w") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +33 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar)` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +30 | f.write(foobar) +31 | +32 | # FURB103 + - with open("file.txt", mode="w") as f: + - f.write(foobar) +33 + pathlib.Path("file.txt").write_text(foobar) +34 | +35 | # FURB103 +36 | with open(foo(), "wb") as f: + +FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` + --> FURB103.py:36:6 + | +35 | # FURB103 +36 | with open(foo(), "wb") as f: + | ^^^^^^^^^^^^^^^^^^^^^^ +37 | # The body of `with` is non-trivial, but the recommendation holds. +38 | bar("pre") + | +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 + | +43 | # FURB103 +44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: + | ^^^^^^^^^^^^^^^^^^^^^^^ +45 | a.write(x) +46 | b.write(y) + | +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 + | +43 | # FURB103 +44 | with open("a.txt", "w") as a, open("b.txt", "wb") as b: + | ^^^^^^^^^^^^^^^^^^^^^^^^ +45 | a.write(x) +46 | b.write(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 + | +48 | # FURB103 +49 | with foo() as a, open("file.txt", "w") as b, foo() as c: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ +50 | # We have other things in here, multiple with items, but the user +51 | # writes a single time to file and that bit they can replace. + | +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 + | +57 | # FURB103 +58 | with open("file.txt", "w", newline="\r\n") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +59 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` +1 + import pathlib +2 | def foo(): +3 | ... +4 | +-------------------------------------------------------------------------------- +56 | +57 | +58 | # FURB103 + - with open("file.txt", "w", newline="\r\n") as f: + - f.write(foobar) +59 + pathlib.Path("file.txt").write_text(foobar, newline="\r\n") +60 | +61 | +62 | import builtins + +FURB103 [*] `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, newline="\r\n")` + --> FURB103.py:66:6 + | +65 | # FURB103 +66 | with builtins.open("file.txt", "w", newline="\r\n") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +67 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` +60 | +61 | +62 | import builtins +63 + import pathlib +64 | +65 | +66 | # FURB103 + - with builtins.open("file.txt", "w", newline="\r\n") as f: + - f.write(foobar) +67 + pathlib.Path("file.txt").write_text(foobar, newline="\r\n") +68 | +69 | +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 + | +73 | # FURB103 +74 | with o("file.txt", "w", newline="\r\n") as f: + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +75 | f.write(foobar) + | +help: Replace with `Path("file.txt").write_text(foobar, newline="\r\n")` +68 | +69 | +70 | from builtins import open as o +71 + import pathlib +72 | +73 | +74 | # FURB103 + - with o("file.txt", "w", newline="\r\n") as f: + - f.write(foobar) +75 + pathlib.Path("file.txt").write_text(foobar, newline="\r\n") +76 | +77 | # Non-errors. +78 | 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 de6a020f87004e..81eea0c15909f9 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 @@ -9,6 +9,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text("t | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 13 | f.write("test") | +help: Replace with `Path("file.txt").write_text("test")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(foobar)` --> FURB103.py:16:6 @@ -18,6 +19,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(f | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 17 | f.write(foobar) | +help: Replace with `Path("file.txt").write_bytes(foobar)` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(b"abc")` --> FURB103.py:20:6 @@ -27,6 +29,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_bytes(b | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 21 | f.write(b"abc") | +help: Replace with `Path("file.txt").write_bytes(b"abc")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, encoding="utf8")` --> FURB103.py:24:6 @@ -36,6 +39,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 25 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, encoding="utf8")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar, errors="ignore")` --> FURB103.py:28:6 @@ -45,6 +49,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 29 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar, errors="ignore")` FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(foobar)` --> FURB103.py:32:6 @@ -54,6 +59,7 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(fo | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 33 | f.write(foobar) | +help: Replace with `Path("file.txt").write_text(foobar)` FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar())` --> FURB103.py:36:6 @@ -64,6 +70,7 @@ FURB103 `open` and `write` should be replaced by `Path(foo()).write_bytes(bar()) 37 | # The body of `with` is non-trivial, but the recommendation holds. 38 | bar("pre") | +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 @@ -74,6 +81,7 @@ FURB103 `open` and `write` should be replaced by `Path("a.txt").write_text(x)` 45 | a.write(x) 46 | b.write(y) | +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 @@ -84,6 +92,7 @@ FURB103 `open` and `write` should be replaced by `Path("b.txt").write_bytes(y)` 45 | a.write(x) 46 | b.write(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 @@ -94,3 +103,4 @@ FURB103 `open` and `write` should be replaced by `Path("file.txt").write_text(ba 50 | # We have other things in here, multiple with items, but the user 51 | # writes a single time to file and that bit they can replace. | +help: Replace with `Path("file.txt").write_text(bar(bar(a + x)))`