mirror of https://github.com/astral-sh/ruff
introduce OpenArgument type
This commit is contained in:
parent
9b38bc776e
commit
fd9e0b17cd
|
|
@ -120,16 +120,13 @@ impl OpenMode {
|
||||||
pub(super) struct FileOpen<'a> {
|
pub(super) struct FileOpen<'a> {
|
||||||
/// With item where the open happens, we use it for the reporting range.
|
/// With item where the open happens, we use it for the reporting range.
|
||||||
pub(super) item: &'a ast::WithItem,
|
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: Option<&'a Expr>,
|
|
||||||
/// The file open mode.
|
/// The file open mode.
|
||||||
pub(super) mode: OpenMode,
|
pub(super) mode: OpenMode,
|
||||||
/// The file open keywords.
|
/// The file open keywords.
|
||||||
pub(super) keywords: Vec<&'a ast::Keyword>,
|
pub(super) keywords: Vec<&'a ast::Keyword>,
|
||||||
/// We only check `open` operations whose file handles are used exactly once.
|
/// We only check `open` operations whose file handles are used exactly once.
|
||||||
pub(super) reference: &'a ResolvedReference,
|
pub(super) reference: &'a ResolvedReference,
|
||||||
/// Pathlib Path object used to open the file, if any.
|
pub(super) argument: OpenArgument<'a>,
|
||||||
pub(super) path_obj: Option<&'a Expr>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileOpen<'_> {
|
impl FileOpen<'_> {
|
||||||
|
|
@ -140,6 +137,45 @@ impl FileOpen<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
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.
|
/// Find and return all `open` operations in the given `with` statement.
|
||||||
pub(super) fn find_file_opens<'a>(
|
pub(super) fn find_file_opens<'a>(
|
||||||
with: &'a ast::StmtWith,
|
with: &'a ast::StmtWith,
|
||||||
|
|
@ -156,16 +192,14 @@ pub(super) fn find_file_opens<'a>(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(clippy::too_many_arguments)]
|
|
||||||
fn resolve_file_open<'a>(
|
fn resolve_file_open<'a>(
|
||||||
item: &'a ast::WithItem,
|
item: &'a ast::WithItem,
|
||||||
with: &'a ast::StmtWith,
|
with: &'a ast::StmtWith,
|
||||||
semantic: &'a SemanticModel<'a>,
|
semantic: &'a SemanticModel<'a>,
|
||||||
read_mode: bool,
|
read_mode: bool,
|
||||||
filename: Option<&'a Expr>,
|
|
||||||
mode: OpenMode,
|
mode: OpenMode,
|
||||||
keywords: Vec<&'a ast::Keyword>,
|
keywords: Vec<&'a ast::Keyword>,
|
||||||
path_obj: Option<&'a Expr>,
|
argument: OpenArgument<'a>,
|
||||||
) -> Option<FileOpen<'a>> {
|
) -> Option<FileOpen<'a>> {
|
||||||
match mode {
|
match mode {
|
||||||
OpenMode::ReadText | OpenMode::ReadBytes => {
|
OpenMode::ReadText | OpenMode::ReadBytes => {
|
||||||
|
|
@ -203,11 +237,10 @@ fn resolve_file_open<'a>(
|
||||||
|
|
||||||
Some(FileOpen {
|
Some(FileOpen {
|
||||||
item,
|
item,
|
||||||
filename,
|
|
||||||
mode,
|
mode,
|
||||||
keywords,
|
keywords,
|
||||||
reference,
|
reference,
|
||||||
path_obj,
|
argument,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -251,10 +284,9 @@ fn find_file_open<'a>(
|
||||||
with,
|
with,
|
||||||
semantic,
|
semantic,
|
||||||
read_mode,
|
read_mode,
|
||||||
Some(filename),
|
|
||||||
mode,
|
mode,
|
||||||
keywords,
|
keywords,
|
||||||
None,
|
OpenArgument::Builtin { filename },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,15 +311,6 @@ fn find_path_open<'a>(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let attr = func.as_attribute_expr()?;
|
let attr = func.as_attribute_expr()?;
|
||||||
let filename = if let Expr::Call(path_call) = attr.value.as_ref() {
|
|
||||||
if path_call.arguments.args.len() == 1 {
|
|
||||||
path_call.arguments.args.first()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let mode = if args.is_empty() {
|
let mode = if args.is_empty() {
|
||||||
OpenMode::ReadText
|
OpenMode::ReadText
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -301,10 +324,11 @@ fn find_path_open<'a>(
|
||||||
with,
|
with,
|
||||||
semantic,
|
semantic,
|
||||||
read_mode,
|
read_mode,
|
||||||
filename,
|
|
||||||
mode,
|
mode,
|
||||||
keywords,
|
keywords,
|
||||||
Some(attr.value.as_ref()),
|
OpenArgument::Pathlib {
|
||||||
|
path: attr.value.as_ref(),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use ruff_text_size::{Ranged, TextRange};
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::snippet::SourceCodeSnippet;
|
use crate::fix::snippet::SourceCodeSnippet;
|
||||||
use crate::importer::ImportRequest;
|
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};
|
use crate::{FixAvailability, Violation};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
|
@ -114,17 +114,11 @@ impl<'a> Visitor<'a> for ReadMatcher<'a, '_> {
|
||||||
.position(|open| open.is_ref(read_from))
|
.position(|open| open.is_ref(read_from))
|
||||||
{
|
{
|
||||||
let open = self.candidates.remove(open);
|
let open = self.candidates.remove(open);
|
||||||
let filename_display = if let Some(filename) = open.filename {
|
let filename_display = open.argument.display(self.checker.source());
|
||||||
self.checker.generator().expr(filename)
|
|
||||||
} else if let Some(path_obj) = open.path_obj {
|
|
||||||
self.checker.locator().slice(path_obj.range()).to_string()
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let suggestion = make_suggestion(&open, self.checker.generator());
|
let suggestion = make_suggestion(&open, self.checker.generator());
|
||||||
let mut diagnostic = self.checker.report_diagnostic(
|
let mut diagnostic = self.checker.report_diagnostic(
|
||||||
ReadWholeFile {
|
ReadWholeFile {
|
||||||
filename: SourceCodeSnippet::from_str(&filename_display),
|
filename: SourceCodeSnippet::from_str(filename_display),
|
||||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||||
},
|
},
|
||||||
open.item.range(),
|
open.item.range(),
|
||||||
|
|
@ -190,7 +184,9 @@ fn generate_fix(
|
||||||
if with_stmt.items.len() != 1 {
|
if with_stmt.items.len() != 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let filename = open.filename?;
|
let OpenArgument::Builtin { filename } = open.argument else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
let locator = checker.locator();
|
let locator = checker.locator();
|
||||||
let filename_code = locator.slice(filename.range());
|
let filename_code = locator.slice(filename.range());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use ruff_text_size::Ranged;
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::fix::snippet::SourceCodeSnippet;
|
use crate::fix::snippet::SourceCodeSnippet;
|
||||||
use crate::importer::ImportRequest;
|
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};
|
use crate::{FixAvailability, Locator, Violation};
|
||||||
|
|
||||||
/// ## What it does
|
/// ## What it does
|
||||||
|
|
@ -46,7 +46,6 @@ pub(crate) struct WriteWholeFile {
|
||||||
filename: SourceCodeSnippet,
|
filename: SourceCodeSnippet,
|
||||||
suggestion: SourceCodeSnippet,
|
suggestion: SourceCodeSnippet,
|
||||||
is_path_open: bool,
|
is_path_open: bool,
|
||||||
has_filename: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Violation for WriteWholeFile {
|
impl Violation for WriteWholeFile {
|
||||||
|
|
@ -57,29 +56,23 @@ impl Violation for WriteWholeFile {
|
||||||
let filename = self.filename.truncated_display();
|
let filename = self.filename.truncated_display();
|
||||||
let suggestion = self.suggestion.truncated_display();
|
let suggestion = self.suggestion.truncated_display();
|
||||||
if self.is_path_open {
|
if self.is_path_open {
|
||||||
if self.has_filename {
|
format!(
|
||||||
format!(
|
"`Path.open()` followed by `write()` can be replaced by `{filename}.{suggestion}`"
|
||||||
"`Path().open()` followed by `write()` can be replaced by `Path({filename}).{suggestion}`"
|
)
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!(
|
|
||||||
"`Path.open()` followed by `write()` can be replaced by `{filename}.{suggestion}`"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
|
format!("`open` and `write` should be replaced by `Path({filename}).{suggestion}`")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn fix_title(&self) -> Option<String> {
|
fn fix_title(&self) -> Option<String> {
|
||||||
let formatted = if self.has_filename {
|
let formatted = if self.is_path_open {
|
||||||
format!(
|
format!(
|
||||||
"Replace with `Path({}).{}`",
|
"Replace with `{}.{}`",
|
||||||
self.filename.truncated_display(),
|
self.filename.truncated_display(),
|
||||||
self.suggestion.truncated_display(),
|
self.suggestion.truncated_display(),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"Replace with `{}.{}`",
|
"Replace with `Path({}).{}`",
|
||||||
self.filename.truncated_display(),
|
self.filename.truncated_display(),
|
||||||
self.suggestion.truncated_display(),
|
self.suggestion.truncated_display(),
|
||||||
)
|
)
|
||||||
|
|
@ -149,27 +142,15 @@ impl<'a> Visitor<'a> for WriteMatcher<'a, '_> {
|
||||||
.position(|open| open.is_ref(write_to))
|
.position(|open| open.is_ref(write_to))
|
||||||
{
|
{
|
||||||
let open = self.candidates.remove(open);
|
let open = self.candidates.remove(open);
|
||||||
let has_filename: bool;
|
|
||||||
let filename_display: String;
|
|
||||||
if self.loop_counter == 0 {
|
if self.loop_counter == 0 {
|
||||||
if let Some(filename) = open.filename {
|
let filename_display = open.argument.display(self.checker.source());
|
||||||
filename_display = self.checker.generator().expr(filename);
|
|
||||||
has_filename = true;
|
|
||||||
} else if let Some(path_obj) = open.path_obj {
|
|
||||||
filename_display =
|
|
||||||
self.checker.locator().slice(path_obj.range()).to_string();
|
|
||||||
has_filename = false;
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let suggestion = make_suggestion(&open, content, self.checker.locator());
|
let suggestion = make_suggestion(&open, content, self.checker.locator());
|
||||||
|
|
||||||
let mut diagnostic = self.checker.report_diagnostic(
|
let mut diagnostic = self.checker.report_diagnostic(
|
||||||
WriteWholeFile {
|
WriteWholeFile {
|
||||||
filename: SourceCodeSnippet::from_str(&filename_display),
|
filename: SourceCodeSnippet::from_str(filename_display),
|
||||||
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
suggestion: SourceCodeSnippet::from_str(&suggestion),
|
||||||
is_path_open: open.path_obj.is_some(),
|
is_path_open: matches!(open.argument, OpenArgument::Pathlib { .. }),
|
||||||
has_filename,
|
|
||||||
},
|
},
|
||||||
open.item.range(),
|
open.item.range(),
|
||||||
);
|
);
|
||||||
|
|
@ -243,12 +224,12 @@ fn generate_fix(
|
||||||
)
|
)
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let target = if let Some(path_obj) = open.path_obj {
|
let target = match open.argument {
|
||||||
locator.slice(path_obj.range()).to_string()
|
OpenArgument::Builtin { filename } => {
|
||||||
} else {
|
let filename_code = locator.slice(filename.range());
|
||||||
let filename = open.filename?;
|
format!("{binding}({filename_code})")
|
||||||
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 replacement = format!("{target}.{suggestion}");
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
---
|
---
|
||||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
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 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
|
||||||
--> FURB103_1.py:3:6
|
--> FURB103_1.py:3:6
|
||||||
|
|
|
|
||||||
1 | from pathlib import Path
|
1 | from pathlib import Path
|
||||||
|
|
@ -20,7 +20,7 @@ help: Replace with `Path("file.txt").write_text("test")`
|
||||||
5 | with Path("file.txt").open("wb") as f:
|
5 | with Path("file.txt").open("wb") as f:
|
||||||
6 | f.write(b"test")
|
6 | f.write(b"test")
|
||||||
|
|
||||||
FURB103 [*] `Path().open()` followed by `write()` can be replaced by `Path("file.txt").write_bytes(b"test")`
|
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_bytes(b"test")`
|
||||||
--> FURB103_1.py:6:6
|
--> FURB103_1.py:6:6
|
||||||
|
|
|
|
||||||
4 | f.write("test")
|
4 | f.write("test")
|
||||||
|
|
@ -40,7 +40,7 @@ help: Replace with `Path("file.txt").write_bytes(b"test")`
|
||||||
8 | with Path("file.txt").open(mode="w") as f:
|
8 | with Path("file.txt").open(mode="w") as f:
|
||||||
9 | f.write("test")
|
9 | f.write("test")
|
||||||
|
|
||||||
FURB103 [*] `Path().open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
|
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test")`
|
||||||
--> FURB103_1.py:9:6
|
--> FURB103_1.py:9:6
|
||||||
|
|
|
|
||||||
7 | f.write(b"test")
|
7 | f.write(b"test")
|
||||||
|
|
@ -60,7 +60,7 @@ help: Replace with `Path("file.txt").write_text("test")`
|
||||||
11 | with Path("file.txt").open("w", encoding="utf8") as f:
|
11 | with Path("file.txt").open("w", encoding="utf8") as f:
|
||||||
12 | f.write("test")
|
12 | f.write("test")
|
||||||
|
|
||||||
FURB103 [*] `Path().open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", encoding="utf8")`
|
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", encoding="utf8")`
|
||||||
--> FURB103_1.py:12:6
|
--> FURB103_1.py:12:6
|
||||||
|
|
|
|
||||||
10 | f.write("test")
|
10 | f.write("test")
|
||||||
|
|
@ -80,7 +80,7 @@ help: Replace with `Path("file.txt").write_text("test", encoding="utf8")`
|
||||||
14 | with Path("file.txt").open("w", errors="ignore") as f:
|
14 | with Path("file.txt").open("w", errors="ignore") as f:
|
||||||
15 | f.write("test")
|
15 | f.write("test")
|
||||||
|
|
||||||
FURB103 [*] `Path().open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", errors="ignore")`
|
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path("file.txt").write_text("test", errors="ignore")`
|
||||||
--> FURB103_1.py:15:6
|
--> FURB103_1.py:15:6
|
||||||
|
|
|
|
||||||
13 | f.write("test")
|
13 | f.write("test")
|
||||||
|
|
@ -100,7 +100,7 @@ help: Replace with `Path("file.txt").write_text("test", errors="ignore")`
|
||||||
17 | with Path(foo()).open("w") as f:
|
17 | with Path(foo()).open("w") as f:
|
||||||
18 | f.write("test")
|
18 | f.write("test")
|
||||||
|
|
||||||
FURB103 [*] `Path().open()` followed by `write()` can be replaced by `Path(foo()).write_text("test")`
|
FURB103 [*] `Path.open()` followed by `write()` can be replaced by `Path(foo()).write_text("test")`
|
||||||
--> FURB103_1.py:18:6
|
--> FURB103_1.py:18:6
|
||||||
|
|
|
|
||||||
16 | f.write("test")
|
16 | f.write("test")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue