]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/doc.rs
Merge pull request #1060 from Manishearth/sugg
[rust.git] / clippy_lints / src / doc.rs
1 use rustc::lint::*;
2 use syntax::ast;
3 use syntax::codemap::{Span, BytePos};
4 use utils::span_lint;
5
6 /// **What it does:** This lint checks for the presence of `_`, `::` or camel-case words outside
7 /// ticks in documentation.
8 ///
9 /// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and camel-case probably
10 /// indicates some code which should be included between ticks. `_` can also be used for empasis in
11 /// markdown, this lint tries to consider that.
12 ///
13 /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks for is limited.
14 ///
15 /// **Examples:**
16 /// ```rust
17 /// /// Do something with the foo_bar parameter. See also that::other::module::foo.
18 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
19 /// fn doit(foo_bar) { .. }
20 /// ```
21 declare_lint! {
22     pub DOC_MARKDOWN, Warn,
23     "checks for the presence of `_`, `::` or camel-case outside ticks in documentation"
24 }
25
26 #[derive(Clone)]
27 pub struct Doc {
28     valid_idents: Vec<String>,
29 }
30
31 impl Doc {
32     pub fn new(valid_idents: Vec<String>) -> Self {
33         Doc { valid_idents: valid_idents }
34     }
35 }
36
37 impl LintPass for Doc {
38     fn get_lints(&self) -> LintArray {
39         lint_array![DOC_MARKDOWN]
40     }
41 }
42
43 impl EarlyLintPass for Doc {
44     fn check_crate(&mut self, cx: &EarlyContext, krate: &ast::Crate) {
45         check_attrs(cx, &self.valid_idents, &krate.attrs);
46     }
47
48     fn check_item(&mut self, cx: &EarlyContext, item: &ast::Item) {
49         check_attrs(cx, &self.valid_idents, &item.attrs);
50     }
51 }
52
53 /// Cleanup documentation decoration (`///` and such).
54 ///
55 /// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
56 /// `syntax::parse::lexer::comments::strip_doc_comment_decoration` because we need to keep track of
57 /// the span but this function is inspired from the later.
58 #[allow(cast_possible_truncation)]
59 pub fn strip_doc_comment_decoration((comment, span): (&str, Span)) -> Vec<(&str, Span)> {
60     // one-line comments lose their prefix
61     const ONELINERS: &'static [&'static str] = &["///!", "///", "//!", "//"];
62     for prefix in ONELINERS {
63         if comment.starts_with(*prefix) {
64             return vec![(
65                 &comment[prefix.len()..],
66                 Span { lo: span.lo + BytePos(prefix.len() as u32), ..span }
67             )];
68         }
69     }
70
71     if comment.starts_with("/*") {
72         return comment[3..comment.len() - 2].lines().map(|line| {
73             let offset = line.as_ptr() as usize - comment.as_ptr() as usize;
74             debug_assert_eq!(offset as u32 as usize, offset);
75
76             (
77                 line,
78                 Span {
79                     lo: span.lo + BytePos(offset as u32),
80                     ..span
81                 }
82             )
83         }).collect();
84     }
85
86     panic!("not a doc-comment: {}", comment);
87 }
88
89 pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
90     let mut docs = vec![];
91
92     for attr in attrs {
93         if attr.node.is_sugared_doc {
94             if let ast::MetaItemKind::NameValue(_, ref doc) = attr.node.value.node {
95                 if let ast::LitKind::Str(ref doc, _) = doc.node {
96                     docs.extend_from_slice(&strip_doc_comment_decoration((doc, attr.span)));
97                 }
98             }
99         }
100     }
101
102     if !docs.is_empty() {
103         let _ = check_doc(cx, valid_idents, &docs);
104     }
105 }
106
107 #[allow(while_let_loop)] // #362
108 fn check_doc(cx: &EarlyContext, valid_idents: &[String], docs: &[(&str, Span)]) -> Result<(), ()> {
109     // In markdown, `_` can be used to emphasize something, or, is a raw `_` depending on context.
110     // There really is no markdown specification that would disambiguate this properly. This is
111     // what GitHub and Rustdoc do:
112     //
113     // foo_bar test_quz    → foo_bar test_quz
114     // foo_bar_baz         → foo_bar_baz (note that the “official” spec says this should be emphasized)
115     // _foo bar_ test_quz_ → <em>foo bar</em> test_quz_
116     // \_foo bar\_         → _foo bar_
117     // (_baz_)             → (<em>baz</em>)
118     // foo _ bar _ baz     → foo _ bar _ baz
119
120     /// Character that can appear in a path
121     fn is_path_char(c: char) -> bool {
122         match c {
123             t if t.is_alphanumeric() => true,
124             ':' | '_' => true,
125             _ => false,
126         }
127     }
128
129     #[derive(Clone, Debug)]
130     /// This type is used to iterate through the documentation characters, keeping the span at the
131     /// same time.
132     struct Parser<'a> {
133         /// First byte of the current potential match
134         current_word_begin: usize,
135         /// List of lines and their associated span
136         docs: &'a [(&'a str, Span)],
137         /// Index of the current line we are parsing
138         line: usize,
139         /// Whether we are in a link
140         link: bool,
141         /// Whether we are at the beginning of a line
142         new_line: bool,
143         /// Whether we were to the end of a line last time `next` was called
144         reset: bool,
145         /// The position of the current character within the current line
146         pos: usize,
147     }
148
149     impl<'a> Parser<'a> {
150         fn advance_begin(&mut self) {
151             self.current_word_begin = self.pos;
152         }
153
154         fn line(&self) -> (&'a str, Span) {
155             self.docs[self.line]
156         }
157
158         fn peek(&self) -> Option<char> {
159             self.line().0[self.pos..].chars().next()
160         }
161
162         #[allow(while_let_on_iterator)] // borrowck complains about for
163         fn jump_to(&mut self, n: char) -> Result<bool, ()> {
164             while let Some((new_line, c)) = self.next() {
165                 if c == n {
166                     self.advance_begin();
167                     return Ok(new_line);
168                 }
169             }
170
171             Err(())
172         }
173
174         fn next_line(&mut self) {
175             self.pos = 0;
176             self.current_word_begin = 0;
177             self.line += 1;
178             self.new_line = true;
179         }
180
181         fn put_back(&mut self, c: char) {
182             self.pos -= c.len_utf8();
183         }
184
185         #[allow(cast_possible_truncation)]
186         fn word(&self) -> (&'a str, Span) {
187             let begin = self.current_word_begin;
188             let end = self.pos;
189
190             debug_assert_eq!(end as u32 as usize, end);
191             debug_assert_eq!(begin as u32 as usize, begin);
192
193             let (doc, mut span) = self.line();
194             span.hi = span.lo + BytePos(end as u32);
195             span.lo = span.lo + BytePos(begin as u32);
196
197             (&doc[begin..end], span)
198         }
199     }
200
201     impl<'a> Iterator for Parser<'a> {
202         type Item = (bool, char);
203
204         fn next(&mut self) -> Option<(bool, char)> {
205             while self.line < self.docs.len() {
206                 if self.reset {
207                     self.line += 1;
208                     self.reset = false;
209                     self.pos = 0;
210                     self.current_word_begin = 0;
211                 }
212
213                 let mut chars = self.line().0[self.pos..].chars();
214                 let c = chars.next();
215
216                 if let Some(c) = c {
217                     self.pos += c.len_utf8();
218                     let new_line = self.new_line;
219                     self.new_line = c == '\n' || (self.new_line && c.is_whitespace());
220                     return Some((new_line, c));
221                 } else if self.line == self.docs.len() - 1 {
222                     return None;
223                 } else {
224                     self.new_line = true;
225                     self.reset = true;
226                     self.pos += 1;
227                     return Some((true, '\n'));
228                 }
229             }
230
231             None
232         }
233     }
234
235     let mut parser = Parser {
236         current_word_begin: 0,
237         docs: docs,
238         line: 0,
239         link: false,
240         new_line: true,
241         reset: false,
242         pos: 0,
243     };
244
245     /// Check for fanced code block.
246     macro_rules! check_block {
247         ($parser:expr, $c:tt, $new_line:expr) => {{
248             check_block!($parser, $c, $c, $new_line)
249         }};
250
251         ($parser:expr, $c:pat, $c_expr:expr, $new_line:expr) => {{
252             fn check_block(parser: &mut Parser, new_line: bool) -> Result<bool, ()> {
253                 if new_line {
254                     let mut lookup_parser = parser.clone();
255                     if let (Some((false, $c)), Some((false, $c))) = (lookup_parser.next(), lookup_parser.next()) {
256                         *parser = lookup_parser;
257                         // 3 or more ` or ~ open a code block to be closed with the same number of ` or ~
258                         let mut open_count = 3;
259                         while let Some((false, $c)) = parser.next() {
260                             open_count += 1;
261                         }
262
263                         loop {
264                             loop {
265                                 if try!(parser.jump_to($c_expr)) {
266                                     break;
267                                 }
268                             }
269
270                             lookup_parser = parser.clone();
271                             if let (Some((false, $c)), Some((false, $c))) = (lookup_parser.next(), lookup_parser.next()) {
272                                 let mut close_count = 3;
273                                 while let Some((false, $c)) = lookup_parser.next() {
274                                     close_count += 1;
275                                 }
276
277                                 if close_count == open_count {
278                                     *parser = lookup_parser;
279                                     return Ok(true);
280                                 }
281                             }
282                         }
283                     }
284                 }
285
286                 Ok(false)
287             }
288
289             check_block(&mut $parser, $new_line)
290         }};
291     }
292
293     loop {
294         match parser.next() {
295             Some((new_line, c)) => {
296                 match c {
297                     '#' if new_line => { // don’t warn on titles
298                         parser.next_line();
299                     }
300                     '`' => {
301                         if try!(check_block!(parser, '`', new_line)) {
302                             continue;
303                         }
304
305                         try!(parser.jump_to('`')); // not a code block, just inline code
306                     }
307                     '~' => {
308                         if try!(check_block!(parser, '~', new_line)) {
309                             continue;
310                         }
311
312                         // ~ does not introduce inline code, but two of them introduce
313                         // strikethrough. Too bad for the consistency but we don't care about
314                         // strikethrough.
315                     }
316                     '[' => {
317                         // Check for a reference definition `[foo]:` at the beginning of a line
318                         let mut link = true;
319
320                         if new_line {
321                             let mut lookup_parser = parser.clone();
322                             if let Some(_) = lookup_parser.find(|&(_, c)| c == ']') {
323                                 if let Some((_, ':')) = lookup_parser.next() {
324                                     lookup_parser.next_line();
325                                     parser = lookup_parser;
326                                     link = false;
327                                 }
328                             }
329                         }
330
331                         parser.advance_begin();
332                         parser.link = link;
333                     }
334                     ']' if parser.link => {
335                         parser.link = false;
336
337                         match parser.peek() {
338                             Some('(') => {
339                                 try!(parser.jump_to(')'));
340                             }
341                             Some('[') => {
342                                 try!(parser.jump_to(']'));
343                             }
344                             Some(_) => continue,
345                             None => return Err(()),
346                         }
347                     }
348                     c if !is_path_char(c) => {
349                         parser.advance_begin();
350                     }
351                     _ => {
352                         if let Some((_, c)) = parser.find(|&(_, c)| !is_path_char(c)) {
353                             parser.put_back(c);
354                         }
355
356                         let (word, span) = parser.word();
357                         check_word(cx, valid_idents, word, span);
358                         parser.advance_begin();
359                     }
360                 }
361
362             }
363             None => break,
364         }
365     }
366
367     Ok(())
368 }
369
370 fn check_word(cx: &EarlyContext, valid_idents: &[String], word: &str, span: Span) {
371     /// Checks if a string a camel-case, ie. contains at least two uppercase letter (`Clippy` is
372     /// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
373     fn is_camel_case(s: &str) -> bool {
374         if s.starts_with(|c: char| c.is_digit(10)) {
375             return false;
376         }
377
378         let s = if s.ends_with('s') {
379             &s[..s.len() - 1]
380         } else {
381             s
382         };
383
384         s.chars().all(char::is_alphanumeric) &&
385         s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
386         s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
387     }
388
389     fn has_underscore(s: &str) -> bool {
390         s != "_" && !s.contains("\\_") && s.contains('_')
391     }
392
393     // Trim punctuation as in `some comment (see foo::bar).`
394     //                                                   ^^
395     // Or even as in `_foo bar_` which is emphasized.
396     let word = word.trim_matches(|c: char| !c.is_alphanumeric());
397
398     if valid_idents.iter().any(|i| i == word) {
399         return;
400     }
401
402     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
403         span_lint(cx,
404                   DOC_MARKDOWN,
405                   span,
406                   &format!("you should put `{}` between ticks in the documentation", word));
407     }
408 }