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

Code before:
fn atom_diagnostic(checker: &Checker, target: &Expr) {
    let mut diagnostic = checker.report_diagnostic(
        OSErrorAlias {
            name: UnqualifiedName::from_expr(target).map(|name| name.to_string()),
        },
        target.range(),
    );
    diagnostic.try_set_fix(|| {
        let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
            "OSError",
            target.start(),
            checker.semantic(),
        )?;
        Ok(Fix::safe_edits(
            Edit::range_replacement(binding, target.range()),
            import_edit,
        ))
    });
}

/// Create a [`Diagnostic`] for a tuple of expressions.
fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
    let Some(first_alias) = aliases.first() else {
        return;
    };
    let mut diagnostic =
        checker.report_diagnostic(OSErrorAlias { name: None }, first_alias.range());
    let semantic = checker.semantic();
    if semantic.has_builtin_binding("OSError") {
        // Filter out any `OSErrors` aliases.
        let mut remaining: Vec<Expr> = tuple
            .iter()
            .filter_map(|element| {
                if aliases.contains(&element) {
                    None
                } else {
                    Some(element.clone())
                }
            })
            .collect();

        // If `OSError` itself isn't already in the tuple, add it.
        if tuple
            .iter()
            .all(|elem| !semantic.match_builtin_expr(elem, "OSError"))
        {
            let node = ast::ExprName {
                id: Name::new_static("OSError"),
                ctx: ExprContext::Load,
                range: TextRange::default(),
                node_index: ruff_python_ast::AtomicNodeIndex::NONE,

Review comment (suggestion):
I think this fix is too simple, unfortunately. If you look at the test case below with a diagnostic emitted on line 51 of the input file (`EnvironmentError, IOError, OSError, select.error`), all of these should actually be underlined because they are all `OSError` aliases.

Code after:
/// Create a [`Diagnostic`] for a single target, like an [`Expr::Name`].
fn atom_diagnostic(checker: &Checker, target: &Expr) {
    let mut diagnostic = checker.report_diagnostic(
        OSErrorAlias {
            name: UnqualifiedName::from_expr(target).map(|name| name.to_string()),
        },
        target.range(),
    );
    diagnostic.try_set_fix(|| {
        let (import_edit, binding) = checker.importer().get_or_import_builtin_symbol(
            "OSError",
            target.start(),
            checker.semantic(),
        )?;
        Ok(Fix::safe_edits(
            Edit::range_replacement(binding, target.range()),
            import_edit,
        ))
    });
}

/// Create a [`Diagnostic`] for a tuple of expressions.
fn tuple_diagnostic(checker: &Checker, tuple: &ast::ExprTuple, aliases: &[&Expr]) {
    let Some(first_alias) = aliases.first() else {
        return;
    };
    let diagnostic_range = if is_up024_precise_highlighting_enabled(checker.settings()) {
        first_alias.range()
    } else {
        tuple.range()
    };
    let mut diagnostic = checker.report_diagnostic(OSErrorAlias { name: None }, diagnostic_range);
    if is_up024_precise_highlighting_enabled(checker.settings()) {
        for alias in aliases.iter().skip(1) {
            diagnostic.secondary_annotation("", alias.range());
        }
    }
    let semantic = checker.semantic();
    if semantic.has_builtin_binding("OSError") {
        // Filter out any `OSErrors` aliases.
        let mut remaining: Vec<Expr> = tuple
            .iter()
            .filter_map(|element| {
                if aliases.contains(&element) {
                    None
                } else {
                    Some(element.clone())
                }
            })
            .collect();