diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs index 15257a3bb11e4..784b4a132e78e 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/rules/replaceable_by_pathlib.rs @@ -1,7 +1,9 @@ +use ruff_diagnostics::Applicability; use ruff_python_ast::{Expr, ExprCall}; use ruff_text_size::Ranged; use crate::checkers::ast::Checker; +use crate::importer::ImportRequest; use crate::rules::flake8_use_pathlib::helpers::{ is_file_descriptor, is_keyword_only_argument_non_default, }; @@ -9,6 +11,7 @@ use crate::rules::flake8_use_pathlib::{ rules::Glob, violations::{Joiner, OsListdir, OsPathJoin, OsPathSplitext, PyPath}, }; +use crate::{Edit, Fix}; pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { let Some(qualified_name) = checker.semantic().resolve_qualified_name(&call.func) else { @@ -18,32 +21,40 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { let range = call.func.range(); match qualified_name.segments() { // PTH118 - ["os", "path", "join"] => checker.report_diagnostic_if_enabled( - OsPathJoin { - module: "path".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash + ["os", "path", "join"] => { + checker.report_diagnostic_if_enabled( + OsPathJoin { + module: "path".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, }, - }, - range, - ), - ["os", "sep", "join"] => checker.report_diagnostic_if_enabled( - OsPathJoin { - module: "sep".to_string(), - joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { - Joiner::Joinpath - } else { - Joiner::Slash + range, + ); + } + ["os", "sep", "join"] => { + checker.report_diagnostic_if_enabled( + OsPathJoin { + module: "sep".to_string(), + joiner: if call.arguments.args.iter().any(Expr::is_starred_expr) { + Joiner::Joinpath + } else { + Joiner::Slash + }, }, - }, - range, - ), + range, + ); + } // PTH122 - ["os", "path", "splitext"] => checker.report_diagnostic_if_enabled(OsPathSplitext, range), + ["os", "path", "splitext"] => { + checker.report_diagnostic_if_enabled(OsPathSplitext, range); + } // PTH124 - ["py", "path", "local"] => checker.report_diagnostic_if_enabled(PyPath, range), + ["py", "path", "local"] => { + checker.report_diagnostic_if_enabled(PyPath, range); + } // PTH207 ["glob", "glob"] => { // `dir_fd` is not supported by pathlib, so check if it's set to non-default values. @@ -61,7 +72,7 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { function: "glob".to_string(), }, range, - ) + ); } ["glob", "iglob"] => { @@ -80,20 +91,37 @@ pub(crate) fn replaceable_by_pathlib(checker: &Checker, call: &ExprCall) { function: "iglob".to_string(), }, range, - ) + ); } // PTH208 ["os", "listdir"] => { - if call - .arguments - .find_argument_value("path", 0) - .is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) - { + let path = call.arguments.find_argument_value("path", 0); + if path.is_some_and(|expr| is_file_descriptor(expr, checker.semantic())) { return; } - checker.report_diagnostic_if_enabled(OsListdir, range) + + if let Some(mut diagnostic) = checker.report_diagnostic_if_enabled(OsListdir, range) { + if let Some(path) = path { + diagnostic.try_set_fix(|| { + let (import_edit, binding) = checker.importer().get_or_import_symbol( + &ImportRequest::import("pathlib", "Path"), + call.start(), + checker.semantic(), + )?; + + Ok(Fix::applicable_edits( + Edit::range_replacement( + format!("{binding}({}).iterdir()", checker.locator().slice(path)), + call.range(), + ), + [import_edit], + Applicability::DisplayOnly, + )) + }); + } + } } - _ => return, - }; + _ => {} + } } diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__os-listdir_PTH208.py.snap b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__os-listdir_PTH208.py.snap index 9aeb2ced0d577..e88620ed4c81b 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__os-listdir_PTH208.py.snap +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/snapshots/ruff_linter__rules__flake8_use_pathlib__tests__os-listdir_PTH208.py.snap @@ -1,7 +1,7 @@ --- source: crates/ruff_linter/src/rules/flake8_use_pathlib/mod.rs --- -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:3:1 | 1 | import os @@ -10,8 +10,18 @@ PTH208 Use `pathlib.Path.iterdir()` instead. | ^^^^^^^^^^ 4 | os.listdir(b'.') | +help: Replace with `Path(...).iterdir()` + | +1 | import os +2 + import pathlib +3 | + - os.listdir('.') +4 + pathlib.Path('.').iterdir() +5 | os.listdir(b'.') + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:4:1 | 3 | os.listdir('.') @@ -20,8 +30,19 @@ PTH208 Use `pathlib.Path.iterdir()` instead. 5 | 6 | string_path = '.' | +help: Replace with `Path(...).iterdir()` + | +1 | import os +2 + import pathlib +3 | +4 | os.listdir('.') + - os.listdir(b'.') +5 + pathlib.Path(b'.').iterdir() +6 | + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:7:1 | 6 | string_path = '.' @@ -30,30 +51,70 @@ PTH208 Use `pathlib.Path.iterdir()` instead. 8 | 9 | bytes_path = b'.' | +help: Replace with `Path(...).iterdir()` + | +1 | import os +2 + import pathlib +3 | +-------------------------------------------------------------------------------- +7 | string_path = '.' + - os.listdir(string_path) +8 + pathlib.Path(string_path).iterdir() +9 | + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:10:1 | 9 | bytes_path = b'.' 10 | os.listdir(bytes_path) | ^^^^^^^^^^ +help: Replace with `Path(...).iterdir()` + | +1 | import os +2 + import pathlib +3 | +-------------------------------------------------------------------------------- +10 | bytes_path = b'.' + - os.listdir(bytes_path) +11 + pathlib.Path(bytes_path).iterdir() +12 | + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:16:1 | 15 | path_path = Path('.') 16 | os.listdir(path_path) | ^^^^^^^^^^ +help: Replace with `Path(...).iterdir()` + | +15 | path_path = Path('.') + - os.listdir(path_path) +16 + Path(path_path).iterdir() +17 | + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:19:4 | 19 | if os.listdir("dir"): | ^^^^^^^^^^ 20 | ... | +help: Replace with `Path(...).iterdir()` + | +18 | + - if os.listdir("dir"): +19 + if Path("dir").iterdir(): +20 | ... + | +note: This is a display-only fix and is likely to be incorrect -PTH208 Use `pathlib.Path.iterdir()` instead. +PTH208 [*] Use `pathlib.Path.iterdir()` instead. --> PTH208.py:22:14 | 20 | ... @@ -62,3 +123,11 @@ PTH208 Use `pathlib.Path.iterdir()` instead. | ^^^^^^^^^^ 23 | ... | +help: Replace with `Path(...).iterdir()` + | +21 | + - if "file" in os.listdir("dir"): +22 + if "file" in Path("dir").iterdir(): +23 | ... + | +note: This is a display-only fix and is likely to be incorrect diff --git a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs index acd34628878e9..dd8bedd3d4a88 100644 --- a/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs +++ b/crates/ruff_linter/src/rules/flake8_use_pathlib/violations.rs @@ -1,7 +1,7 @@ use ruff_macros::{ViolationMetadata, derive_message_formats}; -use crate::Violation; use crate::codes::Category; +use crate::{FixAvailability, Violation}; /// ## What it does /// Checks for uses of `os.path.join`. @@ -211,8 +211,14 @@ impl Violation for PyPath { pub(crate) struct OsListdir; impl Violation for OsListdir { + const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes; + #[derive_message_formats] fn message(&self) -> String { "Use `pathlib.Path.iterdir()` instead.".to_string() } + + fn fix_title(&self) -> Option { + Some("Replace with `Path(...).iterdir()`".to_string()) + } }