Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions crates/ide-assists/src/handlers/add_explicit_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,18 @@ use crate::{AssistContext, AssistId, Assists};
// }
// ```
pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext<'_, '_>) -> Option<()> {
let syntax_node = ctx.find_node_at_offset::<Either<LetStmt, Param>>()?;
let syntax_node = ctx.find_node_at_offset_with_descend::<Either<LetStmt, Param>>()?;
let (ascribed_ty, expr, pat) = if let Either::Left(let_stmt) = syntax_node {
let cursor_in_range = {
let eq_range = let_stmt.eq_token()?.text_range();
let let_range = ctx.sema.original_range_opt(let_stmt.syntax())?.range;
let first = crate::utils::cover_edit_range(ctx.source_file().syntax(), let_range)
.start()
.clone();
Comment on lines +27 to +30

@Veykril Veykril Jun 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let let_range = ctx.sema.original_range_opt(let_stmt.syntax())?.range;
let first = crate::utils::cover_edit_range(ctx.source_file().syntax(), let_range)
.start()
.clone();
let let_range = ctx.sema.original_range_opt(let_stmt.syntax())?;
if let_range.file_id != ctx.file_id() {
return None;
}
let first = crate::utils::cover_edit_range(ctx.source_file().syntax(), let_range.range)
.start()
.clone();

Or something along those lines (dont recall the apis here), technically upmapping can land us in a different file I think (thanks to include! etc)

View changes since the review

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a very niche case, like let x = include!("expr");

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it hard to think of a case that would cause this problem. If it's just that, I tend to merge it as it is

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand, that shouldn't be a lot of trouble to guard against? We had issues with mixing wrong text ranges and files in the past so I'd prefer if we'd try to avoid this. Especially if proc-macros end up being able to mix spans from differing files in the future as well (I don't think they can now, but they might in the future) at which point this can very much break again

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think the original_range_in in #22551 is expected?

@A4-Tacks A4-Tacks Jun 12, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is difficult to solve because macros can mix non adjacent tokens in the same file

In the future, a more consistent solution should be needed to simply run code-action on tokens that are expanded as is in macros

let eq_range = std::iter::successors(Some(first), |it| it.next_sibling_or_token())
.take_while(|it| let_range.contains_range(it.text_range()))
.find(|it| it.kind() == syntax::T![=])?
.text_range();

ctx.offset() < eq_range.start()
};
if !cursor_in_range {
Expand All @@ -44,7 +52,7 @@ pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext<'_, '_>)
};

let module = ctx.sema.scope(pat.syntax())?.module();
let pat_range = pat.syntax().text_range();
let pat_range = ctx.sema.original_range_opt(pat.syntax())?.range;

// Don't enable the assist if there is a type ascription without any placeholders
if let Some(ty) = &ascribed_ty {
Expand Down Expand Up @@ -78,7 +86,7 @@ pub(crate) fn add_explicit_type(acc: &mut Assists, ctx: &AssistContext<'_, '_>)
pat_range,
|builder| match ascribed_ty {
Some(ascribed_ty) => {
builder.replace(ascribed_ty.syntax().text_range(), inferred_type);
builder.replace(ctx.sema.original_range(ascribed_ty.syntax()).range, inferred_type);
}
None => {
builder.insert(pat_range.end(), format!(": {inferred_type}"));
Expand Down Expand Up @@ -275,6 +283,46 @@ fn f() {
);
}

#[test]
fn add_explicit_type_in_macro() {
check_assist(
add_explicit_type,
r#"
//- proc_macros: identity
#[proc_macros::identity]
fn f() {
let $0x = 3;
}
"#,
r#"
#[proc_macros::identity]
fn f() {
let x: i32 = 3;
}
"#,
);

check_assist(
add_explicit_type,
r#"
macro_rules! identity { ($($t:tt)*) => { $($t)* }; }
fn f() {
identity! {
let $0x = 3;
}
}
"#,
r#"
macro_rules! identity { ($($t:tt)*) => { $($t)* }; }
fn f() {
identity! {
let x: i32 = 3;
}
}
"#,
);
}

#[test]
fn add_explicit_type_not_applicable_fn_param() {
cov_mark::check!(add_explicit_type_not_applicable_in_fn_param);
Expand Down
Loading