Skip to content
52 changes: 39 additions & 13 deletions crates/ruff_linter/src/checkers/ast/analyze/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1037,27 +1037,14 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
flake8_simplify::rules::zip_dict_keys_and_values(checker, call);
}
if checker.any_rule_enabled(&[
Rule::OsPathAbspath,
Rule::OsChmod,
Rule::OsMkdir,
Rule::OsMakedirs,
Rule::OsRename,
Rule::OsReplace,
Rule::OsRmdir,
Rule::OsRemove,
Rule::OsUnlink,
Rule::OsGetcwd,
Rule::OsPathExists,
Rule::OsPathExpanduser,
Rule::OsPathIsdir,
Rule::OsPathIsfile,
Rule::OsPathIslink,
Rule::OsReadlink,
Rule::OsStat,
Rule::OsPathIsabs,
Rule::OsPathJoin,
Rule::OsPathBasename,
Rule::OsPathDirname,
Rule::OsPathSamefile,
Rule::OsPathSplitext,
Rule::BuiltinOpen,
Expand All @@ -1080,6 +1067,45 @@ pub(crate) fn expression(expr: &Expr, checker: &Checker) {
if checker.is_rule_enabled(Rule::OsPathGetmtime) {
flake8_use_pathlib::rules::os_path_getmtime(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathAbspath) {
flake8_use_pathlib::rules::os_path_abspath(checker, call);
}
if checker.is_rule_enabled(Rule::OsRmdir) {
flake8_use_pathlib::rules::os_rmdir(checker, call);
}
if checker.is_rule_enabled(Rule::OsRemove) {
flake8_use_pathlib::rules::os_remove(checker, call);
}
if checker.is_rule_enabled(Rule::OsUnlink) {
flake8_use_pathlib::rules::os_unlink(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathExists) {
flake8_use_pathlib::rules::os_path_exists(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathExpanduser) {
flake8_use_pathlib::rules::os_path_expanduser(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathBasename) {
flake8_use_pathlib::rules::os_path_basename(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathDirname) {
flake8_use_pathlib::rules::os_path_dirname(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathIsabs) {
flake8_use_pathlib::rules::os_path_isabs(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathIsdir) {
flake8_use_pathlib::rules::os_path_isdir(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathIsfile) {
flake8_use_pathlib::rules::os_path_isfile(checker, call);
}
if checker.is_rule_enabled(Rule::OsPathIslink) {
flake8_use_pathlib::rules::os_path_islink(checker, call);
}
if checker.is_rule_enabled(Rule::OsReadlink) {
flake8_use_pathlib::rules::os_readlink(checker, call);
}
if checker.is_rule_enabled(Rule::PathConstructorCurrentDirectory) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, call);
}
Expand Down
26 changes: 13 additions & 13 deletions crates/ruff_linter/src/codes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -919,27 +919,27 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Tryceratops, "401") => (RuleGroup::Stable, rules::tryceratops::rules::VerboseLogMessage),

// flake8-use-pathlib
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathAbspath),
(Flake8UsePathlib, "100") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathAbspath),
(Flake8UsePathlib, "101") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsChmod),
(Flake8UsePathlib, "102") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMkdir),
(Flake8UsePathlib, "103") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsMakedirs),
(Flake8UsePathlib, "104") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRename),
(Flake8UsePathlib, "105") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReplace),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsUnlink),
(Flake8UsePathlib, "106") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRmdir),
(Flake8UsePathlib, "107") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsRemove),
(Flake8UsePathlib, "108") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsUnlink),
(Flake8UsePathlib, "109") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsGetcwd),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsReadlink),
(Flake8UsePathlib, "110") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExists),
(Flake8UsePathlib, "111") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathExpanduser),
(Flake8UsePathlib, "112") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsdir),
(Flake8UsePathlib, "113") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsfile),
(Flake8UsePathlib, "114") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIslink),
(Flake8UsePathlib, "115") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsReadlink),
(Flake8UsePathlib, "116") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsStat),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathIsabs),
(Flake8UsePathlib, "117") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathIsabs),
(Flake8UsePathlib, "118") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathJoin),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathDirname),
(Flake8UsePathlib, "119") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathBasename),
(Flake8UsePathlib, "120") => (RuleGroup::Stable, rules::flake8_use_pathlib::rules::OsPathDirname),
(Flake8UsePathlib, "121") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSamefile),
(Flake8UsePathlib, "122") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::OsPathSplitext),
(Flake8UsePathlib, "123") => (RuleGroup::Stable, rules::flake8_use_pathlib::violations::BuiltinOpen),
Expand Down
65 changes: 65 additions & 0 deletions crates/ruff_linter/src/preview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,71 @@ pub(crate) const fn is_fix_os_path_getctime_enabled(settings: &LinterSettings) -
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_abspath_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_rmdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_unlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_remove_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_exists_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_expanduser_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isdir_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isfile_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_islink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_isabs_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_readlink_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_basename_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/19213
pub(crate) const fn is_fix_os_path_dirname_enabled(settings: &LinterSettings) -> bool {
settings.preview.is_enabled()
}

