Repository: astral-sh/ruff
File: crates/ruff_linter/src/rules/flake8_use_pathlib/helpers.rs (Rust)

Code before:
            call.start(),
            checker.semantic(),
        )?;

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

        let edit = Edit::range_replacement(replacement, range);

        let fix = match applicability {
            Some(Applicability::Unsafe) => Fix::unsafe_edits(edit, [import_edit]),
            _ => {
                let determined_applicability = if checker.comment_ranges().intersects(range) {
                    Applicability::Unsafe
                } else if applicability.is_none() {
                    // When applicability is None, we need to determine it based on return type changes
                    if is_top_level_expression_call(checker, call) {
                        // Safe when the call is a top-level expression (return value not used)
                        Applicability::Safe
                    } else {
                        // Unsafe because the return type changes (str/bytes -> Path or None -> Path)
                        Applicability::Unsafe
                    }
                } else {
                    // applicability is Some(Applicability::Safe), use it
                    Applicability::Safe
                };
                Fix::applicable_edits(edit, [import_edit], determined_applicability)
            }
        };

        Ok(fix)
    });
}

pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
    match expr {
        Expr::Name(name) => Some(name),
        Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
        _ => None,
    }
}

/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
    if matches!(
        expr,
        Expr::NumberLiteral(ast::ExprNumberLiteral {

Review comment (suggestion):
I don't think we should do this check here. I checked all of the references for `check_os_pathlib_single_arg_calls`, and it's only used by PTH115, of the rules we're trying to fix in this PR.

I think we should just do the `is_top_level_expression_call` check in PTH115 itself.

Code after:
            call.start(),
            checker.semantic(),
        )?;

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

        let edit = Edit::range_replacement(replacement, range);

        let applicability = match applicability {
            Applicability::DisplayOnly => Applicability::DisplayOnly,
            _ if checker.comment_ranges().intersects(range) => Applicability::Unsafe,
            _ => applicability,
        };

        let fix = Fix::applicable_edits(edit, [import_edit], applicability);

        Ok(fix)
    });
}

pub(crate) fn get_name_expr(expr: &Expr) -> Option<&ast::ExprName> {
    match expr {
        Expr::Name(name) => Some(name),
        Expr::Call(ExprCall { func, .. }) => get_name_expr(func),
        _ => None,
    }
}

/// Returns `true` if the given expression looks like a file descriptor, i.e., if it is an integer.
pub(crate) fn is_file_descriptor(expr: &Expr, semantic: &SemanticModel) -> bool {
    if matches!(
        expr,
        Expr::NumberLiteral(ast::ExprNumberLiteral {
            value: ast::Number::Int(_),
            ..
        })
    ) {
        return true;
    }

    let Some(name) = get_name_expr(expr) else {
        return false;
    };

    let Some(binding) = semantic.only_binding(name).map(|id| semantic.binding(id)) else {
        return false;
    };