From 6fdc1a9774446d214fe5d1458948422a11d3380f Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Wed, 20 May 2026 15:55:49 -0700 Subject: [PATCH 1/2] fix: preserve numeric type suffix in inline_const_as_literal When inlining a numeric scalar const, append the const's type as a literal suffix so the resulting literal keeps its type. Without this, inlining `const N: i32 = 24;` followed by `N.wrapping_add(7i32)` would produce `24.wrapping_add(7i32)`, which fails with E0689 because `24` is left as an ambiguous `{integer}`. The suffix is only attached when the const's outer type is one of the built-in integer or float types and the rendered value ends in a digit, which skips special floats like `inf` and `NaN`. This change was prepared with the assistance of an LLM tool. Closes rust-lang/rust-analyzer#22051 --- .../src/handlers/inline_const_as_literal.rs | 124 +++++++++++++++--- 1 file changed, 108 insertions(+), 16 deletions(-) diff --git a/crates/ide-assists/src/handlers/inline_const_as_literal.rs b/crates/ide-assists/src/handlers/inline_const_as_literal.rs index 46cedea96781..43724cc451f0 100644 --- a/crates/ide-assists/src/handlers/inline_const_as_literal.rs +++ b/crates/ide-assists/src/handlers/inline_const_as_literal.rs @@ -42,11 +42,27 @@ pub(crate) fn inline_const_as_literal( // FIXME: Add support to handle type aliases for builtin scalar types. validate_type_recursively(ctx, Some(&konst_ty), false, fuel)?; - let value = konst + let mut value = konst .eval(ctx.sema.db) .ok()? .render(ctx.sema.db, konst.krate(ctx.sema.db).to_display_target(ctx.sema.db)); + // Preserve the numeric type by appending a suffix when the const has a known + // scalar numeric type. Without this, inlining `const N: i32 = 24;` followed by + // `N.wrapping_add(7i32)` would yield `24.wrapping_add(7i32)`, which fails to + // type-check (E0689) because `24` is left as an ambiguous `{integer}`. + // + // Only attach the suffix when the rendered value ends in a digit. Special float + // values such as `inf` or `NaN` are skipped, since `inff32` is not a valid + // literal; these cases were already producing non-literal output before this + // change. + if let Some(suffix) = numeric_type_suffix(&konst_ty) + && value.as_bytes().last().is_some_and(u8::is_ascii_digit) + && !value.ends_with(suffix) + { + value.push_str(suffix); + } + let id = AssistId::refactor_inline("inline_const_as_literal"); let label = "Inline const as literal".to_owned(); @@ -59,6 +75,36 @@ pub(crate) fn inline_const_as_literal( None } +/// Returns the literal-suffix string (e.g. `"i32"`, `"u64"`, `"f32"`) for a scalar +/// numeric type, otherwise `None`. +fn numeric_type_suffix(ty: &hir::Type<'_>) -> Option<&'static str> { + let builtin = ty.as_builtin()?; + if !(builtin.is_int() || builtin.is_uint() || builtin.is_float()) { + return None; + } + // The builtin type's name matches its literal suffix for every scalar numeric + // type (`i32`, `u64`, `f32`, ...), so look the suffix up by name. + Some(match builtin.name().as_str() { + "i8" => "i8", + "i16" => "i16", + "i32" => "i32", + "i64" => "i64", + "i128" => "i128", + "isize" => "isize", + "u8" => "u8", + "u16" => "u16", + "u32" => "u32", + "u64" => "u64", + "u128" => "u128", + "usize" => "usize", + "f16" => "f16", + "f32" => "f32", + "f64" => "f64", + "f128" => "f128", + _ => return None, + }) +} + fn validate_type_recursively( ctx: &AssistContext<'_, '_>, ty_hir: Option<&hir::Type<'_>>, @@ -126,6 +172,13 @@ mod tests { ("char", "'c'", CHAR), ]; + /// Renders the literal that the assist is expected to inline. For scalar numeric + /// types we now annotate the literal with its type suffix to preserve type info + /// across the inlining (see `numeric_type_suffix`). + fn expected_inlined(ty: &str, val: &str, kind: u8) -> String { + if kind == NUMBER { format!("{val}{ty}") } else { val.to_string() } + } + // -----------Not supported----------- #[test] fn inline_const_as_literal_const_fn_call_slice() { @@ -231,7 +284,8 @@ mod tests { #[test] fn inline_const_as_literal_const_expr() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -243,7 +297,7 @@ mod tests { &format!( r#" const ABC: {ty} = {val}; - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -252,7 +306,8 @@ mod tests { #[test] fn inline_const_as_literal_const_block_expr() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -264,7 +319,7 @@ mod tests { &format!( r#" const ABC: {ty} = {{ {val} }}; - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -273,7 +328,8 @@ mod tests { #[test] fn inline_const_as_literal_const_block_eval_expr() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -285,7 +341,7 @@ mod tests { &format!( r#" const ABC: {ty} = {{ true; {val} }}; - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -294,7 +350,8 @@ mod tests { #[test] fn inline_const_as_literal_const_block_eval_block_expr() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -306,7 +363,7 @@ mod tests { &format!( r#" const ABC: {ty} = {{ true; {{ {val} }} }}; - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -315,7 +372,8 @@ mod tests { #[test] fn inline_const_as_literal_const_fn_call_block_nested_builtin() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -329,7 +387,7 @@ mod tests { r#" const fn abc() -> {ty} {{ {{ {{ {{ {val} }} }} }} }} const ABC: {ty} = abc(); - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -361,7 +419,8 @@ mod tests { #[test] fn inline_const_as_literal_const_fn_call_builtin() { - TEST_PAIRS.iter().for_each(|(ty, val, _)| { + TEST_PAIRS.iter().for_each(|(ty, val, kind)| { + let out = expected_inlined(ty, val, *kind); check_assist( inline_const_as_literal, &format!( @@ -375,7 +434,7 @@ mod tests { r#" const fn abc() -> {ty} {{ {val} }} const ABC: {ty} = abc(); - fn a() {{ {val} }} + fn a() {{ {out} }} "# ), ); @@ -392,7 +451,7 @@ mod tests { "#, r#" const ABC: i32 = 1 + 2 + 3; - fn a() { 6 } + fn a() { 6i32 } "#, ); } @@ -406,7 +465,7 @@ mod tests { "#, r#" const ABC: i32 = { 1 + 2 + 3 }; - fn a() { 6 } + fn a() { 6i32 } "#, ); } @@ -421,7 +480,7 @@ mod tests { "#, r#" const ABC: i32 = { (1 + 2 + 3) }; - fn a() { 6 } + fn a() { 6i32 } "#, ); } @@ -710,4 +769,37 @@ mod tests { "#, ); } + + // Regression test for #22051: the inlined numeric literal must carry the + // const's type suffix, otherwise method calls on the receiver fail to + // type-check (E0689) because the literal is left as `{integer}`. + #[test] + fn inline_const_as_literal_preserves_int_suffix_on_method_receiver() { + check_assist( + inline_const_as_literal, + r#" + const BASE: i32 = 24; + fn main() { let _ = BAS$0E.wrapping_add(7i32); } + "#, + r#" + const BASE: i32 = 24; + fn main() { let _ = 24i32.wrapping_add(7i32); } + "#, + ); + } + + #[test] + fn inline_const_as_literal_preserves_float_suffix() { + check_assist( + inline_const_as_literal, + r#" + const BASE: f32 = 1.5; + fn main() { let _ = BAS$0E; } + "#, + r#" + const BASE: f32 = 1.5; + fn main() { let _ = 1.5f32; } + "#, + ); + } } From 23a659def01e725fd19a10b673c65c106b34fd69 Mon Sep 17 00:00:00 2001 From: LeSingh1 Date: Wed, 20 May 2026 16:27:14 -0700 Subject: [PATCH 2/2] fix: use to_owned instead of to_string on &str in test helper --- crates/ide-assists/src/handlers/inline_const_as_literal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ide-assists/src/handlers/inline_const_as_literal.rs b/crates/ide-assists/src/handlers/inline_const_as_literal.rs index 43724cc451f0..6fa4e1fcf4c0 100644 --- a/crates/ide-assists/src/handlers/inline_const_as_literal.rs +++ b/crates/ide-assists/src/handlers/inline_const_as_literal.rs @@ -176,7 +176,7 @@ mod tests { /// types we now annotate the literal with its type suffix to preserve type info /// across the inlining (see `numeric_type_suffix`). fn expected_inlined(ty: &str, val: &str, kind: u8) -> String { - if kind == NUMBER { format!("{val}{ty}") } else { val.to_string() } + if kind == NUMBER { format!("{val}{ty}") } else { val.to_owned() } } // -----------Not supported-----------