]> git.lizzy.rs Git - rust.git/blob - src/tools/clippy/clippy_lints/src/doc.rs
Rollup merge of #91562 - dtolnay:asyncspace, r=Mark-Simulacrum
[rust.git] / src / tools / clippy / clippy_lints / src / doc.rs
1 use clippy_utils::attrs::is_doc_hidden;
2 use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then};
3 use clippy_utils::source::{first_line_of_span, snippet_with_applicability};
4 use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
5 use clippy_utils::{is_entrypoint_fn, is_expn_of, match_panic_def_id, method_chain_args, return_ty};
6 use if_chain::if_chain;
7 use itertools::Itertools;
8 use rustc_ast::ast::{Async, AttrKind, Attribute, Fn, FnRetTy, ItemKind};
9 use rustc_ast::token::CommentKind;
10 use rustc_data_structures::fx::FxHashSet;
11 use rustc_data_structures::sync::Lrc;
12 use rustc_errors::emitter::EmitterWriter;
13 use rustc_errors::{Applicability, Handler, SuggestionStyle};
14 use rustc_hir as hir;
15 use rustc_hir::intravisit::{self, NestedVisitorMap, Visitor};
16 use rustc_hir::{AnonConst, Expr, ExprKind, QPath};
17 use rustc_lint::{LateContext, LateLintPass};
18 use rustc_middle::hir::map::Map;
19 use rustc_middle::lint::in_external_macro;
20 use rustc_middle::ty;
21 use rustc_parse::maybe_new_parser_from_source_str;
22 use rustc_parse::parser::ForceCollect;
23 use rustc_session::parse::ParseSess;
24 use rustc_session::{declare_tool_lint, impl_lint_pass};
25 use rustc_span::def_id::LocalDefId;
26 use rustc_span::edition::Edition;
27 use rustc_span::source_map::{BytePos, FilePathMapping, MultiSpan, SourceMap, Span};
28 use rustc_span::{sym, FileName, Pos};
29 use std::io;
30 use std::ops::Range;
31 use std::thread;
32 use url::Url;
33
34 declare_clippy_lint! {
35     /// ### What it does
36     /// Checks for the presence of `_`, `::` or camel-case words
37     /// outside ticks in documentation.
38     ///
39     /// ### Why is this bad?
40     /// *Rustdoc* supports markdown formatting, `_`, `::` and
41     /// camel-case probably indicates some code which should be included between
42     /// ticks. `_` can also be used for emphasis in markdown, this lint tries to
43     /// consider that.
44     ///
45     /// ### Known problems
46     /// Lots of bad docs won’t be fixed, what the lint checks
47     /// for is limited, and there are still false positives. HTML elements and their
48     /// content are not linted.
49     ///
50     /// In addition, when writing documentation comments, including `[]` brackets
51     /// inside a link text would trip the parser. Therefore, documenting link with
52     /// `[`SmallVec<[T; INLINE_CAPACITY]>`]` and then [`SmallVec<[T; INLINE_CAPACITY]>`]: SmallVec
53     /// would fail.
54     ///
55     /// ### Examples
56     /// ```rust
57     /// /// Do something with the foo_bar parameter. See also
58     /// /// that::other::module::foo.
59     /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
60     /// fn doit(foo_bar: usize) {}
61     /// ```
62     ///
63     /// ```rust
64     /// // Link text with `[]` brackets should be written as following:
65     /// /// Consume the array and return the inner
66     /// /// [`SmallVec<[T; INLINE_CAPACITY]>`][SmallVec].
67     /// /// [SmallVec]: SmallVec
68     /// fn main() {}
69     /// ```
70     #[clippy::version = "pre 1.29.0"]
71     pub DOC_MARKDOWN,
72     pedantic,
73     "presence of `_`, `::` or camel-case outside backticks in documentation"
74 }
75
76 declare_clippy_lint! {
77     /// ### What it does
78     /// Checks for the doc comments of publicly visible
79     /// unsafe functions and warns if there is no `# Safety` section.
80     ///
81     /// ### Why is this bad?
82     /// Unsafe functions should document their safety
83     /// preconditions, so that users can be sure they are using them safely.
84     ///
85     /// ### Examples
86     /// ```rust
87     ///# type Universe = ();
88     /// /// This function should really be documented
89     /// pub unsafe fn start_apocalypse(u: &mut Universe) {
90     ///     unimplemented!();
91     /// }
92     /// ```
93     ///
94     /// At least write a line about safety:
95     ///
96     /// ```rust
97     ///# type Universe = ();
98     /// /// # Safety
99     /// ///
100     /// /// This function should not be called before the horsemen are ready.
101     /// pub unsafe fn start_apocalypse(u: &mut Universe) {
102     ///     unimplemented!();
103     /// }
104     /// ```
105     #[clippy::version = "1.39.0"]
106     pub MISSING_SAFETY_DOC,
107     style,
108     "`pub unsafe fn` without `# Safety` docs"
109 }
110
111 declare_clippy_lint! {
112     /// ### What it does
113     /// Checks the doc comments of publicly visible functions that
114     /// return a `Result` type and warns if there is no `# Errors` section.
115     ///
116     /// ### Why is this bad?
117     /// Documenting the type of errors that can be returned from a
118     /// function can help callers write code to handle the errors appropriately.
119     ///
120     /// ### Examples
121     /// Since the following function returns a `Result` it has an `# Errors` section in
122     /// its doc comment:
123     ///
124     /// ```rust
125     ///# use std::io;
126     /// /// # Errors
127     /// ///
128     /// /// Will return `Err` if `filename` does not exist or the user does not have
129     /// /// permission to read it.
130     /// pub fn read(filename: String) -> io::Result<String> {
131     ///     unimplemented!();
132     /// }
133     /// ```
134     #[clippy::version = "1.41.0"]
135     pub MISSING_ERRORS_DOC,
136     pedantic,
137     "`pub fn` returns `Result` without `# Errors` in doc comment"
138 }
139
140 declare_clippy_lint! {
141     /// ### What it does
142     /// Checks the doc comments of publicly visible functions that
143     /// may panic and warns if there is no `# Panics` section.
144     ///
145     /// ### Why is this bad?
146     /// Documenting the scenarios in which panicking occurs
147     /// can help callers who do not want to panic to avoid those situations.
148     ///
149     /// ### Examples
150     /// Since the following function may panic it has a `# Panics` section in
151     /// its doc comment:
152     ///
153     /// ```rust
154     /// /// # Panics
155     /// ///
156     /// /// Will panic if y is 0
157     /// pub fn divide_by(x: i32, y: i32) -> i32 {
158     ///     if y == 0 {
159     ///         panic!("Cannot divide by 0")
160     ///     } else {
161     ///         x / y
162     ///     }
163     /// }
164     /// ```
165     #[clippy::version = "1.52.0"]
166     pub MISSING_PANICS_DOC,
167     pedantic,
168     "`pub fn` may panic without `# Panics` in doc comment"
169 }
170
171 declare_clippy_lint! {
172     /// ### What it does
173     /// Checks for `fn main() { .. }` in doctests
174     ///
175     /// ### Why is this bad?
176     /// The test can be shorter (and likely more readable)
177     /// if the `fn main()` is left implicit.
178     ///
179     /// ### Examples
180     /// ``````rust
181     /// /// An example of a doctest with a `main()` function
182     /// ///
183     /// /// # Examples
184     /// ///
185     /// /// ```
186     /// /// fn main() {
187     /// ///     // this needs not be in an `fn`
188     /// /// }
189     /// /// ```
190     /// fn needless_main() {
191     ///     unimplemented!();
192     /// }
193     /// ``````
194     #[clippy::version = "1.40.0"]
195     pub NEEDLESS_DOCTEST_MAIN,
196     style,
197     "presence of `fn main() {` in code examples"
198 }
199
200 #[allow(clippy::module_name_repetitions)]
201 #[derive(Clone)]
202 pub struct DocMarkdown {
203     valid_idents: FxHashSet<String>,
204     in_trait_impl: bool,
205 }
206
207 impl DocMarkdown {
208     pub fn new(valid_idents: FxHashSet<String>) -> Self {
209         Self {
210             valid_idents,
211             in_trait_impl: false,
212         }
213     }
214 }
215
216 impl_lint_pass!(DocMarkdown =>
217     [DOC_MARKDOWN, MISSING_SAFETY_DOC, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, NEEDLESS_DOCTEST_MAIN]
218 );
219
220 impl<'tcx> LateLintPass<'tcx> for DocMarkdown {
221     fn check_crate(&mut self, cx: &LateContext<'tcx>) {
222         let attrs = cx.tcx.hir().attrs(hir::CRATE_HIR_ID);
223         check_attrs(cx, &self.valid_idents, attrs);
224     }
225
226     fn check_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
227         let attrs = cx.tcx.hir().attrs(item.hir_id());
228         let headers = check_attrs(cx, &self.valid_idents, attrs);
229         match item.kind {
230             hir::ItemKind::Fn(ref sig, _, body_id) => {
231                 if !(is_entrypoint_fn(cx, item.def_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) {
232                     let body = cx.tcx.hir().body(body_id);
233                     let mut fpu = FindPanicUnwrap {
234                         cx,
235                         typeck_results: cx.tcx.typeck(item.def_id),
236                         panic_span: None,
237                     };
238                     fpu.visit_expr(&body.value);
239                     lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span);
240                 }
241             },
242             hir::ItemKind::Impl(ref impl_) => {
243                 self.in_trait_impl = impl_.of_trait.is_some();
244             },
245             hir::ItemKind::Trait(_, unsafety, ..) => {
246                 if !headers.safety && unsafety == hir::Unsafety::Unsafe {
247                     span_lint(
248                         cx,
249                         MISSING_SAFETY_DOC,
250                         item.span,
251                         "docs for unsafe trait missing `# Safety` section",
252                     );
253                 }
254             },
255             _ => (),
256         }
257     }
258
259     fn check_item_post(&mut self, _cx: &LateContext<'tcx>, item: &'tcx hir::Item<'_>) {
260         if let hir::ItemKind::Impl { .. } = item.kind {
261             self.in_trait_impl = false;
262         }
263     }
264
265     fn check_trait_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::TraitItem<'_>) {
266         let attrs = cx.tcx.hir().attrs(item.hir_id());
267         let headers = check_attrs(cx, &self.valid_idents, attrs);
268         if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind {
269             if !in_external_macro(cx.tcx.sess, item.span) {
270                 lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, None, None);
271             }
272         }
273     }
274
275     fn check_impl_item(&mut self, cx: &LateContext<'tcx>, item: &'tcx hir::ImplItem<'_>) {
276         let attrs = cx.tcx.hir().attrs(item.hir_id());
277         let headers = check_attrs(cx, &self.valid_idents, attrs);
278         if self.in_trait_impl || in_external_macro(cx.tcx.sess, item.span) {
279             return;
280         }
281         if let hir::ImplItemKind::Fn(ref sig, body_id) = item.kind {
282             let body = cx.tcx.hir().body(body_id);
283             let mut fpu = FindPanicUnwrap {
284                 cx,
285                 typeck_results: cx.tcx.typeck(item.def_id),
286                 panic_span: None,
287             };
288             fpu.visit_expr(&body.value);
289             lint_for_missing_headers(cx, item.def_id, item.span, sig, headers, Some(body_id), fpu.panic_span);
290         }
291     }
292 }
293
294 fn lint_for_missing_headers<'tcx>(
295     cx: &LateContext<'tcx>,
296     def_id: LocalDefId,
297     span: impl Into<MultiSpan> + Copy,
298     sig: &hir::FnSig<'_>,
299     headers: DocHeaders,
300     body_id: Option<hir::BodyId>,
301     panic_span: Option<Span>,
302 ) {
303     if !cx.access_levels.is_exported(def_id) {
304         return; // Private functions do not require doc comments
305     }
306
307     // do not lint if any parent has `#[doc(hidden)]` attribute (#7347)
308     if cx
309         .tcx
310         .hir()
311         .parent_iter(cx.tcx.hir().local_def_id_to_hir_id(def_id))
312         .any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id)))
313     {
314         return;
315     }
316
317     if !headers.safety && sig.header.unsafety == hir::Unsafety::Unsafe {
318         span_lint(
319             cx,
320             MISSING_SAFETY_DOC,
321             span,
322             "unsafe function's docs miss `# Safety` section",
323         );
324     }
325     if !headers.panics && panic_span.is_some() {
326         span_lint_and_note(
327             cx,
328             MISSING_PANICS_DOC,
329             span,
330             "docs for function which may panic missing `# Panics` section",
331             panic_span,
332             "first possible panic found here",
333         );
334     }
335     if !headers.errors {
336         let hir_id = cx.tcx.hir().local_def_id_to_hir_id(def_id);
337         if is_type_diagnostic_item(cx, return_ty(cx, hir_id), sym::Result) {
338             span_lint(
339                 cx,
340                 MISSING_ERRORS_DOC,
341                 span,
342                 "docs for function returning `Result` missing `# Errors` section",
343             );
344         } else {
345             if_chain! {
346                 if let Some(body_id) = body_id;
347                 if let Some(future) = cx.tcx.lang_items().future_trait();
348                 let typeck = cx.tcx.typeck_body(body_id);
349                 let body = cx.tcx.hir().body(body_id);
350                 let ret_ty = typeck.expr_ty(&body.value);
351                 if implements_trait(cx, ret_ty, future, &[]);
352                 if let ty::Opaque(_, subs) = ret_ty.kind();
353                 if let Some(gen) = subs.types().next();
354                 if let ty::Generator(_, subs, _) = gen.kind();
355                 if is_type_diagnostic_item(cx, subs.as_generator().return_ty(), sym::Result);
356                 then {
357                     span_lint(
358                         cx,
359                         MISSING_ERRORS_DOC,
360                         span,
361                         "docs for function returning `Result` missing `# Errors` section",
362                     );
363                 }
364             }
365         }
366     }
367 }
368
369 /// Cleanup documentation decoration.
370 ///
371 /// We can't use `rustc_ast::attr::AttributeMethods::with_desugared_doc` or
372 /// `rustc_ast::parse::lexer::comments::strip_doc_comment_decoration` because we
373 /// need to keep track of
374 /// the spans but this function is inspired from the later.
375 #[allow(clippy::cast_possible_truncation)]
376 #[must_use]
377 pub fn strip_doc_comment_decoration(doc: &str, comment_kind: CommentKind, span: Span) -> (String, Vec<(usize, Span)>) {
378     // one-line comments lose their prefix
379     if comment_kind == CommentKind::Line {
380         let mut doc = doc.to_owned();
381         doc.push('\n');
382         let len = doc.len();
383         // +3 skips the opening delimiter
384         return (doc, vec![(len, span.with_lo(span.lo() + BytePos(3)))]);
385     }
386
387     let mut sizes = vec![];
388     let mut contains_initial_stars = false;
389     for line in doc.lines() {
390         let offset = line.as_ptr() as usize - doc.as_ptr() as usize;
391         debug_assert_eq!(offset as u32 as usize, offset);
392         contains_initial_stars |= line.trim_start().starts_with('*');
393         // +1 adds the newline, +3 skips the opening delimiter
394         sizes.push((line.len() + 1, span.with_lo(span.lo() + BytePos(3 + offset as u32))));
395     }
396     if !contains_initial_stars {
397         return (doc.to_string(), sizes);
398     }
399     // remove the initial '*'s if any
400     let mut no_stars = String::with_capacity(doc.len());
401     for line in doc.lines() {
402         let mut chars = line.chars();
403         for c in &mut chars {
404             if c.is_whitespace() {
405                 no_stars.push(c);
406             } else {
407                 no_stars.push(if c == '*' { ' ' } else { c });
408                 break;
409             }
410         }
411         no_stars.push_str(chars.as_str());
412         no_stars.push('\n');
413     }
414
415     (no_stars, sizes)
416 }
417
418 #[derive(Copy, Clone)]
419 struct DocHeaders {
420     safety: bool,
421     errors: bool,
422     panics: bool,
423 }
424
425 fn check_attrs<'a>(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders {
426     use pulldown_cmark::{BrokenLink, CowStr, Options};
427     /// We don't want the parser to choke on intra doc links. Since we don't
428     /// actually care about rendering them, just pretend that all broken links are
429     /// point to a fake address.
430     #[allow(clippy::unnecessary_wraps)] // we're following a type signature
431     fn fake_broken_link_callback<'a>(_: BrokenLink<'_>) -> Option<(CowStr<'a>, CowStr<'a>)> {
432         Some(("fake".into(), "fake".into()))
433     }
434
435     let mut doc = String::new();
436     let mut spans = vec![];
437
438     for attr in attrs {
439         if let AttrKind::DocComment(comment_kind, comment) = attr.kind {
440             let (comment, current_spans) = strip_doc_comment_decoration(&comment.as_str(), comment_kind, attr.span);
441             spans.extend_from_slice(&current_spans);
442             doc.push_str(&comment);
443         } else if attr.has_name(sym::doc) {
444             // ignore mix of sugared and non-sugared doc
445             // don't trigger the safety or errors check
446             return DocHeaders {
447                 safety: true,
448                 errors: true,
449                 panics: true,
450             };
451         }
452     }
453
454     let mut current = 0;
455     for &mut (ref mut offset, _) in &mut spans {
456         let offset_copy = *offset;
457         *offset = current;
458         current += offset_copy;
459     }
460
461     if doc.is_empty() {
462         return DocHeaders {
463             safety: false,
464             errors: false,
465             panics: false,
466         };
467     }
468
469     let mut cb = fake_broken_link_callback;
470
471     let parser =
472         pulldown_cmark::Parser::new_with_broken_link_callback(&doc, Options::empty(), Some(&mut cb)).into_offset_iter();
473     // Iterate over all `Events` and combine consecutive events into one
474     let events = parser.coalesce(|previous, current| {
475         use pulldown_cmark::Event::Text;
476
477         let previous_range = previous.1;
478         let current_range = current.1;
479
480         match (previous.0, current.0) {
481             (Text(previous), Text(current)) => {
482                 let mut previous = previous.to_string();
483                 previous.push_str(&current);
484                 Ok((Text(previous.into()), previous_range))
485             },
486             (previous, current) => Err(((previous, previous_range), (current, current_range))),
487         }
488     });
489     check_doc(cx, valid_idents, events, &spans)
490 }
491
492 const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
493
494 fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
495     cx: &LateContext<'_>,
496     valid_idents: &FxHashSet<String>,
497     events: Events,
498     spans: &[(usize, Span)],
499 ) -> DocHeaders {
500     // true if a safety header was found
501     use pulldown_cmark::Event::{
502         Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text,
503     };
504     use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph};
505     use pulldown_cmark::{CodeBlockKind, CowStr};
506
507     let mut headers = DocHeaders {
508         safety: false,
509         errors: false,
510         panics: false,
511     };
512     let mut in_code = false;
513     let mut in_link = None;
514     let mut in_heading = false;
515     let mut is_rust = false;
516     let mut edition = None;
517     let mut ticks_unbalanced = false;
518     let mut text_to_check: Vec<(CowStr<'_>, Span)> = Vec::new();
519     let mut paragraph_span = spans.get(0).expect("function isn't called if doc comment is empty").1;
520     for (event, range) in events {
521         match event {
522             Start(CodeBlock(ref kind)) => {
523                 in_code = true;
524                 if let CodeBlockKind::Fenced(lang) = kind {
525                     for item in lang.split(',') {
526                         if item == "ignore" {
527                             is_rust = false;
528                             break;
529                         }
530                         if let Some(stripped) = item.strip_prefix("edition") {
531                             is_rust = true;
532                             edition = stripped.parse::<Edition>().ok();
533                         } else if item.is_empty() || RUST_CODE.contains(&item) {
534                             is_rust = true;
535                         }
536                     }
537                 }
538             },
539             End(CodeBlock(_)) => {
540                 in_code = false;
541                 is_rust = false;
542             },
543             Start(Link(_, url, _)) => in_link = Some(url),
544             End(Link(..)) => in_link = None,
545             Start(Heading(_) | Paragraph | Item) => {
546                 if let Start(Heading(_)) = event {
547                     in_heading = true;
548                 }
549                 ticks_unbalanced = false;
550                 let (_, span) = get_current_span(spans, range.start);
551                 paragraph_span = first_line_of_span(cx, span);
552             },
553             End(Heading(_) | Paragraph | Item) => {
554                 if let End(Heading(_)) = event {
555                     in_heading = false;
556                 }
557                 if ticks_unbalanced {
558                     span_lint_and_help(
559                         cx,
560                         DOC_MARKDOWN,
561                         paragraph_span,
562                         "backticks are unbalanced",
563                         None,
564                         "a backtick may be missing a pair",
565                     );
566                 } else {
567                     for (text, span) in text_to_check {
568                         check_text(cx, valid_idents, &text, span);
569                     }
570                 }
571                 text_to_check = Vec::new();
572             },
573             Start(_tag) | End(_tag) => (), // We don't care about other tags
574             Html(_html) => (),             // HTML is weird, just ignore it
575             SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (),
576             FootnoteReference(text) | Text(text) => {
577                 let (begin, span) = get_current_span(spans, range.start);
578                 paragraph_span = paragraph_span.with_hi(span.hi());
579                 ticks_unbalanced |= text.contains('`') && !in_code;
580                 if Some(&text) == in_link.as_ref() || ticks_unbalanced {
581                     // Probably a link of the form `<http://example.com>`
582                     // Which are represented as a link to "http://example.com" with
583                     // text "http://example.com" by pulldown-cmark
584                     continue;
585                 }
586                 let trimmed_text = text.trim();
587                 headers.safety |= in_heading && trimmed_text == "Safety";
588                 headers.safety |= in_heading && trimmed_text == "Implementation safety";
589                 headers.safety |= in_heading && trimmed_text == "Implementation Safety";
590                 headers.errors |= in_heading && trimmed_text == "Errors";
591                 headers.panics |= in_heading && trimmed_text == "Panics";
592                 if in_code {
593                     if is_rust {
594                         let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
595                         check_code(cx, &text, edition, span);
596                     }
597                 } else {
598                     // Adjust for the beginning of the current `Event`
599                     let span = span.with_lo(span.lo() + BytePos::from_usize(range.start - begin));
600                     text_to_check.push((text, span));
601                 }
602             },
603         }
604     }
605     headers
606 }
607
608 fn get_current_span(spans: &[(usize, Span)], idx: usize) -> (usize, Span) {
609     let index = match spans.binary_search_by(|c| c.0.cmp(&idx)) {
610         Ok(o) => o,
611         Err(e) => e - 1,
612     };
613     spans[index]
614 }
615
616 fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, span: Span) {
617     fn has_needless_main(code: String, edition: Edition) -> bool {
618         rustc_driver::catch_fatal_errors(|| {
619             rustc_span::create_session_globals_then(edition, || {
620                 let filename = FileName::anon_source_code(&code);
621
622                 let sm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
623                 let emitter = EmitterWriter::new(Box::new(io::sink()), None, false, false, false, None, false);
624                 let handler = Handler::with_emitter(false, None, Box::new(emitter));
625                 let sess = ParseSess::with_span_handler(handler, sm);
626
627                 let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code) {
628                     Ok(p) => p,
629                     Err(errs) => {
630                         for mut err in errs {
631                             err.cancel();
632                         }
633                         return false;
634                     },
635                 };
636
637                 let mut relevant_main_found = false;
638                 loop {
639                     match parser.parse_item(ForceCollect::No) {
640                         Ok(Some(item)) => match &item.kind {
641                             // Tests with one of these items are ignored
642                             ItemKind::Static(..)
643                             | ItemKind::Const(..)
644                             | ItemKind::ExternCrate(..)
645                             | ItemKind::ForeignMod(..) => return false,
646                             // We found a main function ...
647                             ItemKind::Fn(box Fn {
648                                 sig, body: Some(block), ..
649                             }) if item.ident.name == sym::main => {
650                                 let is_async = matches!(sig.header.asyncness, Async::Yes { .. });
651                                 let returns_nothing = match &sig.decl.output {
652                                     FnRetTy::Default(..) => true,
653                                     FnRetTy::Ty(ty) if ty.kind.is_unit() => true,
654                                     FnRetTy::Ty(_) => false,
655                                 };
656
657                                 if returns_nothing && !is_async && !block.stmts.is_empty() {
658                                     // This main function should be linted, but only if there are no other functions
659                                     relevant_main_found = true;
660                                 } else {
661                                     // This main function should not be linted, we're done
662                                     return false;
663                                 }
664                             },
665                             // Another function was found; this case is ignored too
666                             ItemKind::Fn(..) => return false,
667                             _ => {},
668                         },
669                         Ok(None) => break,
670                         Err(mut e) => {
671                             e.cancel();
672                             return false;
673                         },
674                     }
675                 }
676
677                 relevant_main_found
678             })
679         })
680         .ok()
681         .unwrap_or_default()
682     }
683
684     // Because of the global session, we need to create a new session in a different thread with
685     // the edition we need.
686     let text = text.to_owned();
687     if thread::spawn(move || has_needless_main(text, edition))
688         .join()
689         .expect("thread::spawn failed")
690     {
691         span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
692     }
693 }
694
695 fn check_text(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) {
696     for word in text.split(|c: char| c.is_whitespace() || c == '\'') {
697         // Trim punctuation as in `some comment (see foo::bar).`
698         //                                                   ^^
699         // Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix.
700         let mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':');
701
702         // Remove leading or trailing single `:` which may be part of a sentence.
703         if word.starts_with(':') && !word.starts_with("::") {
704             word = word.trim_start_matches(':');
705         }
706         if word.ends_with(':') && !word.ends_with("::") {
707             word = word.trim_end_matches(':');
708         }
709
710         if valid_idents.contains(word) || word.chars().all(|c| c == ':') {
711             continue;
712         }
713
714         // Adjust for the current word
715         let offset = word.as_ptr() as usize - text.as_ptr() as usize;
716         let span = Span::new(
717             span.lo() + BytePos::from_usize(offset),
718             span.lo() + BytePos::from_usize(offset + word.len()),
719             span.ctxt(),
720             span.parent(),
721         );
722
723         check_word(cx, word, span);
724     }
725 }
726
727 fn check_word(cx: &LateContext<'_>, word: &str, span: Span) {
728     /// Checks if a string is camel-case, i.e., contains at least two uppercase
729     /// letters (`Clippy` is ok) and one lower-case letter (`NASA` is ok).
730     /// Plurals are also excluded (`IDs` is ok).
731     fn is_camel_case(s: &str) -> bool {
732         if s.starts_with(|c: char| c.is_digit(10)) {
733             return false;
734         }
735
736         let s = s.strip_suffix('s').unwrap_or(s);
737
738         s.chars().all(char::is_alphanumeric)
739             && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1
740             && s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
741     }
742
743     fn has_underscore(s: &str) -> bool {
744         s != "_" && !s.contains("\\_") && s.contains('_')
745     }
746
747     fn has_hyphen(s: &str) -> bool {
748         s != "-" && s.contains('-')
749     }
750
751     if let Ok(url) = Url::parse(word) {
752         // try to get around the fact that `foo::bar` parses as a valid URL
753         if !url.cannot_be_a_base() {
754             span_lint(
755                 cx,
756                 DOC_MARKDOWN,
757                 span,
758                 "you should put bare URLs between `<`/`>` or make a proper Markdown link",
759             );
760
761             return;
762         }
763     }
764
765     // We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343)
766     if has_underscore(word) && has_hyphen(word) {
767         return;
768     }
769
770     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
771         let mut applicability = Applicability::MachineApplicable;
772
773         span_lint_and_then(
774             cx,
775             DOC_MARKDOWN,
776             span,
777             "item in documentation is missing backticks",
778             |diag| {
779                 let snippet = snippet_with_applicability(cx, span, "..", &mut applicability);
780                 diag.span_suggestion_with_style(
781                     span,
782                     "try",
783                     format!("`{}`", snippet),
784                     applicability,
785                     // always show the suggestion in a separate line, since the
786                     // inline presentation adds another pair of backticks
787                     SuggestionStyle::ShowAlways,
788                 );
789             },
790         );
791     }
792 }
793
794 struct FindPanicUnwrap<'a, 'tcx> {
795     cx: &'a LateContext<'tcx>,
796     panic_span: Option<Span>,
797     typeck_results: &'tcx ty::TypeckResults<'tcx>,
798 }
799
800 impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> {
801     type Map = Map<'tcx>;
802
803     fn visit_expr(&mut self, expr: &'tcx Expr<'_>) {
804         if self.panic_span.is_some() {
805             return;
806         }
807
808         // check for `begin_panic`
809         if_chain! {
810             if let ExprKind::Call(func_expr, _) = expr.kind;
811             if let ExprKind::Path(QPath::Resolved(_, path)) = func_expr.kind;
812             if let Some(path_def_id) = path.res.opt_def_id();
813             if match_panic_def_id(self.cx, path_def_id);
814             if is_expn_of(expr.span, "unreachable").is_none();
815             if !is_expn_of_debug_assertions(expr.span);
816             then {
817                 self.panic_span = Some(expr.span);
818             }
819         }
820
821         // check for `assert_eq` or `assert_ne`
822         if is_expn_of(expr.span, "assert_eq").is_some() || is_expn_of(expr.span, "assert_ne").is_some() {
823             self.panic_span = Some(expr.span);
824         }
825
826         // check for `unwrap`
827         if let Some(arglists) = method_chain_args(expr, &["unwrap"]) {
828             let receiver_ty = self.typeck_results.expr_ty(&arglists[0][0]).peel_refs();
829             if is_type_diagnostic_item(self.cx, receiver_ty, sym::Option)
830                 || is_type_diagnostic_item(self.cx, receiver_ty, sym::Result)
831             {
832                 self.panic_span = Some(expr.span);
833             }
834         }
835
836         // and check sub-expressions
837         intravisit::walk_expr(self, expr);
838     }
839
840     // Panics in const blocks will cause compilation to fail.
841     fn visit_anon_const(&mut self, _: &'tcx AnonConst) {}
842
843     fn nested_visit_map(&mut self) -> NestedVisitorMap<Self::Map> {
844         NestedVisitorMap::OnlyBodies(self.cx.tcx.hir())
845     }
846 }
847
848 fn is_expn_of_debug_assertions(span: Span) -> bool {
849     const MACRO_NAMES: &[&str] = &["debug_assert", "debug_assert_eq", "debug_assert_ne"];
850     MACRO_NAMES.iter().any(|name| is_expn_of(span, name).is_some())
851 }