Skip to content

Commit 0fe6ecd

Browse files
fix
1 parent fa1ca0b commit 0fe6ecd

5 files changed

Lines changed: 182 additions & 95 deletions

File tree

‎pyrefly/lib/commands/infer.rs‎

Lines changed: 45 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use std::collections::HashSet;
1010
use std::path::Path;
1111

1212
use clap::Parser;
13+
use dupe::Dupe;
1314
use pyrefly_config::args::ConfigOverrideArgs;
1415
use pyrefly_config::base::InferReturnTypes;
1516
use pyrefly_config::finder::ConfigFinder;
@@ -19,9 +20,8 @@ use pyrefly_util::forgetter::Forgetter;
1920
use pyrefly_util::fs_anyhow;
2021
use pyrefly_util::includes::Includes;
2122
use pyrefly_util::thread_pool::ThreadCount;
22-
use ruff_python_ast::Stmt;
23-
use ruff_python_ast::helpers::is_docstring_stmt;
2423
use ruff_text_size::Ranged;
24+
use ruff_text_size::TextRange;
2525
use ruff_text_size::TextSize;
2626

2727
use crate::annotation::format_annotation;
@@ -30,6 +30,8 @@ use crate::commands::config_finder::ConfigConfigurerWrapper;
3030
use crate::commands::files::FilesArgs;
3131
use crate::commands::util::CommandExitStatus;
3232
use crate::lsp::wasm::inlay_hints::ParameterAnnotation;
33+
use crate::state::ide::ImportEdit;
34+
use crate::state::ide::insert_import_edit_with_forced_import_format;
3335
use crate::state::lsp::AnnotationKind;
3436
use crate::state::require::Require;
3537
use crate::state::state::State;
@@ -39,15 +41,6 @@ use crate::types::simplify::unions_with_literals;
3941
use crate::types::stdlib::Stdlib;
4042
use crate::types::types::Type;
4143

42-
/// Check if a statement is a `from __future__ import ...` statement.
43-
/// New imports must be inserted after `__future__` imports to produce valid Python.
44-
fn is_future_import_stmt(stmt: &Stmt) -> bool {
45-
matches!(
46-
stmt,
47-
Stmt::ImportFrom(import_from) if import_from.module.as_ref().is_some_and(|m| m.id == "__future__")
48-
)
49-
}
50-
5144
#[deny(clippy::missing_docs_in_private_items)]
5245
/// Flags for controlling the behavior of the autotype command
5346
#[derive(Debug, Clone, Parser)]
@@ -278,15 +271,6 @@ fn format_hints(
278271
(qualified_hints, needed_imports)
279272
}
280273

