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