]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/doc.rs
split clippy into lints, plugin and cargo-clippy
[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 in_multiline = false;
55     for attr in attrs {
56         if attr.node.is_sugared_doc {
57             if let ast::MetaItemKind::NameValue(_, ref doc) = attr.node.value.node {
58                 if let ast::LitKind::Str(ref doc, _) = doc.node {
59                     // doc comments start with `///` or `//!`
60                     let real_doc = &doc[3..];
61                     let mut span = attr.span;
62                     span.lo = span.lo + BytePos(3);
63
64                     // check for multiline code blocks
65                     if real_doc.trim_left().starts_with("```") {
66                         in_multiline = !in_multiline;
67                     }
68                     if !in_multiline {
69                         check_doc(cx, valid_idents, real_doc, span);
70                     }
71                 }
72             }
73         }
74     }
75 }
76
77 macro_rules! jump_to {
78     // Get the next character’s first byte UTF-8 friendlyly.
79     (@next_char, $chars: expr, $len: expr) => {{
80         if let Some(&(pos, _)) = $chars.peek() {
81             pos
82         } else {
83             $len
84         }
85     }};
86
87     // Jump to the next `$c`. If no such character is found, give up.
88     ($chars: expr, $c: expr, $len: expr) => {{
89         if $chars.find(|&(_, c)| c == $c).is_some() {
90             jump_to!(@next_char, $chars, $len)
91         }
92         else {
93             return;
94         }
95     }};
96 }
97
98 #[allow(while_let_loop)] // #362
99 pub fn check_doc(cx: &EarlyContext, valid_idents: &[String], doc: &str, span: Span) {
100     // In markdown, `_` can be used to emphasize something, or, is a raw `_` depending on context.
101     // There really is no markdown specification that would disambiguate this properly. This is
102     // what GitHub and Rustdoc do:
103     //
104     // foo_bar test_quz    → foo_bar test_quz
105     // foo_bar_baz         → foo_bar_baz (note that the “official” spec says this should be emphasized)
106     // _foo bar_ test_quz_ → <em>foo bar</em> test_quz_
107     // \_foo bar\_         → _foo bar_
108     // (_baz_)             → (<em>baz</em>)
109     // foo _ bar _ baz     → foo _ bar _ baz
110
111     /// Character that can appear in a word
112     fn is_word_char(c: char) -> bool {
113         match c {
114             t if t.is_alphanumeric() => true,
115             ':' | '_' => true,
116             _ => false,
117         }
118     }
119
120     #[allow(cast_possible_truncation)]
121     fn word_span(mut span: Span, begin: usize, end: usize) -> Span {
122         debug_assert_eq!(end as u32 as usize, end);
123         debug_assert_eq!(begin as u32 as usize, begin);
124         span.hi = span.lo + BytePos(end as u32);
125         span.lo = span.lo + BytePos(begin as u32);
126         span
127     }
128
129     let mut new_line = true;
130     let len = doc.len();
131     let mut chars = doc.char_indices().peekable();
132     let mut current_word_begin = 0;
133     loop {
134         match chars.next() {
135             Some((_, c)) => {
136                 match c {
137                     '#' if new_line => { // don’t warn on titles
138                         current_word_begin = jump_to!(chars, '\n', len);
139                     }
140                     '`' => {
141                         current_word_begin = jump_to!(chars, '`', len);
142                     }
143                     '[' => {
144                         let end = jump_to!(chars, ']', len);
145                         let link_text = &doc[current_word_begin + 1..end];
146                         let word_span = word_span(span, current_word_begin + 1, end + 1);
147
148                         match chars.peek() {
149                             Some(&(_, c)) => {
150                                 // Trying to parse a link. Let’s ignore the link.
151
152                                 // FIXME: how does markdown handles such link?
153                                 // https://en.wikipedia.org/w/index.php?title=)
154                                 match c {
155                                     '(' => { // inline link
156                                         current_word_begin = jump_to!(chars, ')', len);
157                                         check_doc(cx, valid_idents, link_text, word_span);
158                                     }
159                                     '[' => { // reference link
160                                         current_word_begin = jump_to!(chars, ']', len);
161                                         check_doc(cx, valid_idents, link_text, word_span);
162                                     }
163                                     ':' => { // reference link
164                                         current_word_begin = jump_to!(chars, '\n', len);
165                                     }
166                                     _ => { // automatic reference link
167                                         current_word_begin = jump_to!(@next_char, chars, len);
168                                         check_doc(cx, valid_idents, link_text, word_span);
169                                     }
170                                 }
171                             }
172                             None => return,
173                         }
174                     }
175                     // anything that’s neither alphanumeric nor '_' is not part of an ident anyway
176                     c if !c.is_alphanumeric() && c != '_' => {
177                         current_word_begin = jump_to!(@next_char, chars, len);
178                     }
179                     _ => {
180                         let end = match chars.find(|&(_, c)| !is_word_char(c)) {
181                             Some((end, _)) => end,
182                             None => len,
183                         };
184                         let word_span = word_span(span, current_word_begin, end);
185                         check_word(cx, valid_idents, &doc[current_word_begin..end], word_span);
186                         current_word_begin = jump_to!(@next_char, chars, len);
187                     }
188                 }
189
190                 new_line = c == '\n' || (new_line && c.is_whitespace());
191             }
192             None => break,
193         }
194     }
195 }
196
197 fn check_word(cx: &EarlyContext, valid_idents: &[String], word: &str, span: Span) {
198     /// Checks if a string a camel-case, ie. contains at least two uppercase letter (`Clippy` is
199     /// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
200     fn is_camel_case(s: &str) -> bool {
201         if s.starts_with(|c: char| c.is_digit(10)) {
202             return false;
203         }
204
205         let s = if s.ends_with('s') {
206             &s[..s.len() - 1]
207         } else {
208             s
209         };
210
211         s.chars().all(char::is_alphanumeric) &&
212         s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
213         s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
214     }
215
216     fn has_underscore(s: &str) -> bool {
217         s != "_" && !s.contains("\\_") && s.contains('_')
218     }
219
220     // Trim punctuation as in `some comment (see foo::bar).`
221     //                                                   ^^
222     // Or even as in `_foo bar_` which is emphasized.
223     let word = word.trim_matches(|c: char| !c.is_alphanumeric());
224
225     if valid_idents.iter().any(|i| i == word) {
226         return;
227     }
228
229     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
230         span_lint(cx,
231                   DOC_MARKDOWN,
232                   span,
233                   &format!("you should put `{}` between ticks in the documentation", word));
234     }
235 }