Unnamed repository; edit this file 'description' to name the repository.
Merge pull request #18934 from 1hakusai1/goto_definition_from_into
feat: Add the ability to jump from `into` to `from` definitions
Lukas Wirth 2025-01-20
parent 8a5aa80 · parent 913ec54 · commit b2f822b
-rw-r--r--crates/hir-expand/src/mod_path.rs3
-rw-r--r--crates/hir/src/semantics.rs4
-rw-r--r--crates/hir/src/source_analyzer.rs74
-rw-r--r--crates/ide/src/goto_definition.rs162
-rw-r--r--crates/intern/src/symbol/symbols.rs4
-rw-r--r--crates/test-utils/src/minicore.rs33
6 files changed, 278 insertions, 2 deletions
diff --git a/crates/hir-expand/src/mod_path.rs b/crates/hir-expand/src/mod_path.rs
index 4f2f9ec40d..89eae862bd 100644
--- a/crates/hir-expand/src/mod_path.rs
+++ b/crates/hir-expand/src/mod_path.rs
@@ -398,6 +398,9 @@ macro_rules! __known_path {
(core::fmt::Debug) => {};
(std::fmt::format) => {};
(core::ops::Try) => {};
+ (core::convert::From) => {};
+ (core::convert::TryFrom) => {};
+ (core::str::FromStr) => {};
($path:path) => {
compile_error!("Please register your known path in the path module")
};
diff --git a/crates/hir/src/semantics.rs b/crates/hir/src/semantics.rs
index 92f6c4b5ab..d89d5e8fdc 100644
--- a/crates/hir/src/semantics.rs
+++ b/crates/hir/src/semantics.rs
@@ -1439,6 +1439,10 @@ impl<'db> SemanticsImpl<'db> {
self.analyze(call.syntax())?.resolve_method_call_fallback(self.db, call)
}
+ pub fn resolve_known_blanket_dual_impls(&self, call: &ast::MethodCallExpr) -> Option<Function> {
+ self.analyze(call.syntax())?.resolve_known_blanket_dual_impls(self.db, call)
+ }
+
fn resolve_range_pat(&self, range_pat: &ast::RangePat) -> Option<StructId> {
self.analyze(range_pat.syntax())?.resolve_range_pat(self.db, range_pat)
}
diff --git a/crates/hir/src/source_analyzer.rs b/crates/hir/src/source_analyzer.rs
index b699ccde41..6b78d7a363 100644
--- a/crates/hir/src/source_analyzer.rs
+++ b/crates/hir/src/source_analyzer.rs
@@ -322,6 +322,68 @@ impl SourceAnalyzer {
}
}
+ // If the method is into(), try_into(), parse(), resolve it to from, try_from, from_str.
+ pub(crate) fn resolve_known_blanket_dual_impls(
+ &self,
+ db: &dyn HirDatabase,
+ call: &ast::MethodCallExpr,
+ ) -> Option<Function> {
+ // e.g. if the method call is let b = a.into(),
+ // - receiver_type is A (type of a)
+ // - return_type is B (type of b)
+ // We will find the definition of B::from(a: A).
+ let callable = self.resolve_method_call_as_callable(db, call)?;
+ let (_, receiver_type) = callable.receiver_param(db)?;
+ let return_type = callable.return_type();
+ let (search_method, substs) = match call.name_ref()?.text().as_str() {
+ "into" => {
+ let trait_ =
+ self.resolver.resolve_known_trait(db.upcast(), &path![core::convert::From])?;
+ (
+ self.trait_fn(db, trait_, "from")?,
+ hir_ty::TyBuilder::subst_for_def(db, trait_, None)
+ .push(return_type.ty)
+ .push(receiver_type.ty)
+ .build(),
+ )
+ }
+ "try_into" => {
+ let trait_ = self
+ .resolver
+ .resolve_known_trait(db.upcast(), &path![core::convert::TryFrom])?;
+ (
+ self.trait_fn(db, trait_, "try_from")?,
+ hir_ty::TyBuilder::subst_for_def(db, trait_, None)
+ // If the method is try_into() or parse(), return_type is Result<T, Error>.
+ // Get T from type arguments of Result<T, Error>.
+ .push(return_type.type_arguments().next()?.ty)
+ .push(receiver_type.ty)
+ .build(),
+ )
+ }
+ "parse" => {
+ let trait_ =
+ self.resolver.resolve_known_trait(db.upcast(), &path![core::str::FromStr])?;
+ (
+ self.trait_fn(db, trait_, "from_str")?,
+ hir_ty::TyBuilder::subst_for_def(db, trait_, None)
+ .push(return_type.type_arguments().next()?.ty)
+ .build(),
+ )
+ }
+ _ => return None,
+ };
+
+ let found_method = self.resolve_impl_method_or_trait_def(db, search_method, substs);
+ // If found_method == search_method, the method in trait itself is resolved.
+ // It means the blanket dual impl is not found.
+ if found_method == search_method {
+ None
+ } else {
+ Some(found_method.into())
+ }
+ }
+
pub(crate) fn resolve_expr_as_callable(
&self,
db: &dyn HirDatabase,
@@ -1247,6 +1309,18 @@ impl SourceAnalyzer {
Some((trait_id, fn_id))
}
+ fn trait_fn(
+ &self,
+ db: &dyn HirDatabase,
+ trait_id: TraitId,
+ method_name: &str,
+ ) -> Option<FunctionId> {
+ db.trait_data(trait_id).items.iter().find_map(|(item_name, item)| match item {
+ AssocItemId::FunctionId(t) if item_name.as_str() == method_name => Some(*t),
+ _ => None,
+ })
+ }
+
fn ty_of_expr(&self, db: &dyn HirDatabase, expr: &ast::Expr) -> Option<&Ty> {
self.infer.as_ref()?.type_of_expr_or_pat(self.expr_id(db, expr)?)
}
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index 6c739de82d..f804cc3677 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -81,6 +81,10 @@ pub(crate) fn goto_definition(
return Some(RangeInfo::new(original_token.text_range(), navs));
}
+ if let Some(navs) = find_definition_for_known_blanket_dual_impls(sema, &original_token) {
+ return Some(RangeInfo::new(original_token.text_range(), navs));
+ }
+
let navs = sema
.descend_into_macros_no_opaque(original_token.clone())
.into_iter()
@@ -125,6 +129,18 @@ pub(crate) fn goto_definition(
Some(RangeInfo::new(original_token.text_range(), navs))
}
+// If the token is into(), try_into(), parse(), search the definition of From, TryFrom, FromStr.
+fn find_definition_for_known_blanket_dual_impls(
+ sema: &Semantics<'_, RootDatabase>,
+ original_token: &SyntaxToken,
+) -> Option<Vec<NavigationTarget>> {
+ let method_call = ast::MethodCallExpr::cast(original_token.parent()?.parent()?)?;
+ let target_method = sema.resolve_known_blanket_dual_impls(&method_call)?;
+
+ let def = Definition::from(target_method);
+ Some(def_to_nav(sema.db, def))
+}
+
fn try_lookup_include_path(
sema: &Semantics<'_, RootDatabase>,
token: ast::String,
@@ -3022,4 +3038,150 @@ fn foo() {
"#,
);
}
+ #[test]
+ fn into_call_to_from_definition() {
+ check(
+ r#"
+//- minicore: from
+struct A;
+
+struct B;
+
+impl From<A> for B {
+ fn from(value: A) -> Self {
+ //^^^^
+ B
+ }
+}
+
+fn f() {
+ let a = A;
+ let b: B = a.into$0();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn into_call_to_from_definition_with_trait_bounds() {
+ check(
+ r#"
+//- minicore: from, iterator
+struct A;
+
+impl<T> From<T> for A
+where
+ T: IntoIterator<Item = i64>,
+{
+ fn from(value: T) -> Self {
+ //^^^^
+ A
+ }
+}
+
+fn f() {
+ let a: A = [1, 2, 3].into$0();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn goto_into_definition_if_exists() {
+ check(
+ r#"
+//- minicore: from
+struct A;
+
+struct B;
+
+impl Into<B> for A {
+ fn into(self) -> B {
+ //^^^^
+ B
+ }
+}
+
+fn f() {
+ let a = A;
+ let b: B = a.into$0();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn try_into_call_to_try_from_definition() {
+ check(
+ r#"
+//- minicore: from
+struct A;
+
+struct B;
+
+impl TryFrom<A> for B {
+ type Error = String;
+
+ fn try_from(value: A) -> Result<Self, Self::Error> {
+ //^^^^^^^^
+ Ok(B)
+ }
+}
+
+fn f() {
+ let a = A;
+ let b: Result<B, _> = a.try_into$0();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn goto_try_into_definition_if_exists() {
+ check(
+ r#"
+//- minicore: from
+struct A;
+
+struct B;
+
+impl TryInto<B> for A {
+ type Error = String;
+
+ fn try_into(self) -> Result<B, Self::Error> {
+ //^^^^^^^^
+ Ok(B)
+ }
+}
+
+fn f() {
+ let a = A;
+ let b: Result<B, _> = a.try_into$0();
+}
+ "#,
+ );
+ }
+
+ #[test]
+ fn parse_call_to_from_str_definition() {
+ check(
+ r#"
+//- minicore: from, str
+struct A;
+
+impl FromStr for A {
+ type Error = String;
+
+ fn from_str(value: &str) -> Result<Self, Self::Error> {
+ //^^^^^^^^
+ Ok(A)
+ }
+}
+
+fn f() {
+ let a: Result<A, _> = "aaaaaa".parse$0();
+}
+ "#,
+ );
+ }
}
diff --git a/crates/intern/src/symbol/symbols.rs b/crates/intern/src/symbol/symbols.rs
index 131b21a46b..b3b46421b5 100644
--- a/crates/intern/src/symbol/symbols.rs
+++ b/crates/intern/src/symbol/symbols.rs
@@ -174,6 +174,7 @@ define_symbols! {
const_param_ty,
Context,
Continue,
+ convert,
copy,
Copy,
core_panic,
@@ -239,6 +240,8 @@ define_symbols! {
format_unsafe_arg,
format,
freeze,
+ From,
+ FromStr,
from_output,
from_residual,
from_usize,
@@ -457,6 +460,7 @@ define_symbols! {
transmute_trait,
transparent,
Try,
+ TryFrom,
tuple_trait,
u128,
u16,
diff --git a/crates/test-utils/src/minicore.rs b/crates/test-utils/src/minicore.rs
index 4462c2ce1e..fd06736a25 100644
--- a/crates/test-utils/src/minicore.rs
+++ b/crates/test-utils/src/minicore.rs
@@ -32,7 +32,7 @@
//! error: fmt
//! fmt: option, result, transmute, coerce_unsized, copy, clone, derive
//! fn: tuple
-//! from: sized
+//! from: sized, result
//! future: pin
//! coroutine: pin
//! dispatch_from_dyn: unsize, pin
@@ -332,6 +332,25 @@ pub mod convert {
t
}
}
+
+ pub trait TryFrom<T>: Sized {
+ type Error;
+ fn try_from(value: T) -> Result<Self, Self::Error>;
+ }
+ pub trait TryInto<T>: Sized {
+ type Error;
+ fn try_into(self) -> Result<T, Self::Error>;
+ }
+
+ impl<T, U> TryInto<U> for T
+ where
+ U: TryFrom<T>,
+ {
+ type Error = U::Error;
+ fn try_into(self) -> Result<U, U::Error> {
+ U::try_from(self)
+ }
+ }
// endregion:from
// region:as_ref
@@ -1555,6 +1574,15 @@ pub mod str {
pub const unsafe fn from_utf8_unchecked(v: &[u8]) -> &str {
""
}
+ pub trait FromStr: Sized {
+ type Err;
+ fn from_str(s: &str) -> Result<Self, Self::Err>;
+ }
+ impl str {
+ pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
+ FromStr::from_str(self)
+ }
+ }
}
// endregion:str
@@ -1814,7 +1842,7 @@ pub mod prelude {
cmp::{Eq, PartialEq}, // :eq
cmp::{Ord, PartialOrd}, // :ord
convert::AsRef, // :as_ref
- convert::{From, Into}, // :from
+ convert::{From, Into, TryFrom, TryInto}, // :from
default::Default, // :default
iter::{IntoIterator, Iterator}, // :iterator
macros::builtin::{derive, derive_const}, // :derive
@@ -1829,6 +1857,7 @@ pub mod prelude {
option::Option::{self, None, Some}, // :option
panic, // :panic
result::Result::{self, Err, Ok}, // :result
+ str::FromStr, // :str
};
}