281-
// Sort the hints by reverse order so we don't have to recalculate positions
282-
fn sort_inlay_hints(
283-
inlay_hints: Vec<(ruff_text_size::TextSize, String)>,
284-
) -> Vec<(ruff_text_size::TextSize, String)> {
285-
let mut sorted_inlay_hints = inlay_hints;
286-
sorted_inlay_hints.sort_by(|(a, _), (b, _)| b.cmp(a));
287-
sorted_inlay_hints
288-
}
289-
290274
fn hint_to_string(
291275
hint: Type,
292276
stdlib: &Stdlib,
@@ -371,7 +355,7 @@ impl InferArgs {
371355
if let Some(inferred_types) = inferred_types {
372356
parameter_types.extend(inferred_types);
373357
let heap = TypeHeap::new();
374-
let (formatted, needed_imports) = format_hints(
358+
let (annotations, needed_imports) = format_hints(
375359
parameter_types,
376360
&stdlib,
377361
&|cls| {
@@ -389,46 +373,44 @@ impl InferArgs {
389373
&heap,
390374
handle.module(),
391375
);
392-
let sorted = sort_inlay_hints(formatted);
393376
let file_path = handle.path().as_path();
394377
// Build import edits once so dry-run and write share them.
395-
let imports: Vec<(TextSize, String, String)> = if flags.imports()
378+
let imports: Vec<ImportEdit> = if flags.imports()
396379
&& !needed_imports.is_empty()
397380
&& let Some(ast) = transaction.get_ast(&handle)
398381
{
399-
let position = ast
400-
.body
401-
.iter()
402-
.find(|stmt| !is_docstring_stmt(stmt) && !is_future_import_stmt(stmt))
403-
.map_or(ast.range.end(), |stmt| stmt.range().start());
404-
let mut imports: Vec<(TextSize, String, String)> = needed_imports
382+
needed_imports
405383
.into_iter()
406384
.map(|(module_name, name)| {
407-
let import_text =
408-
format!("from {} import {}\n", module_name.as_str(), name);
409-
(position, import_text, module_name.as_str().to_owned())
385+
let handle_to_import_from = transaction
386+
.import_handle(&handle, module_name, None)
387+
.finding()
388+
.expect("infer import source should always resolve");
389+
insert_import_edit_with_forced_import_format(
390+
&ast,
391+
handle.dupe(),
392+
handle_to_import_from.dupe(),
393+
&name,
394+
true,
395+
)
410396
})
411-
.collect();
412-
imports.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
413-
imports
397+
.filter(|edit| !edit.insert_text.is_empty())
398+
.collect()
414399
} else {
415400
Vec::new()
416401
};
417402
if flags.dry_run() {
418-
if !sorted.is_empty() || !imports.is_empty() {
403+
if !annotations.is_empty() || !imports.is_empty() {
419404
any_changes = true;
420405
println!(
421406
"{}: would add {} annotation(s) and {} import(s)",
422407
file_path.display(),
423-
sorted.len(),
408+
annotations.len(),
424409
imports.len()
425410
);
426411
}
427412
} else {
428-
Self::add_annotations_to_file(file_path, sorted)?;
429-
if !imports.is_empty() {
430-
Self::add_imports_to_file(file_path, imports)?;
431-
}
413+
Self::apply_edits_to_file(file_path, annotations, imports)?;
432414
}
433415
}
434416
}
@@ -439,34 +421,30 @@ impl InferArgs {
439421
})
440422
}
441423

442-
fn add_annotations_to_file(
443-
file_path: &Path,
444-
sorted: Vec<(TextSize, String)>,
445-
) -> anyhow::Result<()> {
446-
let file_content = fs_anyhow::read_to_string(file_path)?;
447-
let mut result = file_content;
448-
for inlay_hint in sorted {
449-
let (position, hint) = inlay_hint;
450-
// Convert the TextSize to a byte offset
451-
let offset = (position).into();
452-
if offset <= result.len() {
453-
result.insert_str(offset, &hint);
454-
}
455-
}
456-
fs_anyhow::write(file_path, result)
457-
}
458-
459-
fn add_imports_to_file(
424+
/// Apply all edits from one AST snapshot in descending offset order.
425+
fn apply_edits_to_file(
460426
file_path: &Path,
461-
imports: Vec<(TextSize, String, String)>,
427+
annotations: Vec<(TextSize, String)>,
428+
imports: Vec<ImportEdit>,
462429
) -> anyhow::Result<()> {
463-
let file_content = fs_anyhow::read_to_string(file_path)?;
464-
let mut result = file_content;
465-
for (position, import, _) in imports {
466-
let offset = (position).into();
467-
if !result.contains(&import) {
468-
result.insert_str(offset, &import);
469-
}
430+
let mut result = fs_anyhow::read_to_string(file_path)?;
431+
let mut edits: Vec<(TextRange, String)> = imports
432+
.into_iter()
433+
.map(|edit| (edit.range, edit.insert_text))
434+
.chain(
435+
annotations
436+
.into_iter()
437+
.map(|(position, text)| (TextRange::empty(position), text)),
438+
)
439+
.collect();
440+
edits.sort_by(|(range1, text1), (range2, text2)| {
441+
range2
442+
.start()
443+
.cmp(&range1.start())
444+
.then_with(|| text1.cmp(text2))
445+
});
446+
for (range, edit_text) in edits {
447+
result.replace_range(range.start().to_usize()..range.end().to_usize(), &edit_text);
470448
}
471449
fs_anyhow::write(file_path, result)
472450
}

‎pyrefly/lib/lsp/wasm/inlay_hints.rs‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ use crate::binding::binding::Key;
3434
use crate::binding::binding::KeyClassField;
3535
use crate::binding::binding::UnpackedPosition;
3636
use crate::binding::bindings::Bindings;
37+
use crate::state::ide::fallback_import_insertion_point;
3738
use crate::state::ide::import_regular_import_edit;
3839
use crate::state::ide::insert_import_edit;
3940
use crate::state::import_tracker::ImportTracker;
@@ -140,17 +141,19 @@ impl TypeHintRenderer<'_, '_> {
140141

141142
let mut missing = missing.into_iter().collect::<Vec<_>>();
142143
missing.sort_by(|left, right| left.as_str().cmp(right.as_str()));
143-
let mut import_edit = None;
144+
// Inlay hints apply all imports as one edit, so every import must be a
145+
// complete line at the same legal insertion point rather than a merge suffix.
146+
let import_position = fallback_import_insertion_point(self.ast.as_ref());
147+
let mut import_text = String::new();
144148
for module in missing {
145149
if let Some(handle_to_import) = self
146150
.transaction
147151
.import_handle(self.handle, module, None)
148152
.finding()
149153
{
150-
let (position, insert_text, _) =
154+
let (_, insert_text, _) =
151155
import_regular_import_edit(self.ast.as_ref(), handle_to_import, None);
152-
let (_, combined) = import_edit.get_or_insert((position, String::new()));
153-
combined.push_str(&insert_text);
156+
import_text.push_str(&insert_text);
154157
}
155158
}
156159
for import in direct {
@@ -162,12 +165,13 @@ impl TypeHintRenderer<'_, '_> {
162165
&import.heads.join(", "),
163166
self.import_format,
164167
);
165-
let (_, combined) = import_edit.get_or_insert((edit.range.start(), String::new()));
166-
combined.push_str(&edit.insert_text);
168+
import_text.push_str(&edit.display_text);
169+
import_text.push('\n');
167170
}
168-
let imports = match import_edit {
169-
Some(import_edit) => vec![import_edit],
170-
None => Vec::new(),
171+
let imports = if import_text.is_empty() {
172+
Vec::new()
173+
} else {
174+
vec![(import_position, import_text)]
171175
};
172176
InlayHintEdits {
173177
annotation: format!("{prefix}{text}"),

0 commit comments

Comments
 (0)