]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/doc.rs
Rustup to rust-lang/rust#66878
[rust.git] / clippy_lints / src / doc.rs
1 use crate::utils::span_lint;
2 use itertools::Itertools;
3 use pulldown_cmark;
4 use rustc::hir;
5 use rustc::impl_lint_pass;
6 use rustc::lint::{LateContext, LateLintPass, LintArray, LintPass};
7 use rustc_data_structures::fx::FxHashSet;
8 use rustc_session::declare_tool_lint;
9 use std::ops::Range;
10 use syntax::ast::{AttrKind, Attribute};
11 use syntax::source_map::{BytePos, Span};
12 use syntax_pos::Pos;
13 use url::Url;
14
15 declare_clippy_lint! {
16     /// **What it does:** Checks for the presence of `_`, `::` or camel-case words
17     /// outside ticks in documentation.
18     ///
19     /// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and
20     /// camel-case probably indicates some code which should be included between
21     /// ticks. `_` can also be used for emphasis in markdown, this lint tries to
22     /// consider that.
23     ///
24     /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks
25     /// for is limited, and there are still false positives.
26     ///
27     /// **Examples:**
28     /// ```rust
29     /// /// Do something with the foo_bar parameter. See also
30     /// /// that::other::module::foo.
31     /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
32     /// fn doit(foo_bar: usize) {}
33     /// ```
34     pub DOC_MARKDOWN,
35     pedantic,
36     "presence of `_`, `::` or camel-case outside backticks in documentation"
37 }
38
39 declare_clippy_lint! {
40     /// **What it does:** Checks for the doc comments of publicly visible
41     /// unsafe functions and warns if there is no `# Safety` section.
42     ///
43     /// **Why is this bad?** Unsafe functions should document their safety
44     /// preconditions, so that users can be sure they are using them safely.
45     ///
46     /// **Known problems:** None.
47     ///
48     /// **Examples**:
49     /// ```rust
50     ///# type Universe = ();
51     /// /// This function should really be documented
52     /// pub unsafe fn start_apocalypse(u: &mut Universe) {
53     ///     unimplemented!();
54     /// }
55     /// ```
56     ///
57     /// At least write a line about safety:
58     ///
59     /// ```rust
60     ///# type Universe = ();
61     /// /// # Safety
62     /// ///
63     /// /// This function should not be called before the horsemen are ready.
64     /// pub unsafe fn start_apocalypse(u: &mut Universe) {
65     ///     unimplemented!();
66     /// }
67     /// ```
68     pub MISSING_SAFETY_DOC,
69     style,
70     "`pub unsafe fn` without `# Safety` docs"
71 }
72
73 declare_clippy_lint! {
74     /// **What it does:** Checks for `fn main() { .. }` in doctests
75     ///
76     /// **Why is this bad?** The test can be shorter (and likely more readable)
77     /// if the `fn main()` is left implicit.
78     ///
79     /// **Known problems:** None.
80     ///
81     /// **Examples:**
82     /// ``````rust
83     /// /// An example of a doctest with a `main()` function
84     /// ///
85     /// /// # Examples
86     /// ///
87     /// /// ```
88     /// /// fn main() {
89     /// ///     // this needs not be in an `fn`
90     /// /// }
91     /// /// ```
92     /// fn needless_main() {
93     ///     unimplemented!();
94     /// }
95     /// ``````
96     pub NEEDLESS_DOCTEST_MAIN,
97     style,
98     "presence of `fn main() {` in code examples"
99 }
100
101 #[allow(clippy::module_name_repetitions)]
102 #[derive(Clone)]
103 pub struct DocMarkdown {
104     valid_idents: FxHashSet<String>,
105     in_trait_impl: bool,
106 }
107
108 impl DocMarkdown {
109     pub fn new(valid_idents: FxHashSet<String>) -> Self {
110         Self {
111             valid_idents,
112             in_trait_impl: false,
113         }
114     }
115 }
116
117 impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC, NEEDLESS_DOCTEST_MAIN]);
118
119 impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
120     fn check_crate(&mut self, cx: &LateContext<'a, 'tcx>, krate: &'tcx hir::Crate) {
121         check_attrs(cx, &self.valid_idents, &krate.attrs);
122     }
123
124     fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::Item) {
125         if check_attrs(cx, &self.valid_idents, &item.attrs) {
126             return;
127         }
128         // no safety header
129         match item.kind {
130             hir::ItemKind::Fn(ref sig, ..) => {
131                 if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
132                     span_lint(
133                         cx,
134                         MISSING_SAFETY_DOC,
135                         item.span,
136                         "unsafe function's docs miss `# Safety` section",
137                     );
138                 }
139             },
140             hir::ItemKind::Impl(_, _, _, _, ref trait_ref, ..) => {
141                 self.in_trait_impl = trait_ref.is_some();
142             },
143             _ => {},
144         }
145     }
146
147     fn check_item_post(&mut self, _cx: &LateContext<'a, 'tcx>, item: &'tcx hir::Item) {
148         if let hir::ItemKind::Impl(..) = item.kind {
149             self.in_trait_impl = false;
150         }
151     }
152
153     fn check_trait_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::TraitItem) {
154         if check_attrs(cx, &self.valid_idents, &item.attrs) {
155             return;
156         }
157         // no safety header
158         if let hir::TraitItemKind::Method(ref sig, ..) = item.kind {
159             if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
160                 span_lint(
161                     cx,
162                     MISSING_SAFETY_DOC,
163                     item.span,
164                     "unsafe function's docs miss `# Safety` section",
165                 );
166             }
167         }
168     }
169
170     fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::ImplItem) {
171         if check_attrs(cx, &self.valid_idents, &item.attrs) || self.in_trait_impl {
172             return;
173         }
174         // no safety header
175         if let hir::ImplItemKind::Method(ref sig, ..) = item.kind {
176             if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
177                 span_lint(
178                     cx,
179                     MISSING_SAFETY_DOC,
180                     item.span,
181                     "unsafe function's docs miss `# Safety` section",
182                 );
183             }
184         }
185     }
186 }
187
188 /// Cleanup documentation decoration (`///` and such).
189 ///
190 /// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
191 /// `syntax::parse::lexer::comments::strip_doc_comment_decoration` because we
192 /// need to keep track of
193 /// the spans but this function is inspired from the later.
194 #[allow(clippy::cast_possible_truncation)]
195 #[must_use]
196 pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(usize, Span)>) {
197     // one-line comments lose their prefix
198     const ONELINERS: &[&str] = &["///!", "///", "//!", "//"];
199     for prefix in ONELINERS {
200         if comment.starts_with(*prefix) {
201             let doc = &comment[prefix.len()..];
202             let mut doc = doc.to_owned();
203             doc.push('\n');
204             return (
205                 doc.to_owned(),
206                 vec![(doc.len(), span.with_lo(span.lo() + BytePos(prefix.len() as u32)))],
207             );
208         }
209     }
210
211     if comment.starts_with("/*") {
212         let doc = &comment[3..comment.len() - 2];
213         let mut sizes = vec![];
214         let mut contains_initial_stars = false;
215         for line in doc.lines() {
216             let offset = line.as_ptr() as usize - comment.as_ptr() as usize;
217             debug_assert_eq!(offset as u32 as usize, offset);
218             contains_initial_stars |= line.trim_start().starts_with('*');
219             // +1 for the newline
220             sizes.push((line.len() + 1, span.with_lo(span.lo() + BytePos(offset as u32))));
221         }
222         if !contains_initial_stars {
223             return (doc.to_string(), sizes);
224         }
225         // remove the initial '*'s if any
226         let mut no_stars = String::with_capacity(doc.len());
227         for line in doc.lines() {
228             let mut chars = line.chars();
229             while let Some(c) = chars.next() {
230                 if c.is_whitespace() {
231                     no_stars.push(c);
232                 } else {
233                     no_stars.push(if c == '*' { ' ' } else { c });
234                     break;
235                 }
236             }
237             no_stars.push_str(chars.as_str());
238             no_stars.push('\n');
239         }
240         return (no_stars, sizes);
241     }
242
243     panic!("not a doc-comment: {}", comment);
244 }
245
246 pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> bool {
247     let mut doc = String::new();
248     let mut spans = vec![];
249
250     for attr in attrs {
251         if let AttrKind::DocComment(ref comment) = attr.kind {
252             let comment = comment.to_string();
253             let (comment, current_spans) = strip_doc_comment_decoration(&comment, attr.span);
254             spans.extend_from_slice(&current_spans);
255             doc.push_str(&comment);
256         } else if attr.check_name(sym!(doc)) {
257             // ignore mix of sugared and non-sugared doc
258             return true; // don't trigger the safety check
259         }
260     }
261
262     let mut current = 0;
263     for &mut (ref mut offset, _) in &mut spans {
264         let offset_copy = *offset;
265         *offset = current;
266         current += offset_copy;
267     }
268
269     if doc.is_empty() {
270         return false;
271     }
272
273     let parser = pulldown_cmark::Parser::new(&doc).into_offset_iter();
274     // Iterate over all `Events` and combine consecutive events into one
275     let events = parser.coalesce(|previous, current| {
276         use pulldown_cmark::Event::*;
277
278         let previous_range = previous.1;
279         let current_range = current.1;
280
281         match (previous.0, current.0) {
282             (Text(previous), Text(current)) => {
283                 let mut previous = previous.to_string();
284                 previous.push_str(&current);
285                 Ok((Text(previous.into()), previous_range))
286             },
287             (previous, current) => Err(((previous, previous_range), (current, current_range))),
288         }
289     });
290     check_doc(cx, valid_idents, events, &spans)
291 }
292
293 fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
294     cx: &LateContext<'_, '_>,
295     valid_idents: &FxHashSet<String>,
296     events: Events,
297     spans: &[(usize, Span)],
298 ) -> bool {
299     // true if a safety header was found
300     use pulldown_cmark::Event::*;
301     use pulldown_cmark::Tag::*;
302
303     let mut safety_header = false;
304     let mut in_code = false;
305     let mut in_link = None;
306     let mut in_heading = false;
307
308     for (event, range) in events {
309         match event {
310             Start(CodeBlock(_)) => in_code = true,
311             End(CodeBlock(_)) => in_code = false,
312             Start(Link(_, url, _)) => in_link = Some(url),
313             End(Link(..)) => in_link = None,
314             Start(Heading(_)) => in_heading = true,
315             End(Heading(_)) => in_heading = false,
316             Start(_tag) | End(_tag) => (), // We don't care about other tags
317             Html(_html) => (),             // HTML is weird, just ignore it
318             SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (),
319             FootnoteReference(text) | Text(text) => {
320                 if Some(&text) == in_link.as_ref() {
321                     // Probably a link of the form `<http://example.com>`
322                     // Which are represented as a link to "http://example.com" with
323                     // text "http://example.com" by pulldown-cmark
324                     continue;
325                 }
326                 safety_header |= in_heading && text.trim() == "Safety";
327                 let index = match spans.binary_search_by(|c| c.0.cmp(&range.start)) {
328                     Ok(o) => o,
329                     Err(e) => e - 1,
330                 };
331                 let (begin, span) = spans[index];
332                 if in_code {
333                     check_code(cx, &text, span);
334                 } else {
335                     // Adjust for the beginning of the current `Event`
336                     let span = span.with_lo(span.lo() + BytePos::from_usize(range.start - begin));
337
338                     check_text(cx, valid_idents, &text, span);
339                 }
340             },
341         }
342     }
343     safety_header
344 }
345
346 fn check_code(cx: &LateContext<'_, '_>, text: &str, span: Span) {
347     if text.contains("fn main() {") && !(text.contains("static") || text.contains("fn main() {}")) {
348         span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest");
349     }
350 }
351
352 fn check_text(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) {
353     for word in text.split(|c: char| c.is_whitespace() || c == '\'') {
354         // Trim punctuation as in `some comment (see foo::bar).`
355         //                                                   ^^
356         // Or even as in `_foo bar_` which is emphasized.
357         let word = word.trim_matches(|c: char| !c.is_alphanumeric());
358
359         if valid_idents.contains(word) {
360             continue;
361         }
362
363         // Adjust for the current word
364         let offset = word.as_ptr() as usize - text.as_ptr() as usize;
365         let span = Span::new(
366             span.lo() + BytePos::from_usize(offset),
367             span.lo() + BytePos::from_usize(offset + word.len()),
368             span.ctxt(),
369         );
370
371         check_word(cx, word, span);
372     }
373 }
374
375 fn check_word(cx: &LateContext<'_, '_>, word: &str, span: Span) {
376     /// Checks if a string is camel-case, i.e., contains at least two uppercase
377     /// letters (`Clippy` is ok) and one lower-case letter (`NASA` is ok).
378     /// Plurals are also excluded (`IDs` is ok).
379     fn is_camel_case(s: &str) -> bool {
380         if s.starts_with(|c: char| c.is_digit(10)) {
381             return false;
382         }
383
384         let s = if s.ends_with('s') { &s[..s.len() - 1] } else { s };
385
386         s.chars().all(char::is_alphanumeric)
387             && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1
388             && s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
389     }
390
391     fn has_underscore(s: &str) -> bool {
392         s != "_" && !s.contains("\\_") && s.contains('_')
393     }
394
395     fn has_hyphen(s: &str) -> bool {
396         s != "-" && s.contains('-')
397     }
398
399     if let Ok(url) = Url::parse(word) {
400         // try to get around the fact that `foo::bar` parses as a valid URL
401         if !url.cannot_be_a_base() {
402             span_lint(
403                 cx,
404                 DOC_MARKDOWN,
405                 span,
406                 "you should put bare URLs between `<`/`>` or make a proper Markdown link",
407             );
408
409             return;
410         }
411     }
412
413     // We assume that mixed-case words are not meant to be put inside bacticks. (Issue #2343)
414     if has_underscore(word) && has_hyphen(word) {
415         return;
416     }
417
418     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
419         span_lint(
420             cx,
421             DOC_MARKDOWN,
422             span,
423             &format!("you should put `{}` between ticks in the documentation", word),
424         );
425     }
426 }