Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add the ability to jump from into to from definitions #18934

Merged
merged 12 commits into from
Jan 20, 2025
3 changes: 3 additions & 0 deletions crates/hir-expand/src/mod_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
};
Expand Down
4 changes: 4 additions & 0 deletions crates/hir/src/semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,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)
}
Expand Down
74 changes: 74 additions & 0 deletions crates/hir/src/source_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)?)
}
Expand Down
162 changes: 162 additions & 0 deletions crates/ide/src/goto_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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>,
Veykril marked this conversation as resolved.
Show resolved Hide resolved
original_token: &SyntaxToken,
) -> Option<Vec<NavigationTarget>> {
let method_call = ast::MethodCallExpr::cast(original_token.parent()?.parent()?)?;
Copy link
Member

Choose a reason for hiding this comment

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

Fwiw we should also make this work with non method call syntax, but that can wait for a follow up PR

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,
Expand Down Expand Up @@ -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();
}
"#,
);
}
}
4 changes: 4 additions & 0 deletions crates/intern/src/symbol/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ define_symbols! {
const_param_ty,
Context,
Continue,
convert,
copy,
Copy,
core_panic,
Expand Down Expand Up @@ -239,6 +240,8 @@ define_symbols! {
format_unsafe_arg,
format,
freeze,
From,
FromStr,
from_output,
from_residual,
from_usize,
Expand Down Expand Up @@ -457,6 +460,7 @@ define_symbols! {
transmute_trait,
transparent,
Try,
TryFrom,
tuple_trait,
u128,
u16,
Expand Down
33 changes: 31 additions & 2 deletions crates/test-utils/src/minicore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1532,6 +1551,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

Expand Down Expand Up @@ -1791,7 +1819,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
Expand All @@ -1806,6 +1834,7 @@ pub mod prelude {
option::Option::{self, None, Some}, // :option
panic, // :panic
result::Result::{self, Err, Ok}, // :result
str::FromStr, // :str
};
}

Expand Down
Loading