]> git.lizzy.rs Git - rust.git/blob - clippy_lints/src/doc.rs
Rewrite `doc_markdown` to use `pulldown-cmark`
[rust.git] / clippy_lints / src / doc.rs
1 use itertools::Itertools;
2 use pulldown_cmark;
3 use rustc::lint::*;
4 use syntax::ast;
5 use syntax::codemap::{Span, BytePos};
6 use utils::span_lint;
7
8 /// **What it does:** Checks for the presence of `_`, `::` or camel-case words
9 /// outside ticks in documentation.
10 ///
11 /// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and
12 /// camel-case probably indicates some code which should be included between
13 /// ticks. `_` can also be used for empasis in markdown, this lint tries to
14 /// consider that.
15 ///
16 /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks
17 /// for is limited, and there are still false positives.
18 ///
19 /// **Examples:**
20 /// ```rust
21 /// /// Do something with the foo_bar parameter. See also that::other::module::foo.
22 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
23 /// fn doit(foo_bar) { .. }
24 /// ```
25 declare_lint! {
26     pub DOC_MARKDOWN,
27     Warn,
28     "presence of `_`, `::` or camel-case outside backticks in documentation"
29 }
30
31 #[derive(Clone)]
32 pub struct Doc {
33     valid_idents: Vec<String>,
34 }
35
36 impl Doc {
37     pub fn new(valid_idents: Vec<String>) -> Self {
38         Doc { valid_idents: valid_idents }
39     }
40 }
41
42 impl LintPass for Doc {
43     fn get_lints(&self) -> LintArray {
44         lint_array![DOC_MARKDOWN]
45     }
46 }
47
48 impl EarlyLintPass for Doc {
49     fn check_crate(&mut self, cx: &EarlyContext, krate: &ast::Crate) {
50         check_attrs(cx, &self.valid_idents, &krate.attrs);
51     }
52
53     fn check_item(&mut self, cx: &EarlyContext, item: &ast::Item) {
54         check_attrs(cx, &self.valid_idents, &item.attrs);
55     }
56 }
57
58 struct Parser<'a> {
59     parser: pulldown_cmark::Parser<'a>,
60 }
61
62 impl<'a> Parser<'a> {
63     fn new(parser: pulldown_cmark::Parser<'a>) -> Parser<'a> {
64         Self { parser }
65     }
66 }
67
68 impl<'a> Iterator for Parser<'a> {
69     type Item = (usize, pulldown_cmark::Event<'a>);
70
71     fn next(&mut self) -> Option<Self::Item> {
72         let offset = self.parser.get_offset();
73         self.parser.next().map(|event| (offset, event))
74     }
75 }
76
77 /// Cleanup documentation decoration (`///` and such).
78 ///
79 /// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
80 /// `syntax::parse::lexer::comments::strip_doc_comment_decoration` because we need to keep track of
81 /// the spans but this function is inspired from the later.
82 #[allow(cast_possible_truncation)]
83 pub fn strip_doc_comment_decoration(comment: String, span: Span) -> (String, Vec<(usize, Span)>) {
84     // one-line comments lose their prefix
85     const ONELINERS: &'static [&'static str] = &["///!", "///", "//!", "//"];
86     for prefix in ONELINERS {
87         if comment.starts_with(*prefix) {
88             let doc = &comment[prefix.len()..];
89             let mut doc = doc.to_owned();
90             doc.push('\n');
91             return (
92                 doc.to_owned(),
93                 vec![(doc.len(), Span { lo: span.lo + BytePos(prefix.len() as u32), ..span })]
94             );
95         }
96     }
97
98     if comment.starts_with("/*") {
99         let doc = &comment[3..comment.len() - 2];
100         let mut sizes = vec![];
101
102         for line in doc.lines() {
103             let offset = line.as_ptr() as usize - comment.as_ptr() as usize;
104             debug_assert_eq!(offset as u32 as usize, offset);
105
106             sizes.push((line.len(), Span { lo: span.lo + BytePos(offset as u32), ..span }));
107         }
108
109         return (doc.to_string(), sizes);
110     }
111
112     panic!("not a doc-comment: {}", comment);
113 }
114
115 pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
116     let mut doc = String::new();
117     let mut spans = vec![];
118
119     for attr in attrs {
120         if attr.is_sugared_doc {
121             if let Some(ref current) = attr.value_str() {
122                 let current = current.to_string();
123                 let (current, current_spans) = strip_doc_comment_decoration(current, attr.span);
124                 spans.extend_from_slice(&current_spans);
125                 doc.push_str(&current);
126             }
127         }
128     }
129
130     let mut current = 0;
131     for &mut (ref mut offset, _) in &mut spans {
132         let offset_copy = *offset;
133         *offset = current;
134         current += offset_copy;
135     }
136
137     println!("{:?}", spans);
138     if !doc.is_empty() {
139         let parser = Parser::new(pulldown_cmark::Parser::new(&doc));
140         let parser = parser.coalesce(|x, y| {
141             use pulldown_cmark::Event::*;
142
143             let x_offset = x.0;
144             let y_offset = y.0;
145
146             match (x.1, y.1) {
147                 (Text(x), Text(y)) => Ok((x_offset, Text((x.into_owned() + &y).into()))),
148                 (x, y) => Err(((x_offset, x), (y_offset, y))),
149             }
150         });
151         check_doc(cx, valid_idents, parser, &spans);
152     }
153 }
154
155 fn check_doc<'a, Events: Iterator<Item=(usize, pulldown_cmark::Event<'a>)>>(
156     cx: &EarlyContext,
157     valid_idents: &[String],
158     docs: Events,
159     spans: &[(usize, Span)]
160 ) {
161     use pulldown_cmark::Event::*;
162     use pulldown_cmark::Tag::*;
163
164     let mut in_code = false;
165
166     for (offset, event) in docs {
167         println!("{:?}, {:?}", offset, event);
168         match event {
169             Start(CodeBlock(_)) | Start(Code) => in_code = true,
170             End(CodeBlock(_)) | End(Code) => in_code = false,
171             Start(_tag) | End(_tag) => (), // We don't care about other tags
172             Html(_html) | InlineHtml(_html) => (), // HTML is weird, just ignore it
173             FootnoteReference(footnote) => (), // TODO
174             SoftBreak => (),
175             HardBreak => (),
176             Text(text) => {
177                 if !in_code {
178                     let index = match spans.binary_search_by(|c| c.0.cmp(&offset)) {
179                         Ok(o) => o,
180                         Err(e) => e-1,
181                     };
182
183                     let (_, span) = spans[index];
184                     check_text(cx, valid_idents, &text, span);
185                 }
186             },
187         }
188     }
189 }
190
191 fn check_text(cx: &EarlyContext, valid_idents: &[String], text: &str, span: Span) {
192     for word in text.split_whitespace() {
193         // Trim punctuation as in `some comment (see foo::bar).`
194         //                                                   ^^
195         // Or even as in `_foo bar_` which is emphasized.
196         let word = word.trim_matches(|c: char| !c.is_alphanumeric());
197
198         if valid_idents.iter().any(|i| i == word) {
199             continue;
200         }
201
202         check_word(cx, word, span);
203     }
204 }
205
206 fn check_word(cx: &EarlyContext, word: &str, span: Span) {
207     /// Checks if a string is camel-case, ie. contains at least two uppercase letter (`Clippy` is
208     /// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
209     fn is_camel_case(s: &str) -> bool {
210         if s.starts_with(|c: char| c.is_digit(10)) {
211             return false;
212         }
213
214         let s = if s.ends_with('s') {
215             &s[..s.len() - 1]
216         } else {
217             s
218         };
219
220         s.chars().all(char::is_alphanumeric) && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
221         s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
222     }
223
224     fn has_underscore(s: &str) -> bool {
225         s != "_" && !s.contains("\\_") && s.contains('_')
226     }
227
228     if has_underscore(word) || word.contains("::") || is_camel_case(word) {
229         span_lint(cx,
230                   DOC_MARKDOWN,
231                   span,
232                   &format!("you should put `{}` between ticks in the documentation", word));
233     }
234 }