// https://github.com/astral-sh/ruff/pull/11436
// https://github.com/astral-sh/ruff/pull/11168
pub(crate) const fn is_dunder_init_fix_unused_import_enabled(settings: &LinterSettings) -> bool {
Expand Down
32 changes: 17 additions & 15 deletions crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use crate::checkers::ast::Checker;
use crate::importer::ImportRequest;
use crate::{Applicability, Edit, Fix, Violation};
use ruff_python_ast::{self as ast};
use ruff_python_ast::{Expr, ExprCall};
use ruff_text_size::Ranged;

pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
pub(crate) fn is_keyword_only_argument_non_default(arguments: &ast::Arguments, name: &str) -> bool {
arguments
.find_keyword(name)
.is_some_and(|keyword| !keyword.value.is_none_literal_expr())
}

pub(crate) fn is_pathlib_path_call(checker: &Checker, expr: &Expr) -> bool {
expr.as_call_expr().is_some_and(|expr_call| {
checker
.semantic()
Expand All @@ -13,27 +20,22 @@ pub(crate) fn is_path_call(checker: &Checker, expr: &Expr) -> bool {
})
}

pub(crate) fn check_os_path_get_calls(
/// We check functions that take only 1 argument, this does not apply to functions
/// with `dir_fd` argument, because `dir_fd` is not supported by pathlib,
/// so check if it's set to non-default values
pub(crate) fn check_os_pathlib_single_arg_calls(
checker: &Checker,
call: &ExprCall,
fn_name: &str,
attr: &str,
fn_argument: &str,
fix_enabled: bool,
violation: impl Violation,
) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", fn_name])
{
return;
}

if call.arguments.len() != 1 {
return;
}

let Some(arg) = call.arguments.find_argument_value("filename", 0) else {
let Some(arg) = call.arguments.find_argument_value(fn_argument, 0) else {
return;
};

Expand All @@ -56,10 +58,10 @@ pub(crate) fn check_os_path_get_calls(
Applicability::Safe
};

let replacement = if is_path_call(checker, arg) {
format!("{arg_code}.stat().{attr}")
let replacement = if is_pathlib_path_call(checker, arg) {
format!("{arg_code}.{attr}")
} else {
format!("{binding}({arg_code}).stat().{attr}")
format!("{binding}({arg_code}).{attr}")
};

Ok(Fix::applicable_edits(
Expand Down
26 changes: 26 additions & 0 deletions crates/ruff_linter/src/rules/flake8_use_pathlib/rules/mod.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,45 @@
pub(crate) use glob_rule::*;
pub(crate) use invalid_pathlib_with_suffix::*;
pub(crate) use os_path_abspath::*;
pub(crate) use os_path_basename::*;
pub(crate) use os_path_dirname::*;
pub(crate) use os_path_exists::*;
pub(crate) use os_path_expanduser::*;
pub(crate) use os_path_getatime::*;
pub(crate) use os_path_getctime::*;
pub(crate) use os_path_getmtime::*;
pub(crate) use os_path_getsize::*;
pub(crate) use os_path_isabs::*;
pub(crate) use os_path_isdir::*;
pub(crate) use os_path_isfile::*;
pub(crate) use os_path_islink::*;
pub(crate) use os_readlink::*;
pub(crate) use os_remove::*;
pub(crate) use os_rmdir::*;
pub(crate) use os_sep_split::*;
pub(crate) use os_unlink::*;
pub(crate) use path_constructor_current_directory::*;
pub(crate) use replaceable_by_pathlib::*;

mod glob_rule;
mod invalid_pathlib_with_suffix;
mod os_path_abspath;
mod os_path_basename;
mod os_path_dirname;
mod os_path_exists;
mod os_path_expanduser;
mod os_path_getatime;
mod os_path_getctime;
mod os_path_getmtime;
mod os_path_getsize;
mod os_path_isabs;
mod os_path_isdir;
mod os_path_isfile;
mod os_path_islink;
mod os_readlink;
mod os_remove;
mod os_rmdir;
mod os_sep_split;
mod os_unlink;
mod path_constructor_current_directory;
mod replaceable_by_pathlib;
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use crate::checkers::ast::Checker;
use crate::preview::is_fix_os_path_abspath_enabled;
use crate::rules::flake8_use_pathlib::helpers::check_os_pathlib_single_arg_calls;
use crate::{FixAvailability, Violation};
use ruff_macros::{ViolationMetadata, derive_message_formats};
use ruff_python_ast::ExprCall;

/// ## What it does
/// Checks for uses of `os.path.abspath`.
///
/// ## Why is this bad?
/// `pathlib` offers a high-level API for path manipulation, as compared to
/// the lower-level API offered by `os.path`. When possible, using `Path` object
/// methods such as `Path.resolve()` can improve readability over the `os.path`
/// module's counterparts (e.g., `os.path.abspath()`).
///
/// ## Examples
/// ```python
/// import os
///
/// file_path = os.path.abspath("../path/to/file")
/// ```
///
/// Use instead:
/// ```python
/// from pathlib import Path
///
/// file_path = Path("../path/to/file").resolve()
/// ```
///
/// ## Known issues
/// While using `pathlib` can improve the readability and type safety of your code,
/// it can be less performant than the lower-level alternatives that work directly with strings,
/// especially on older versions of Python.
///
/// ## 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.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
#[derive(ViolationMetadata)]
pub(crate) struct OsPathAbspath;

impl Violation for OsPathAbspath {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
"`os.path.abspath()` should be replaced by `Path.resolve()`".to_string()
}

fn fix_title(&self) -> Option<String> {
Some("Replace with `Path(...).resolve()`".to_string())
}
}

/// PTH100
pub(crate) fn os_path_abspath(checker: &Checker, call: &ExprCall) {
if checker
.semantic()
.resolve_qualified_name(&call.func)
.is_none_or(|qualified_name| qualified_name.segments() != ["os", "path", "abspath"])
{
return;
}
check_os_pathlib_single_arg_calls(
checker,
call,
"resolve()",
"path",
is_fix_os_path_abspath_enabled(checker.settings()),
OsPathAbspath,
);
}
Loading
Loading