@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
13
13
use ruff_diagnostics:: Edit ;
14
14
use ruff_python_ast:: imports:: { AnyImport , Import , ImportFrom } ;
15
15
use ruff_python_codegen:: Stylist ;
16
- use ruff_python_semantic:: SemanticModel ;
16
+ use ruff_python_semantic:: { ImportedName , SemanticModel } ;
17
17
use ruff_python_trivia:: textwrap:: indent;
18
18
use ruff_source_file:: Locator ;
19
19
@@ -132,7 +132,48 @@ impl<'a> Importer<'a> {
132
132
) ?;
133
133
134
134
// Import the `TYPE_CHECKING` symbol from the typing module.
135
- let ( type_checking_edit, type_checking) = self . get_or_import_type_checking ( at, semantic) ?;
135
+ let ( type_checking_edit, type_checking) =
136
+ if let Some ( type_checking) = Self :: find_type_checking ( at, semantic) ? {
137
+ // Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
138
+ // statement that we're modifying, avoid adding a no-op edit. For example, here,
139
+ // the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
140
+ // from the import:
141
+ // ```python
142
+ // from __future__ import annotations
143
+ //
144
+ // from typing import Final, TYPE_CHECKING
145
+ //
146
+ // Const: Final[dict] = {}
147
+ // ```
148
+ let edit = if type_checking. statement ( semantic) == import. statement {
149
+ None
150
+ } else {
151
+ Some ( Edit :: range_replacement (
152
+ self . locator . slice ( type_checking. range ( ) ) . to_string ( ) ,
153
+ type_checking. range ( ) ,
154
+ ) )
155
+ } ;
156
+ ( edit, type_checking. into_name ( ) )
157
+ } else {
158
+ // Special-case: if the `TYPE_CHECKING` symbol would be added to the same import
159
+ // we're modifying, import it as a separate import statement. For example, here,
160
+ // we're concurrently removing `Final` and adding `TYPE_CHECKING`, so it's easier to
161
+ // use a separate import statement:
162
+ // ```python
163
+ // from __future__ import annotations
164
+ //
165
+ // from typing import Final
166
+ //
167
+ // Const: Final[dict] = {}
168
+ // ```
169
+ let ( edit, name) = self . import_symbol (
170
+ & ImportRequest :: import_from ( "typing" , "TYPE_CHECKING" ) ,
171
+ at,
172
+ Some ( import. statement ) ,
173
+ semantic,
174
+ ) ?;
175
+ ( Some ( edit) , name)
176
+ } ;
136
177
137
178
// Add the import to a `TYPE_CHECKING` block.
138
179
let add_import_edit = if let Some ( block) = self . preceding_type_checking_block ( at) {
@@ -157,28 +198,21 @@ impl<'a> Importer<'a> {
157
198
} )
158
199
}
159
200
160
- /// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to
161
- /// make the symbol available in the current scope along with the bound name of the symbol.
162
- fn get_or_import_type_checking (
163
- & self ,
201
+ /// Find a reference to `typing.TYPE_CHECKING`.
202
+ fn find_type_checking (
164
203
at : TextSize ,
165
204
semantic : & SemanticModel ,
166
- ) -> Result < ( Edit , String ) , ResolutionError > {
205
+ ) -> Result < Option < ImportedName > , ResolutionError > {
167
206
for module in semantic. typing_modules ( ) {
168
- if let Some ( ( edit , name ) ) = self . get_symbol (
207
+ if let Some ( imported_name ) = Self :: find_symbol (
169
208
& ImportRequest :: import_from ( module, "TYPE_CHECKING" ) ,
170
209
at,
171
210
semantic,
172
211
) ? {
173
- return Ok ( ( edit , name ) ) ;
212
+ return Ok ( Some ( imported_name ) ) ;
174
213
}
175
214
}
176
-
177
- self . import_symbol (
178
- & ImportRequest :: import_from ( "typing" , "TYPE_CHECKING" ) ,
179
- at,
180
- semantic,
181
- )
215
+ Ok ( None )
182
216
}
183
217
184
218
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
@@ -192,16 +226,15 @@ impl<'a> Importer<'a> {
192
226
semantic : & SemanticModel ,
193
227
) -> Result < ( Edit , String ) , ResolutionError > {
194
228
self . get_symbol ( symbol, at, semantic) ?
195
- . map_or_else ( || self . import_symbol ( symbol, at, semantic) , Ok )
229
+ . map_or_else ( || self . import_symbol ( symbol, at, None , semantic) , Ok )
196
230
}
197
231
198
- /// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
199
- fn get_symbol (
200
- & self ,
232
+ /// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
233
+ fn find_symbol (
201
234
symbol : & ImportRequest ,
202
235
at : TextSize ,
203
236
semantic : & SemanticModel ,
204
- ) -> Result < Option < ( Edit , String ) > , ResolutionError > {
237
+ ) -> Result < Option < ImportedName > , ResolutionError > {
205
238
// If the symbol is already available in the current scope, use it.
206
239
let Some ( imported_name) =
207
240
semantic. resolve_qualified_import_name ( symbol. module , symbol. member )
@@ -226,6 +259,21 @@ impl<'a> Importer<'a> {
226
259
return Err ( ResolutionError :: IncompatibleContext ) ;
227
260
}
228
261
262
+ Ok ( Some ( imported_name) )
263
+ }
264
+
265
+ /// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
266
+ fn get_symbol (
267
+ & self ,
268
+ symbol : & ImportRequest ,
269
+ at : TextSize ,
270
+ semantic : & SemanticModel ,
271
+ ) -> Result < Option < ( Edit , String ) > , ResolutionError > {
272
+ // Find the symbol in the current scope.
273
+ let Some ( imported_name) = Self :: find_symbol ( symbol, at, semantic) ? else {
274
+ return Ok ( None ) ;
275
+ } ;
276
+
229
277
// We also add a no-op edit to force conflicts with any other fixes that might try to
230
278
// remove the import. Consider:
231
279
//
@@ -259,9 +307,13 @@ impl<'a> Importer<'a> {
259
307
& self ,
260
308
symbol : & ImportRequest ,
261
309
at : TextSize ,
310
+ except : Option < & Stmt > ,
262
311
semantic : & SemanticModel ,
263
312
) -> Result < ( Edit , String ) , ResolutionError > {
264
- if let Some ( stmt) = self . find_import_from ( symbol. module , at) {
313
+ if let Some ( stmt) = self
314
+ . find_import_from ( symbol. module , at)
315
+ . filter ( |stmt| except != Some ( stmt) )
316
+ {
265
317
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
266
318
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
267
319
// bound name.
@@ -423,14 +475,18 @@ impl RuntimeImportEdit {
423
475
#[ derive( Debug ) ]
424
476
pub ( crate ) struct TypingImportEdit {
425
477
/// The edit to add the `TYPE_CHECKING` symbol to the module.
426
- type_checking_edit : Edit ,
478
+ type_checking_edit : Option < Edit > ,
427
479
/// The edit to add the import to a `TYPE_CHECKING` block.
428
480
add_import_edit : Edit ,
429
481
}
430
482
431
483
impl TypingImportEdit {
432
- pub ( crate ) fn into_edits ( self ) -> Vec < Edit > {
433
- vec ! [ self . type_checking_edit, self . add_import_edit]
484
+ pub ( crate ) fn into_edits ( self ) -> ( Edit , Option < Edit > ) {
485
+ if let Some ( type_checking_edit) = self . type_checking_edit {
486
+ ( type_checking_edit, Some ( self . add_import_edit ) )
487
+ } else {
488
+ ( self . add_import_edit , None )
489
+ }
434
490
}
435
491
}
436
492
0 commit comments