diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
new file mode 100644
index 0000000000000..b29425b178e97
--- /dev/null
+++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_1.py
@@ -0,0 +1,7 @@
+"""Add `TYPE_CHECKING` to an existing `typing` import. Another member is moved."""
+
+from __future__ import annotations
+
+from typing import Final
+
+Const: Final[dict] = {}
diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
new file mode 100644
index 0000000000000..24e0534746987
--- /dev/null
+++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_2.py
@@ -0,0 +1,7 @@
+"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
+
+from __future__ import annotations
+
+from typing import Final, TYPE_CHECKING
+
+Const: Final[dict] = {}
diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
new file mode 100644
index 0000000000000..eff6d5efaca25
--- /dev/null
+++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/exempt_type_checking_3.py
@@ -0,0 +1,7 @@
+"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
+
+from __future__ import annotations
+
+from typing import Final, Mapping
+
+Const: Final[dict] = {}
diff --git a/crates/ruff_linter/src/importer/mod.rs b/crates/ruff_linter/src/importer/mod.rs
index cb1c4b1ea6fb9..6dce7a35a107b 100644
--- a/crates/ruff_linter/src/importer/mod.rs
+++ b/crates/ruff_linter/src/importer/mod.rs
@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
use ruff_diagnostics::Edit;
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
use ruff_python_codegen::Stylist;
-use ruff_python_semantic::SemanticModel;
+use ruff_python_semantic::{ImportedName, SemanticModel};
use ruff_python_trivia::textwrap::indent;
use ruff_source_file::Locator;
@@ -132,7 +132,48 @@ impl<'a> Importer<'a> {
)?;
// Import the `TYPE_CHECKING` symbol from the typing module.
- let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?;
+ let (type_checking_edit, type_checking) =
+ if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
+ // Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
+ // statement that we're modifying, avoid adding a no-op edit. For example, here,
+ // the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
+ // from the import:
+ // ```python
+ // from __future__ import annotations
+ //
+ // from typing import Final, TYPE_CHECKING
+ //
+ // Const: Final[dict] = {}
+ // ```
+ let edit = if type_checking.statement(semantic) == import.statement {
+ None
+ } else {
+ Some(Edit::range_replacement(
+ self.locator.slice(type_checking.range()).to_string(),
+ type_checking.range(),
+ ))
+ };
+ (edit, type_checking.into_name())
+ } else {
+ // Special-case: if the `TYPE_CHECKING` symbol would be added to the same import
+ // we're modifying, import it as a separate import statement. For example, here,
+ // we're concurrently removing `Final` and adding `TYPE_CHECKING`, so it's easier to
+ // use a separate import statement:
+ // ```python
+ // from __future__ import annotations
+ //
+ // from typing import Final
+ //
+ // Const: Final[dict] = {}
+ // ```
+ let (edit, name) = self.import_symbol(
+ &ImportRequest::import_from("typing", "TYPE_CHECKING"),
+ at,
+ Some(import.statement),
+ semantic,
+ )?;
+ (Some(edit), name)
+ };
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
@@ -157,28 +198,21 @@ impl<'a> Importer<'a> {
})
}
- /// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to
- /// make the symbol available in the current scope along with the bound name of the symbol.
- fn get_or_import_type_checking(
- &self,
+ /// Find a reference to `typing.TYPE_CHECKING`.
+ fn find_type_checking(
at: TextSize,
semantic: &SemanticModel,
- ) -> Result<(Edit, String), ResolutionError> {
+ ) -> Result