[refurb] Add fixes for FURB101, FURB103 (#20520)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions

## Summary

Part of `PTH-*` fixes:
https://github.com/astral-sh/ruff/pull/19404#issuecomment-3089639686

## Test Plan
`cargo nextest run furb`
This commit is contained in:
chiri 2025-10-07 01:09:07 +03:00 committed by GitHub
parent 70f51e9648
commit b66a3e7451
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 698 additions and 34 deletions

View file

@ -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()
}

View file

@ -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),

View file

@ -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(

View file

@ -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<String> {
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<FileOpen<'a>>,
with_stmt: &'a ast::StmtWith,
}
impl<'a, 'b> ReadMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
fn new(
checker: &'a Checker<'b>,
candidates: Vec<FileOpen<'a>>,
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<Fix> {
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,
))
}

View file

@ -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<String> {
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<FileOpen<'a>>,
loop_counter: u32,
with_stmt: &'a ast::StmtWith,
}
impl<'a, 'b> WriteMatcher<'a, 'b> {
fn new(checker: &'a Checker<'b>, candidates: Vec<FileOpen<'a>>) -> Self {
fn new(
checker: &'a Checker<'b>,
candidates: Vec<FileOpen<'a>>,
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<Fix> {
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,
))
}

View file

@ -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()`

View file

@ -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")`

View file

@ -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()`

View file

@ -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 |

View file

@ -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)))`