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

Code before:
            Strategy::QuoteUsages => format!(
                "Quote references to `{qualified_name}`. Import is in a type-checking block."
            ),
        }
    }

    fn fix_title(&self) -> Option<String> {
        let Self { strategy, .. } = self;
        match strategy {
            Strategy::MoveImport => Some("Move out of type-checking block".to_string()),
            Strategy::QuoteUsages => Some("Quote references".to_string()),
        }
    }
}

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum Action {
    /// The import should be moved out of the type-checking block.
    Move,
    /// All usages of the import should be wrapped in quotes.
    Quote,
    /// The import should be ignored.
    Ignore,
}

struct RuntimeInformation {
    first_reference: ResolvedReferenceId,
    count: usize,
}

impl RuntimeInformation {
    fn annotate(&self, checker: &Checker, diagnostic: &mut crate::checkers::ast::DiagnosticGuard) {
        let reference = checker.semantic().reference(self.first_reference);
        diagnostic.secondary_annotation("Used at runtime here", reference);

        if self.count > 1 {
            diagnostic.info(format_args!(
                "Also used at runtime in {} other locations",
                self.count - 1
            ));
        }
    }
}

type ImportAction<'a> = Vec<(ImportBinding<'a>, Option<RuntimeInformation>)>;

/// TC004
pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &Scope) {
    // Collect all runtime imports by statement.
    let mut actions: FxHashMap<(NodeId, Action), ImportAction> = FxHashMap::default();


Review comment (suggestion):
I know I was the one who said

> Storing this information in ImportBinding doesn't necessarily feel like the best approach

but I think I would still prefer my patch from https://github.com/astral-sh/ruff/issues/16490#issuecomment-3700255982. This type doesn't seem to help all that much and also causes us to allocate several intermediate `Vec`s, at least in the current state of the PR.

Code after:
            Strategy::QuoteUsages => format!(
                "Quote references to `{qualified_name}`. Import is in a type-checking block."
            ),
        }
    }

    fn fix_title(&self) -> Option<String> {
        let Self { strategy, .. } = self;
        match strategy {
            Strategy::MoveImport => Some("Move out of type-checking block".to_string()),
            Strategy::QuoteUsages => Some("Quote references".to_string()),
        }
    }
}

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum Action {
    /// The import should be moved out of the type-checking block.
    Move,
    /// All usages of the import should be wrapped in quotes.
    Quote,
    /// The import should be ignored.
    Ignore,
}

/// TC004
pub(crate) fn runtime_import_in_type_checking_block(checker: &Checker, scope: &Scope) {
    // Collect all runtime imports by statement.
    let mut actions: FxHashMap<(NodeId, Action), Vec<ImportBinding>> = FxHashMap::default();

    // If we have a module-level __getattr__ method we don't necessarily know that the
    // references in __all__ refer to typing-only imports, the __getattr__ might be
    // able to handle that attribute access and return the correct thing at runtime.
    let ignore_dunder_all_references = checker
        .semantic()
        .lookup_symbol_in_scope("__getattr__", ScopeId::global(), false)
        .is_some();

    for binding_id in scope.binding_ids() {
        let binding = checker.semantic().binding(binding_id);

        let Some(import) = binding.as_any_import() else {
            continue;
        };

        let Some(reference_id) = binding.references.first().copied() else {
            continue;
        };

        if binding.context.is_typing()
            && let Some(runtime_reference) = binding.references().find_map(|reference_id| {