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