]> git.lizzy.rs Git - rust.git/blob - crates/ide/src/doc_links.rs
Explicitly check for reference locals or fields in Name classification
[rust.git] / crates / ide / src / doc_links.rs
1 //! Extracts, resolves and rewrites links and intra-doc links in markdown documentation.
2
3 use std::{
4     convert::{TryFrom, TryInto},
5     iter::once,
6 };
7
8 use itertools::Itertools;
9 use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
10 use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
11 use url::Url;
12
13 use hir::{
14     db::{DefDatabase, HirDatabase},
15     Adt, AsAssocItem, AssocItem, AssocItemContainer, Crate, Field, HasAttrs, ItemInNs, ModuleDef,
16 };
17 use ide_db::{
18     defs::{Definition, NameClass, NameRefClass},
19     helpers::pick_best_token,
20     RootDatabase,
21 };
22 use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxNode, TextRange, T};
23
24 use crate::{FilePosition, Semantics};
25
26 pub(crate) type DocumentationLink = String;
27
28 /// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
29 pub(crate) fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
30     let mut cb = broken_link_clone_cb;
31     let doc =
32         Parser::new_with_broken_link_callback(markdown, Options::ENABLE_TASKLISTS, Some(&mut cb));
33
34     let doc = map_links(doc, |target, title: &str| {
35         // This check is imperfect, there's some overlap between valid intra-doc links
36         // and valid URLs so we choose to be too eager to try to resolve what might be
37         // a URL.
38         if target.contains("://") {
39             (target.to_string(), title.to_string())
40         } else {
41             // Two possibilities:
42             // * path-based links: `../../module/struct.MyStruct.html`
43             // * module-based links (AKA intra-doc links): `super::super::module::MyStruct`
44             if let Some(rewritten) = rewrite_intra_doc_link(db, *definition, target, title) {
45                 return rewritten;
46             }
47             if let Definition::ModuleDef(def) = *definition {
48                 if let Some(target) = rewrite_url_link(db, def, target) {
49                     return (target, title.to_string());
50                 }
51             }
52
53             (target.to_string(), title.to_string())
54         }
55     });
56     let mut out = String::new();
57     let mut options = CmarkOptions::default();
58     options.code_block_backticks = 3;
59     cmark_with_options(doc, &mut out, None, options).ok();
60     out
61 }
62
63 /// Remove all links in markdown documentation.
64 pub(crate) fn remove_links(markdown: &str) -> String {
65     let mut drop_link = false;
66
67     let opts = Options::ENABLE_TASKLISTS | Options::ENABLE_FOOTNOTES;
68
69     let mut cb = |_: BrokenLink| {
70         let empty = InlineStr::try_from("").unwrap();
71         Some((CowStr::Inlined(empty), CowStr::Inlined(empty)))
72     };
73     let doc = Parser::new_with_broken_link_callback(markdown, opts, Some(&mut cb));
74     let doc = doc.filter_map(move |evt| match evt {
75         Event::Start(Tag::Link(link_type, ref target, ref title)) => {
76             if link_type == LinkType::Inline && target.contains("://") {
77                 Some(Event::Start(Tag::Link(link_type, target.clone(), title.clone())))
78             } else {
79                 drop_link = true;
80                 None
81             }
82         }
83         Event::End(_) if drop_link => {
84             drop_link = false;
85             None
86         }
87         _ => Some(evt),
88     });
89
90     let mut out = String::new();
91     let mut options = CmarkOptions::default();
92     options.code_block_backticks = 3;
93     cmark_with_options(doc, &mut out, None, options).ok();
94     out
95 }
96
97 /// Retrieve a link to documentation for the given symbol.
98 pub(crate) fn external_docs(
99     db: &RootDatabase,
100     position: &FilePosition,
101 ) -> Option<DocumentationLink> {
102     let sema = Semantics::new(db);
103     let file = sema.parse(position.file_id).syntax().clone();
104     let token = pick_best_token(file.token_at_offset(position.offset), |kind| match kind {
105         IDENT | INT_NUMBER => 3,
106         T!['('] | T![')'] => 2,
107         kind if kind.is_trivia() => 0,
108         _ => 1,
109     })?;
110     let token = sema.descend_into_macros(token);
111
112     let node = token.parent()?;
113     let definition = match_ast! {
114         match node {
115             ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced_field())?,
116             ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.defined_or_referenced_field())?,
117             _ => return None,
118         }
119     };
120
121     get_doc_link(db, definition)
122 }
123
124 /// Extracts all links from a given markdown text.
125 pub(crate) fn extract_definitions_from_markdown(
126     markdown: &str,
127 ) -> Vec<(TextRange, String, Option<hir::Namespace>)> {
128     Parser::new_with_broken_link_callback(
129         markdown,
130         Options::ENABLE_TASKLISTS,
131         Some(&mut broken_link_clone_cb),
132     )
133     .into_offset_iter()
134     .filter_map(|(event, range)| {
135         if let Event::Start(Tag::Link(_, target, title)) = event {
136             let link = if target.is_empty() { title } else { target };
137             let (link, ns) = parse_intra_doc_link(&link);
138             Some((
139                 TextRange::new(range.start.try_into().ok()?, range.end.try_into().ok()?),
140                 link.to_string(),
141                 ns,
142             ))
143         } else {
144             None
145         }
146     })
147     .collect()
148 }
149
150 pub(crate) fn resolve_doc_path_for_def(
151     db: &dyn HirDatabase,
152     def: Definition,
153     link: &str,
154     ns: Option<hir::Namespace>,
155 ) -> Option<hir::ModuleDef> {
156     match def {
157         Definition::ModuleDef(def) => match def {
158             hir::ModuleDef::Module(it) => it.resolve_doc_path(db, link, ns),
159             hir::ModuleDef::Function(it) => it.resolve_doc_path(db, link, ns),
160             hir::ModuleDef::Adt(it) => it.resolve_doc_path(db, link, ns),
161             hir::ModuleDef::Variant(it) => it.resolve_doc_path(db, link, ns),
162             hir::ModuleDef::Const(it) => it.resolve_doc_path(db, link, ns),
163             hir::ModuleDef::Static(it) => it.resolve_doc_path(db, link, ns),
164             hir::ModuleDef::Trait(it) => it.resolve_doc_path(db, link, ns),
165             hir::ModuleDef::TypeAlias(it) => it.resolve_doc_path(db, link, ns),
166             hir::ModuleDef::BuiltinType(_) => None,
167         },
168         Definition::Macro(it) => it.resolve_doc_path(db, link, ns),
169         Definition::Field(it) => it.resolve_doc_path(db, link, ns),
170         Definition::SelfType(_)
171         | Definition::Local(_)
172         | Definition::GenericParam(_)
173         | Definition::Label(_) => None,
174     }
175 }
176
177 pub(crate) fn doc_attributes(
178     sema: &Semantics<RootDatabase>,
179     node: &SyntaxNode,
180 ) -> Option<(hir::AttrsWithOwner, Definition)> {
181     match_ast! {
182         match node {
183             ast::SourceFile(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
184             ast::Module(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Module(def)))),
185             ast::Fn(it)          => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Function(def)))),
186             ast::Struct(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Struct(def))))),
187             ast::Union(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Union(def))))),
188             ast::Enum(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Adt(hir::Adt::Enum(def))))),
189             ast::Variant(it)     => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Variant(def)))),
190             ast::Trait(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Trait(def)))),
191             ast::Static(it)      => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Static(def)))),
192             ast::Const(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::Const(def)))),
193             ast::TypeAlias(it)   => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::ModuleDef(hir::ModuleDef::TypeAlias(def)))),
194             ast::Impl(it)        => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::SelfType(def))),
195             ast::RecordField(it) => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
196             ast::TupleField(it)  => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Field(def))),
197             ast::Macro(it)       => sema.to_def(&it).map(|def| (def.attrs(sema.db), Definition::Macro(def))),
198             // ast::Use(it) => sema.to_def(&it).map(|def| (Box::new(it) as _, def.attrs(sema.db))),
199             _ => None
200         }
201     }
202 }
203
204 fn broken_link_clone_cb<'a, 'b>(link: BrokenLink<'a>) -> Option<(CowStr<'b>, CowStr<'b>)> {
205     // These allocations are actually unnecessary but the lifetimes on BrokenLinkCallback are wrong
206     // this is fixed in the repo but not on the crates.io release yet
207     Some((
208         /*url*/ link.reference.to_owned().into(),
209         /*title*/ link.reference.to_owned().into(),
210     ))
211 }
212
213 // FIXME:
214 // BUG: For Option::Some
215 // Returns https://doc.rust-lang.org/nightly/core/prelude/v1/enum.Option.html#variant.Some
216 // Instead of https://doc.rust-lang.org/nightly/core/option/enum.Option.html
217 //
218 // This should cease to be a problem if RFC2988 (Stable Rustdoc URLs) is implemented
219 // https://github.com/rust-lang/rfcs/pull/2988
220 fn get_doc_link(db: &RootDatabase, definition: Definition) -> Option<String> {
221     // Get the outermost definition for the module def. This is used to resolve the public path to the type,
222     // then we can join the method, field, etc onto it if required.
223     let target_def: ModuleDef = match definition {
224         Definition::ModuleDef(def) => match def {
225             ModuleDef::Function(f) => f
226                 .as_assoc_item(db)
227                 .and_then(|assoc| match assoc.container(db) {
228                     AssocItemContainer::Trait(t) => Some(t.into()),
229                     AssocItemContainer::Impl(impl_) => {
230                         impl_.self_ty(db).as_adt().map(|adt| adt.into())
231                     }
232                 })
233                 .unwrap_or_else(|| def),
234             def => def,
235         },
236         Definition::Field(f) => f.parent_def(db).into(),
237         // FIXME: Handle macros
238         _ => return None,
239     };
240
241     let ns = ItemInNs::from(target_def);
242
243     let krate = match definition {
244         // Definition::module gives back the parent module, we don't want that as it fails for root modules
245         Definition::ModuleDef(ModuleDef::Module(module)) => module.krate(),
246         _ => definition.module(db)?.krate(),
247     };
248     // FIXME: using import map doesn't make sense here. What we want here is
249     // canonical path. What import map returns is the shortest path suitable for
250     // import. See this test:
251     cov_mark::hit!(test_reexport_order);
252     let import_map = db.import_map(krate.into());
253
254     let mut base = krate.display_name(db)?.to_string();
255     let is_root_module = matches!(
256         definition,
257         Definition::ModuleDef(ModuleDef::Module(module)) if krate.root_module(db) == module
258     );
259     if !is_root_module {
260         base = once(base)
261             .chain(import_map.path_of(ns)?.segments.iter().map(|name| name.to_string()))
262             .join("/");
263     }
264     base += "/";
265
266     let filename = get_symbol_filename(db, &target_def);
267     let fragment = match definition {
268         Definition::ModuleDef(def) => match def {
269             ModuleDef::Function(f) => {
270                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Function(f)))
271             }
272             ModuleDef::Const(c) => {
273                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::Const(c)))
274             }
275             ModuleDef::TypeAlias(ty) => {
276                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(AssocItem::TypeAlias(ty)))
277             }
278             _ => None,
279         },
280         Definition::Field(field) => get_symbol_fragment(db, &FieldOrAssocItem::Field(field)),
281         _ => None,
282     };
283
284     get_doc_url(db, &krate)?
285         .join(&base)
286         .ok()
287         .and_then(|mut url| {
288             if !matches!(definition, Definition::ModuleDef(ModuleDef::Module(..))) {
289                 url.path_segments_mut().ok()?.pop();
290             };
291             Some(url)
292         })
293         .and_then(|url| url.join(filename.as_deref()?).ok())
294         .and_then(
295             |url| if let Some(fragment) = fragment { url.join(&fragment).ok() } else { Some(url) },
296         )
297         .map(|url| url.into())
298 }
299
300 fn rewrite_intra_doc_link(
301     db: &RootDatabase,
302     def: Definition,
303     target: &str,
304     title: &str,
305 ) -> Option<(String, String)> {
306     let link = if target.is_empty() { title } else { target };
307     let (link, ns) = parse_intra_doc_link(link);
308     let resolved = resolve_doc_path_for_def(db, def, link, ns)?;
309     let krate = resolved.module(db)?.krate();
310     let canonical_path = resolved.canonical_path(db)?;
311     let mut new_url = get_doc_url(db, &krate)?
312         .join(&format!("{}/", krate.display_name(db)?))
313         .ok()?
314         .join(&canonical_path.replace("::", "/"))
315         .ok()?
316         .join(&get_symbol_filename(db, &resolved)?)
317         .ok()?;
318
319     if let ModuleDef::Trait(t) = resolved {
320         if let Some(assoc_item) = t.items(db).into_iter().find_map(|assoc_item| {
321             if let Some(name) = assoc_item.name(db) {
322                 if *link == format!("{}::{}", canonical_path, name) {
323                     return Some(assoc_item);
324                 }
325             }
326             None
327         }) {
328             if let Some(fragment) =
329                 get_symbol_fragment(db, &FieldOrAssocItem::AssocItem(assoc_item))
330             {
331                 new_url = new_url.join(&fragment).ok()?;
332             }
333         };
334     }
335
336     Some((new_url.into(), strip_prefixes_suffixes(title).to_string()))
337 }
338
339 /// Try to resolve path to local documentation via path-based links (i.e. `../gateway/struct.Shard.html`).
340 fn rewrite_url_link(db: &RootDatabase, def: ModuleDef, target: &str) -> Option<String> {
341     if !(target.contains('#') || target.contains(".html")) {
342         return None;
343     }
344
345     let module = def.module(db)?;
346     let krate = module.krate();
347     let canonical_path = def.canonical_path(db)?;
348     let base = format!("{}/{}", krate.display_name(db)?, canonical_path.replace("::", "/"));
349
350     get_doc_url(db, &krate)
351         .and_then(|url| url.join(&base).ok())
352         .and_then(|url| {
353             get_symbol_filename(db, &def).as_deref().map(|f| url.join(f).ok()).flatten()
354         })
355         .and_then(|url| url.join(target).ok())
356         .map(|url| url.into())
357 }
358
359 /// Rewrites a markdown document, applying 'callback' to each link.
360 fn map_links<'e>(
361     events: impl Iterator<Item = Event<'e>>,
362     callback: impl Fn(&str, &str) -> (String, String),
363 ) -> impl Iterator<Item = Event<'e>> {
364     let mut in_link = false;
365     let mut link_target: Option<CowStr> = None;
366
367     events.map(move |evt| match evt {
368         Event::Start(Tag::Link(_link_type, ref target, _)) => {
369             in_link = true;
370             link_target = Some(target.clone());
371             evt
372         }
373         Event::End(Tag::Link(link_type, _target, _)) => {
374             in_link = false;
375             Event::End(Tag::Link(link_type, link_target.take().unwrap(), CowStr::Borrowed("")))
376         }
377         Event::Text(s) if in_link => {
378             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
379             link_target = Some(CowStr::Boxed(link_target_s.into()));
380             Event::Text(CowStr::Boxed(link_name.into()))
381         }
382         Event::Code(s) if in_link => {
383             let (link_target_s, link_name) = callback(&link_target.take().unwrap(), &s);
384             link_target = Some(CowStr::Boxed(link_target_s.into()));
385             Event::Code(CowStr::Boxed(link_name.into()))
386         }
387         _ => evt,
388     })
389 }
390
391 const TYPES: ([&str; 9], [&str; 0]) =
392     (["type", "struct", "enum", "mod", "trait", "union", "module", "prim", "primitive"], []);
393 const VALUES: ([&str; 8], [&str; 1]) =
394     (["value", "function", "fn", "method", "const", "static", "mod", "module"], ["()"]);
395 const MACROS: ([&str; 2], [&str; 1]) = (["macro", "derive"], ["!"]);
396
397 /// Extract the specified namespace from an intra-doc-link if one exists.
398 ///
399 /// # Examples
400 ///
401 /// * `struct MyStruct` -> ("MyStruct", `Namespace::Types`)
402 /// * `panic!` -> ("panic", `Namespace::Macros`)
403 /// * `fn@from_intra_spec` -> ("from_intra_spec", `Namespace::Values`)
404 fn parse_intra_doc_link(s: &str) -> (&str, Option<hir::Namespace>) {
405     let s = s.trim_matches('`');
406
407     [
408         (hir::Namespace::Types, (TYPES.0.iter(), TYPES.1.iter())),
409         (hir::Namespace::Values, (VALUES.0.iter(), VALUES.1.iter())),
410         (hir::Namespace::Macros, (MACROS.0.iter(), MACROS.1.iter())),
411     ]
412     .iter()
413     .cloned()
414     .find_map(|(ns, (mut prefixes, mut suffixes))| {
415         if let Some(prefix) = prefixes.find(|&&prefix| {
416             s.starts_with(prefix)
417                 && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ')
418         }) {
419             Some((&s[prefix.len() + 1..], ns))
420         } else {
421             suffixes.find_map(|&suffix| s.strip_suffix(suffix).zip(Some(ns)))
422         }
423     })
424     .map_or((s, None), |(s, ns)| (s, Some(ns)))
425 }
426
427 fn strip_prefixes_suffixes(s: &str) -> &str {
428     [
429         (TYPES.0.iter(), TYPES.1.iter()),
430         (VALUES.0.iter(), VALUES.1.iter()),
431         (MACROS.0.iter(), MACROS.1.iter()),
432     ]
433     .iter()
434     .cloned()
435     .find_map(|(mut prefixes, mut suffixes)| {
436         if let Some(prefix) = prefixes.find(|&&prefix| {
437             s.starts_with(prefix)
438                 && s.chars().nth(prefix.len()).map_or(false, |c| c == '@' || c == ' ')
439         }) {
440             Some(&s[prefix.len() + 1..])
441         } else {
442             suffixes.find_map(|&suffix| s.strip_suffix(suffix))
443         }
444     })
445     .unwrap_or(s)
446 }
447
448 /// Get the root URL for the documentation of a crate.
449 ///
450 /// ```
451 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
452 /// ^^^^^^^^^^^^^^^^^^^^^^^^^^
453 /// ```
454 fn get_doc_url(db: &RootDatabase, krate: &Crate) -> Option<Url> {
455     krate
456         .get_html_root_url(db)
457         .or_else(|| {
458             // Fallback to docs.rs. This uses `display_name` and can never be
459             // correct, but that's what fallbacks are about.
460             //
461             // FIXME: clicking on the link should just open the file in the editor,
462             // instead of falling back to external urls.
463             Some(format!("https://docs.rs/{}/*/", krate.display_name(db)?))
464         })
465         .and_then(|s| Url::parse(&s).ok())
466 }
467
468 /// Get the filename and extension generated for a symbol by rustdoc.
469 ///
470 /// ```
471 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
472 ///                                    ^^^^^^^^^^^^^^^^^^^
473 /// ```
474 fn get_symbol_filename(db: &dyn HirDatabase, definition: &ModuleDef) -> Option<String> {
475     Some(match definition {
476         ModuleDef::Adt(adt) => match adt {
477             Adt::Struct(s) => format!("struct.{}.html", s.name(db)),
478             Adt::Enum(e) => format!("enum.{}.html", e.name(db)),
479             Adt::Union(u) => format!("union.{}.html", u.name(db)),
480         },
481         ModuleDef::Module(_) => "index.html".to_string(),
482         ModuleDef::Trait(t) => format!("trait.{}.html", t.name(db)),
483         ModuleDef::TypeAlias(t) => format!("type.{}.html", t.name(db)),
484         ModuleDef::BuiltinType(t) => format!("primitive.{}.html", t.name()),
485         ModuleDef::Function(f) => format!("fn.{}.html", f.name(db)),
486         ModuleDef::Variant(ev) => {
487             format!("enum.{}.html#variant.{}", ev.parent_enum(db).name(db), ev.name(db))
488         }
489         ModuleDef::Const(c) => format!("const.{}.html", c.name(db)?),
490         ModuleDef::Static(s) => format!("static.{}.html", s.name(db)?),
491     })
492 }
493
494 enum FieldOrAssocItem {
495     Field(Field),
496     AssocItem(AssocItem),
497 }
498
499 /// Get the fragment required to link to a specific field, method, associated type, or associated constant.
500 ///
501 /// ```
502 /// https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
503 ///                                                       ^^^^^^^^^^^^^^
504 /// ```
505 fn get_symbol_fragment(db: &dyn HirDatabase, field_or_assoc: &FieldOrAssocItem) -> Option<String> {
506     Some(match field_or_assoc {
507         FieldOrAssocItem::Field(field) => format!("#structfield.{}", field.name(db)),
508         FieldOrAssocItem::AssocItem(assoc) => match assoc {
509             AssocItem::Function(function) => {
510                 let is_trait_method = function
511                     .as_assoc_item(db)
512                     .and_then(|assoc| assoc.containing_trait(db))
513                     .is_some();
514                 // This distinction may get more complicated when specialization is available.
515                 // Rustdoc makes this decision based on whether a method 'has defaultness'.
516                 // Currently this is only the case for provided trait methods.
517                 if is_trait_method && !function.has_body(db) {
518                     format!("#tymethod.{}", function.name(db))
519                 } else {
520                     format!("#method.{}", function.name(db))
521                 }
522             }
523             AssocItem::Const(constant) => format!("#associatedconstant.{}", constant.name(db)?),
524             AssocItem::TypeAlias(ty) => format!("#associatedtype.{}", ty.name(db)),
525         },
526     })
527 }
528
529 #[cfg(test)]
530 mod tests {
531     use expect_test::{expect, Expect};
532
533     use crate::fixture;
534
535     fn check(ra_fixture: &str, expect: Expect) {
536         let (analysis, position) = fixture::position(ra_fixture);
537         let url = analysis.external_docs(position).unwrap().expect("could not find url for symbol");
538
539         expect.assert_eq(&url)
540     }
541
542     #[test]
543     fn test_doc_url_crate() {
544         check(
545             r#"
546 //- /main.rs crate:main deps:test
547 use test$0::Foo;
548 //- /lib.rs crate:test
549 pub struct Foo;
550 "#,
551             expect![[r#"https://docs.rs/test/*/test/index.html"#]],
552         );
553     }
554
555     #[test]
556     fn test_doc_url_struct() {
557         check(
558             r#"
559 pub struct Fo$0o;
560 "#,
561             expect![[r#"https://docs.rs/test/*/test/struct.Foo.html"#]],
562         );
563     }
564
565     #[test]
566     fn test_doc_url_fn() {
567         check(
568             r#"
569 pub fn fo$0o() {}
570 "#,
571             expect![[r##"https://docs.rs/test/*/test/fn.foo.html#method.foo"##]],
572         );
573     }
574
575     #[test]
576     fn test_doc_url_inherent_method() {
577         check(
578             r#"
579 pub struct Foo;
580
581 impl Foo {
582     pub fn met$0hod() {}
583 }
584
585 "#,
586             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#method.method"##]],
587         );
588     }
589
590     #[test]
591     fn test_doc_url_trait_provided_method() {
592         check(
593             r#"
594 pub trait Bar {
595     fn met$0hod() {}
596 }
597
598 "#,
599             expect![[r##"https://docs.rs/test/*/test/trait.Bar.html#method.method"##]],
600         );
601     }
602
603     #[test]
604     fn test_doc_url_trait_required_method() {
605         check(
606             r#"
607 pub trait Foo {
608     fn met$0hod();
609 }
610
611 "#,
612             expect![[r##"https://docs.rs/test/*/test/trait.Foo.html#tymethod.method"##]],
613         );
614     }
615
616     #[test]
617     fn test_doc_url_field() {
618         check(
619             r#"
620 pub struct Foo {
621     pub fie$0ld: ()
622 }
623
624 "#,
625             expect![[r##"https://docs.rs/test/*/test/struct.Foo.html#structfield.field"##]],
626         );
627     }
628
629     #[test]
630     fn test_module() {
631         check(
632             r#"
633 pub mod foo {
634     pub mod ba$0r {}
635 }
636         "#,
637             expect![[r#"https://docs.rs/test/*/test/foo/bar/index.html"#]],
638         )
639     }
640
641     #[test]
642     fn test_reexport_order() {
643         cov_mark::check!(test_reexport_order);
644         // FIXME: This should return
645         //
646         //    https://docs.rs/test/*/test/wrapper/modulestruct.Item.html
647         //
648         // That is, we should point inside the module, rather than at the
649         // re-export.
650         check(
651             r#"
652 pub mod wrapper {
653     pub use module::Item;
654
655     pub mod module {
656         pub struct Item;
657     }
658 }
659
660 fn foo() {
661     let bar: wrapper::It$0em;
662 }
663         "#,
664             expect![[r#"https://docs.rs/test/*/test/wrapper/struct.Item.html"#]],
665         )
666     }
667 }