From 0c433c23b155734aa190157c8899b5d854a75c5a Mon Sep 17 00:00:00 2001 From: Ryo Yoshida Date: Mon, 7 Aug 2023 00:59:35 +0900 Subject: [PATCH] Support doc links that resolve to fields --- crates/hir-def/src/resolver.rs | 38 +------ crates/hir/src/attrs.rs | 179 ++++++++++++++++++++++++------ crates/hir/src/lib.rs | 2 +- crates/ide-db/src/defs.rs | 12 +- crates/ide/src/doc_links/tests.rs | 56 ++++++++++ 5 files changed, 219 insertions(+), 68 deletions(-) diff --git a/crates/hir-def/src/resolver.rs b/crates/hir-def/src/resolver.rs index b112c1070d4..d04d2fa0e97 100644 --- a/crates/hir-def/src/resolver.rs +++ b/crates/hir-def/src/resolver.rs @@ -21,11 +21,11 @@ use crate::{ path::{ModPath, Path, PathKind}, per_ns::PerNs, visibility::{RawVisibility, Visibility}, - AdtId, AssocItemId, ConstId, ConstParamId, CrateRootModuleId, DefWithBodyId, EnumId, - EnumVariantId, ExternBlockId, ExternCrateId, FunctionId, GenericDefId, GenericParamId, - HasModule, ImplId, ItemContainerId, LifetimeParamId, LocalModuleId, Lookup, Macro2Id, MacroId, - MacroRulesId, ModuleDefId, ModuleId, ProcMacroId, StaticId, StructId, TraitAliasId, TraitId, - TypeAliasId, TypeOrConstParamId, TypeOwnerId, TypeParamId, UseId, VariantId, + AdtId, ConstId, ConstParamId, CrateRootModuleId, DefWithBodyId, EnumId, EnumVariantId, + ExternBlockId, ExternCrateId, FunctionId, GenericDefId, GenericParamId, HasModule, ImplId, + ItemContainerId, LifetimeParamId, LocalModuleId, Lookup, Macro2Id, MacroId, MacroRulesId, + ModuleDefId, ModuleId, ProcMacroId, StaticId, StructId, TraitAliasId, TraitId, TypeAliasId, + TypeOrConstParamId, TypeOwnerId, TypeParamId, UseId, VariantId, }; #[derive(Debug, Clone)] @@ -148,34 +148,6 @@ impl Resolver { self.resolve_module_path(db, path, BuiltinShadowMode::Module) } - // FIXME: This shouldn't exist - pub fn resolve_module_path_in_trait_assoc_items( - &self, - db: &dyn DefDatabase, - path: &ModPath, - ) -> Option { - let (item_map, module) = self.item_scope(); - let (module_res, idx) = - item_map.resolve_path(db, module, path, BuiltinShadowMode::Module, None); - match module_res.take_types()? { - ModuleDefId::TraitId(it) => { - let idx = idx?; - let unresolved = &path.segments()[idx..]; - let assoc = match unresolved { - [it] => it, - _ => return None, - }; - let &(_, assoc) = db.trait_data(it).items.iter().find(|(n, _)| n == assoc)?; - Some(match assoc { - AssocItemId::FunctionId(it) => PerNs::values(it.into(), Visibility::Public), - AssocItemId::ConstId(it) => PerNs::values(it.into(), Visibility::Public), - AssocItemId::TypeAliasId(it) => PerNs::types(it.into(), Visibility::Public), - }) - } - _ => None, - } - } - pub fn resolve_path_in_type_ns( &self, db: &dyn DefDatabase, diff --git a/crates/hir/src/attrs.rs b/crates/hir/src/attrs.rs index 0f2fb2c8118..cd0410168c2 100644 --- a/crates/hir/src/attrs.rs +++ b/crates/hir/src/attrs.rs @@ -3,18 +3,18 @@ use hir_def::{ attr::{AttrsWithOwner, Documentation}, item_scope::ItemInNs, - path::ModPath, - resolver::HasResolver, - AttrDefId, GenericParamId, ModuleDefId, + path::{ModPath, Path}, + resolver::{HasResolver, Resolver, TypeNs}, + AssocItemId, AttrDefId, GenericParamId, ModuleDefId, }; -use hir_expand::hygiene::Hygiene; +use hir_expand::{hygiene::Hygiene, name::Name}; use hir_ty::db::HirDatabase; use syntax::{ast, AstNode}; use crate::{ - Adt, AssocItem, Const, ConstParam, Enum, ExternCrateDecl, Field, Function, GenericParam, Impl, - LifetimeParam, Macro, Module, ModuleDef, Static, Struct, Trait, TraitAlias, TypeAlias, - TypeParam, Union, Variant, + Adt, AsAssocItem, AssocItem, BuiltinType, Const, ConstParam, Enum, ExternCrateDecl, Field, + Function, GenericParam, Impl, LifetimeParam, Macro, Module, ModuleDef, Static, Struct, Trait, + TraitAlias, TypeAlias, TypeParam, Union, Variant, VariantDef, }; pub trait HasAttrs { @@ -25,7 +25,7 @@ pub trait HasAttrs { db: &dyn HirDatabase, link: &str, ns: Option, - ) -> Option; + ) -> Option; } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] @@ -35,6 +35,13 @@ pub enum Namespace { Macros, } +/// Subset of `ide_db::Definition` that doc links can resolve to. +pub enum DocLinkDef { + ModuleDef(ModuleDef), + Field(Field), + SelfType(Trait), +} + macro_rules! impl_has_attrs { ($(($def:ident, $def_id:ident),)*) => {$( impl HasAttrs for $def { @@ -46,9 +53,14 @@ macro_rules! impl_has_attrs { let def = AttrDefId::$def_id(self.into()); db.attrs(def).docs() } - fn resolve_doc_path(self, db: &dyn HirDatabase, link: &str, ns: Option) -> Option { + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option + ) -> Option { let def = AttrDefId::$def_id(self.into()); - resolve_doc_path(db, def, link, ns).map(ModuleDef::from) + resolve_doc_path(db, def, link, ns) } } )*}; @@ -79,7 +91,12 @@ macro_rules! impl_has_attrs_enum { fn docs(self, db: &dyn HirDatabase) -> Option { $enum::$variant(self).docs(db) } - fn resolve_doc_path(self, db: &dyn HirDatabase, link: &str, ns: Option) -> Option { + fn resolve_doc_path( + self, + db: &dyn HirDatabase, + link: &str, + ns: Option + ) -> Option { $enum::$variant(self).resolve_doc_path(db, link, ns) } } @@ -111,7 +128,7 @@ impl HasAttrs for AssocItem { db: &dyn HirDatabase, link: &str, ns: Option, - ) -> Option { + ) -> Option { match self { AssocItem::Function(it) => it.resolve_doc_path(db, link, ns), AssocItem::Const(it) => it.resolve_doc_path(db, link, ns), @@ -147,9 +164,9 @@ impl HasAttrs for ExternCrateDecl { db: &dyn HirDatabase, link: &str, ns: Option, - ) -> Option { + ) -> Option { let def = AttrDefId::ExternCrateId(self.into()); - resolve_doc_path(db, def, link, ns).map(ModuleDef::from) + resolve_doc_path(db, def, link, ns) } } @@ -159,7 +176,7 @@ fn resolve_doc_path( def: AttrDefId, link: &str, ns: Option, -) -> Option { +) -> Option { let resolver = match def { AttrDefId::ModuleId(it) => it.resolver(db.upcast()), AttrDefId::FieldId(it) => it.parent.resolver(db.upcast()), @@ -184,8 +201,107 @@ fn resolve_doc_path( .resolver(db.upcast()), }; - let modpath = { - // FIXME: this is not how we should get a mod path here + let mut modpath = modpath_from_str(db, link)?; + + let resolved = resolver.resolve_module_path_in_items(db.upcast(), &modpath); + if resolved.is_none() { + let last_name = modpath.pop_segment()?; + resolve_assoc_or_field(db, resolver, modpath, last_name, ns) + } else { + let def = match ns { + Some(Namespace::Types) => resolved.take_types(), + Some(Namespace::Values) => resolved.take_values(), + Some(Namespace::Macros) => resolved.take_macros().map(ModuleDefId::MacroId), + None => resolved.iter_items().next().map(|it| match it { + ItemInNs::Types(it) => it, + ItemInNs::Values(it) => it, + ItemInNs::Macros(it) => ModuleDefId::MacroId(it), + }), + }; + Some(DocLinkDef::ModuleDef(def?.into())) + } +} + +fn resolve_assoc_or_field( + db: &dyn HirDatabase, + resolver: Resolver, + path: ModPath, + name: Name, + ns: Option, +) -> Option { + let path = Path::from_known_path_with_no_generic(path); + // FIXME: This does not handle `Self` on trait definitions, which we should resolve to the + // trait itself. + let base_def = resolver.resolve_path_in_type_ns_fully(db.upcast(), &path)?; + + let ty = match base_def { + TypeNs::SelfType(id) => Impl::from(id).self_ty(db), + TypeNs::GenericParam(_) => { + // Even if this generic parameter has some trait bounds, rustdoc doesn't + // resolve `name` to trait items. + return None; + } + TypeNs::AdtId(id) | TypeNs::AdtSelfType(id) => Adt::from(id).ty(db), + TypeNs::EnumVariantId(id) => { + // Enum variants don't have path candidates. + let variant = Variant::from(id); + return resolve_field(db, variant.into(), name, ns); + } + TypeNs::TypeAliasId(id) => { + let alias = TypeAlias::from(id); + if alias.as_assoc_item(db).is_some() { + // We don't normalize associated type aliases, so we have nothing to + // resolve `name` to. + return None; + } + alias.ty(db) + } + TypeNs::BuiltinType(id) => BuiltinType::from(id).ty(db), + TypeNs::TraitId(id) => { + // Doc paths in this context may only resolve to an item of this trait + // (i.e. no items of its supertraits), so we need to handle them here + // independently of others. + return db.trait_data(id).items.iter().find(|it| it.0 == name).map(|(_, assoc_id)| { + let def = match *assoc_id { + AssocItemId::FunctionId(it) => ModuleDef::Function(it.into()), + AssocItemId::ConstId(it) => ModuleDef::Const(it.into()), + AssocItemId::TypeAliasId(it) => ModuleDef::TypeAlias(it.into()), + }; + DocLinkDef::ModuleDef(def) + }); + } + TypeNs::TraitAliasId(_) => { + // XXX: Do these get resolved? + return None; + } + }; + + // FIXME: Resolve associated items here, e.g. `Option::map`. Note that associated items take + // precedence over fields. + + let variant_def = match ty.as_adt()? { + Adt::Struct(it) => it.into(), + Adt::Union(it) => it.into(), + Adt::Enum(_) => return None, + }; + resolve_field(db, variant_def, name, ns) +} + +fn resolve_field( + db: &dyn HirDatabase, + def: VariantDef, + name: Name, + ns: Option, +) -> Option { + if let Some(Namespace::Types | Namespace::Macros) = ns { + return None; + } + def.fields(db).into_iter().find(|f| f.name(db) == name).map(DocLinkDef::Field) +} + +fn modpath_from_str(db: &dyn HirDatabase, link: &str) -> Option { + // FIXME: this is not how we should get a mod path here. + let try_get_modpath = |link: &str| { let ast_path = ast::SourceFile::parse(&format!("type T = {link};")) .syntax_node() .descendants() @@ -193,23 +309,20 @@ fn resolve_doc_path( if ast_path.syntax().text() != link { return None; } - ModPath::from_src(db.upcast(), ast_path, &Hygiene::new_unhygienic())? + ModPath::from_src(db.upcast(), ast_path, &Hygiene::new_unhygienic()) }; - let resolved = resolver.resolve_module_path_in_items(db.upcast(), &modpath); - let resolved = if resolved.is_none() { - resolver.resolve_module_path_in_trait_assoc_items(db.upcast(), &modpath)? - } else { - resolved - }; - match ns { - Some(Namespace::Types) => resolved.take_types(), - Some(Namespace::Values) => resolved.take_values(), - Some(Namespace::Macros) => resolved.take_macros().map(ModuleDefId::MacroId), - None => resolved.iter_items().next().map(|it| match it { - ItemInNs::Types(it) => it, - ItemInNs::Values(it) => it, - ItemInNs::Macros(it) => ModuleDefId::MacroId(it), - }), + let full = try_get_modpath(link); + if full.is_some() { + return full; } + + // Tuple field names cannot be a part of `ModPath` usually, but rustdoc can + // resolve doc paths like `TupleStruct::0`. + // FIXME: Find a better way to handle these. + let (base, maybe_tuple_field) = link.rsplit_once("::")?; + let tuple_field = Name::new_tuple_field(maybe_tuple_field.parse().ok()?); + let mut modpath = try_get_modpath(base)?; + modpath.push_segment(tuple_field); + Some(modpath) } diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index bf041b61f2f..de60c88844d 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -87,7 +87,7 @@ use triomphe::Arc; use crate::db::{DefDatabase, HirDatabase}; pub use crate::{ - attrs::{HasAttrs, Namespace}, + attrs::{DocLinkDef, HasAttrs, Namespace}, diagnostics::{ AnyDiagnostic, BreakOutsideOfLoop, CaseType, ExpectedFunction, InactiveCode, IncoherentImpl, IncorrectCase, InvalidDeriveTarget, MacroDefError, MacroError, diff --git a/crates/ide-db/src/defs.rs b/crates/ide-db/src/defs.rs index 5e4562d9c58..4ce80532e8e 100644 --- a/crates/ide-db/src/defs.rs +++ b/crates/ide-db/src/defs.rs @@ -7,7 +7,7 @@ use arrayvec::ArrayVec; use hir::{ - Adt, AsAssocItem, AssocItem, BuiltinAttr, BuiltinType, Const, Crate, DeriveHelper, + Adt, AsAssocItem, AssocItem, BuiltinAttr, BuiltinType, Const, Crate, DeriveHelper, DocLinkDef, ExternCrateDecl, Field, Function, GenericParam, HasVisibility, Impl, Label, Local, Macro, Module, ModuleDef, Name, PathResolution, Semantics, Static, ToolModule, Trait, TraitAlias, TypeAlias, Variant, Visibility, @@ -649,3 +649,13 @@ impl From for Definition { } } } + +impl From for Definition { + fn from(def: DocLinkDef) -> Self { + match def { + DocLinkDef::ModuleDef(it) => it.into(), + DocLinkDef::Field(it) => it.into(), + DocLinkDef::SelfType(it) => it.into(), + } + } +} diff --git a/crates/ide/src/doc_links/tests.rs b/crates/ide/src/doc_links/tests.rs index 05a64b33bfd..8036c77072b 100644 --- a/crates/ide/src/doc_links/tests.rs +++ b/crates/ide/src/doc_links/tests.rs @@ -517,6 +517,62 @@ fn function(); ) } +#[test] +fn doc_links_field() { + check_doc_links( + r#" +/// [`S::f`] +/// [`S2::f`] +/// [`T::0`] +/// [`U::a`] +/// [`E::A::f`] +/// [`E::B::0`] +struct S$0 { + f: i32, + //^ S::f + //^ S2::f +} +type S2 = S; +struct T(i32); + //^^^ T::0 +union U { + a: i32, + //^ U::a +} +enum E { + A { f: i32 }, + //^ E::A::f + B(i32), + //^^^ E::B::0 +} +"#, + ); +} + +#[test] +fn doc_links_field_via_self() { + check_doc_links( + r#" +/// [`Self::f`] +struct S$0 { + f: i32, + //^ Self::f +} +"#, + ); +} + +#[test] +fn doc_links_tuple_field_via_self() { + check_doc_links( + r#" +/// [`Self::0`] +struct S$0(i32); + //^^^ Self::0 +"#, + ); +} + #[test] fn rewrite_html_root_url() { check_rewrite(