]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/doc.rs
fd4640c21584e399c3d99b38e64afa708a977772
[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 pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
54     let mut docs = vec![];
55
56     let mut in_multiline = false;
57     for attr in attrs {
58         if attr.node.is_sugared_doc {
59             if let ast::MetaItemKind::NameValue(_, ref doc) = attr.node.value.node {
60                 if let ast::LitKind::Str(ref doc, _) = doc.node {
61                     // doc comments start with `///` or `//!`
62                     let real_doc = &doc[3..];
63                     let mut span = attr.span;
64                     span.lo = span.lo + BytePos(3);
65
66                     // check for multiline code blocks
67                     if real_doc.trim_left().starts_with("```") {
68                         in_multiline = !in_multiline;
69                     }
70                     if !in_multiline {
71                         docs.push((real_doc, span));
72                     }
73                 }
74             }
75         }
76     }
77
78     for (doc, span) in docs {
79         let _ = check_doc(cx, valid_idents, doc, span);
80     }
81 }
82
83 #[allow(while_let_loop)] // #362
84 pub fn check_doc(cx: &EarlyContext, valid_idents: &[String], doc: &str, span: Span) -> Result<(), ()> {
85     // In markdown, `_` can be used to emphasize something, or, is a raw `_` depending on context.
86     // There really is no markdown specification that would disambiguate this properly. This is
87     // what GitHub and Rustdoc do:
88     //
89     // foo_bar test_quz    → foo_bar test_quz
90     // foo_bar_baz         → foo_bar_baz (note that the “official” spec says this should be emphasized)
91     // _foo bar_ test_quz_ → <em>foo bar</em> test_quz_
92     // \_foo bar\_         → _foo bar_
93     // (_baz_)             → (<em>baz</em>)
94     // foo _ bar _ baz     → foo _ bar _ baz
95
96     /// Character that can appear in a path
97     fn is_path_char(c: char) -> bool {
98         match c {
99             t if t.is_alphanumeric() => true,
100             ':' | '_' => true,
101             _ => false,
102         }
103     }
104
105     #[derive(Clone, Debug)]
106     struct Parser<'a> {
107         link: bool,
108         line: &'a str,
109         span: Span,
110         current_word_begin: usize,
111         new_line: bool,
112         pos: usize,
113     }
114
115     impl<'a> Parser<'a> {
116         fn advance_begin(&mut self) {
117             self.current_word_begin = self.pos;
118         }
119
120         fn peek(&self) -> Option<char> {
121             self.line[self.pos..].chars().next()
122         }
123
124         fn jump_to(&mut self, n: char) -> Result<(), ()> {
125             while let Some(c) = self.next() {
126                 if c == n {
127                     self.advance_begin();
128                     return Ok(());
129                 }
130             }
131
132             return Err(());
133         }
134
135         fn put_back(&mut self, c: char) {
136             self.pos -= c.len_utf8();
137         }
138
139         #[allow(cast_possible_truncation)]
140         fn word(&self) -> (&'a str, Span) {
141             let begin = self.current_word_begin;
142             let end = self.pos;
143
144             debug_assert_eq!(end as u32 as usize, end);
145             debug_assert_eq!(begin as u32 as usize, begin);
146
147             let mut span = self.span;
148             span.hi = span.lo + BytePos(end as u32);
149             span.lo = span.lo + BytePos(begin as u32);
150
151             (&self.line[begin..end], span)
152         }
153     }
154
155     impl<'a> Iterator for Parser<'a> {
156         type Item = char;
157
158         fn next(&mut self) -> Option<char> {
159             let mut chars = self.line[self.pos..].chars();
160             let c = chars.next();
161
162             if let Some(c) = c {
163                 self.pos += c.len_utf8();
164             } else {
165                 // TODO: new line
166             }
167
168             c
169         }
170     }
171
172     let mut parser = Parser {
173         link: false,
174         line: doc,
175         span: span,
176         current_word_begin: 0,
177         new_line: true,
178         pos: 0,
179     };
180
181     loop {
182         match parser.next() {
183             Some(c) => {
184                 match c {
185                     '#' if new_line => { // don’t warn on titles
186                         try!(parser.jump_to('\n'));
187                     }
188                     '`' => {
189                         try!(parser.jump_to('`'));
190                     }
191                     '[' => {
192                         // Check for a reference definition `[foo]:` at the beginning of a line
193                         let mut link = true;
194                         if parser.new_line {
195                             let mut lookup_parser = parser.clone();
196                             if let Some(_) = lookup_parser.find(|&c| c == ']') {
197                                 if let Some(':') = lookup_parser.next() {
198                                     try!(lookup_parser.jump_to(')'));
199                                     parser = lookup_parser;
200                                     link = false;
201                                 }
202                             }
203                         }
204
205                         parser.advance_begin();
206                         parser.link = link;
207                     }
208                     ']' if parser.link => {
209                         parser.link = false;
210
211                         match parser.peek() {
212                             Some('(') => try!(parser.jump_to(')')),
213                             Some('[') => try!(parser.jump_to(']')),
214                             Some(_) => continue,
215                             None => return Err(()),
216                         }
217                     }
218                     c if !is_path_char(c) => {
219                         parser.advance_begin();
220                     }
221                     _ => {
222                         if let Some(c) = parser.find(|&c| !is_path_char(c)) {
223                             parser.put_back(c);
224                         }
225
226                         let (word, span) = parser.word();
227                         check_word(cx, valid_idents, word, span);
228                         parser.advance_begin();
229                     }
230                 }
231
232                 parser.new_line = c == '\n' || (parser.new_line && c.is_whitespace());
233             }
234             None => break,
235         }
236     }
237
238     Ok(())
239 }
240
241 fn check_word(cx: &EarlyContext, valid_idents: &[String], word: &str, span: Span) {
242     /// Checks if a string a camel-case, ie. contains at least two uppercase letter (`Clippy` is
243     /// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
244     fn is_camel_case(s: &str) -> bool {
245         if s.starts_with(|c: char| c.is_digit(10)) {
246             return false;
247         }
248
249         let s = if s.ends_with('s') {
250             &s[..s.len() - 1]
251         } else {
252             s
253         };
254
255         s.chars().all(char::is_alphanumeric) &&
256         s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
257         s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
258     }
259
260     fn has_underscore(s: &str) -> bool {
261         s != "_" && !s.contains("\\_") && s.contains('_')
262     }
263
264     // Trim punctuation as in `some comment (see foo::bar).`
265     //                                                   ^^
266     // Or even as in `_foo bar_` which is emphasized.
267     let word = word.trim_matches(|c: char| !c.is_alphanumeric());
268
269     if valid_idents.iter().any(|i| i == word) {
270         return;
271     }
272
273     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
274         span_lint(cx,
275                   DOC_MARKDOWN,
276                   span,
277                   &format!("you should put `{}` between ticks in the documentation", word));
278     }
279 }