3 use syntax::codemap::{Span, BytePos};
6 /// **What it does:** Checks for the presence of `_`, `::` or camel-case words
7 /// outside ticks in documentation.
9 /// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and
10 /// camel-case probably indicates some code which should be included between
11 /// ticks. `_` can also be used for empasis in markdown, this lint tries to
14 /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks
15 /// for is limited, and there are still false positives.
19 /// /// Do something with the foo_bar parameter. See also that::other::module::foo.
20 /// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
21 /// fn doit(foo_bar) { .. }
26 "presence of `_`, `::` or camel-case outside backticks in documentation"
31 valid_idents: Vec<String>,
35 pub fn new(valid_idents: Vec<String>) -> Self {
36 Doc { valid_idents: valid_idents }
40 impl LintPass for Doc {
41 fn get_lints(&self) -> LintArray {
42 lint_array![DOC_MARKDOWN]
46 impl EarlyLintPass for Doc {
47 fn check_crate(&mut self, cx: &EarlyContext, krate: &ast::Crate) {
48 check_attrs(cx, &self.valid_idents, &krate.attrs);
51 fn check_item(&mut self, cx: &EarlyContext, item: &ast::Item) {
52 check_attrs(cx, &self.valid_idents, &item.attrs);
56 /// Cleanup documentation decoration (`///` and such).
58 /// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
59 /// `syntax::parse::lexer::comments::strip_doc_comment_decoration` because we need to keep track of
60 /// the span but this function is inspired from the later.
61 #[allow(cast_possible_truncation)]
62 pub fn strip_doc_comment_decoration((comment, span): (String, Span)) -> Vec<(String, Span)> {
63 // one-line comments lose their prefix
64 const ONELINERS: &'static [&'static str] = &["///!", "///", "//!", "//"];
65 for prefix in ONELINERS {
66 if comment.starts_with(*prefix) {
67 return vec![(comment[prefix.len()..].to_owned(),
68 Span { lo: span.lo + BytePos(prefix.len() as u32), ..span })];
72 if comment.starts_with("/*") {
73 return comment[3..comment.len() - 2]
76 let offset = line.as_ptr() as usize - comment.as_ptr() as usize;
77 debug_assert_eq!(offset as u32 as usize, offset);
79 (line.to_owned(), Span { lo: span.lo + BytePos(offset as u32), ..span })
84 panic!("not a doc-comment: {}", comment);
87 pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
88 let mut docs = vec![];
91 if attr.is_sugared_doc {
92 if let ast::MetaItemKind::NameValue(ref doc) = attr.value.node {
93 if let ast::LitKind::Str(ref doc, _) = doc.node {
94 let doc = (*doc.as_str()).to_owned();
95 docs.extend_from_slice(&strip_doc_comment_decoration((doc, attr.span)));
101 if !docs.is_empty() {
102 let _ = check_doc(cx, valid_idents, &docs);
106 #[allow(while_let_loop)] // #362
107 fn check_doc(cx: &EarlyContext, valid_idents: &[String], docs: &[(String, Span)]) -> Result<(), ()> {
108 // In markdown, `_` can be used to emphasize something, or, is a raw `_` depending on context.
109 // There really is no markdown specification that would disambiguate this properly. This is
110 // what GitHub and Rustdoc do:
112 // foo_bar test_quz → foo_bar test_quz
113 // foo_bar_baz → foo_bar_baz (note that the “official” spec says this should be emphasized)
114 // _foo bar_ test_quz_ → <em>foo bar</em> test_quz_
115 // \_foo bar\_ → _foo bar_
116 // (_baz_) → (<em>baz</em>)
117 // foo _ bar _ baz → foo _ bar _ baz
119 /// Character that can appear in a path
120 fn is_path_char(c: char) -> bool {
122 t if t.is_alphanumeric() => true,
128 #[derive(Clone, Debug)]
129 /// This type is used to iterate through the documentation characters, keeping the span at the
132 /// First byte of the current potential match
133 current_word_begin: usize,
134 /// List of lines and their associated span
135 docs: &'a [(String, Span)],
136 /// Index of the current line we are parsing
138 /// Whether we are in a link
140 /// Whether we are at the beginning of a line
142 /// Whether we were to the end of a line last time `next` was called
144 /// The position of the current character within the current line
148 impl<'a> Parser<'a> {
149 fn advance_begin(&mut self) {
150 self.current_word_begin = self.pos;
153 fn line(&self) -> (&'a str, Span) {
154 let (ref doc, span) = self.docs[self.line];
158 fn peek(&self) -> Option<char> {
159 self.line().0[self.pos..].chars().next()
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() {
166 self.advance_begin();
174 fn next_line(&mut self) {
176 self.current_word_begin = 0;
178 self.new_line = true;
181 fn put_back(&mut self, c: char) {
182 self.pos -= c.len_utf8();
185 #[allow(cast_possible_truncation)]
186 fn word(&self) -> (&'a str, Span) {
187 let begin = self.current_word_begin;
190 debug_assert_eq!(end as u32 as usize, end);
191 debug_assert_eq!(begin as u32 as usize, begin);
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);
197 (&doc[begin..end], span)
201 impl<'a> Iterator for Parser<'a> {
202 type Item = (bool, char);
204 fn next(&mut self) -> Option<(bool, char)> {
205 while self.line < self.docs.len() {
210 self.current_word_begin = 0;
213 let mut chars = self.line().0[self.pos..].chars();
214 let c = chars.next();
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 {
224 self.new_line = true;
227 return Some((true, '\n'));
235 let mut parser = Parser {
236 current_word_begin: 0,
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)
251 ($parser:expr, $c:pat, $c_expr:expr, $new_line:expr) => {{
252 fn check_block(parser: &mut Parser, new_line: bool) -> Result<bool, ()> {
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() {
265 if try!(parser.jump_to($c_expr)) {
270 lookup_parser = parser.clone();
271 let a = lookup_parser.next();
272 let b = lookup_parser.next();
273 if let (Some((false, $c)), Some((false, $c))) = (a, b) {
274 let mut close_count = 3;
275 while let Some((false, $c)) = lookup_parser.next() {
279 if close_count == open_count {
280 *parser = lookup_parser;
291 check_block(&mut $parser, $new_line)
296 match parser.next() {
297 Some((new_line, c)) => {
300 // don’t warn on titles
304 if try!(check_block!(parser, '`', new_line)) {
308 try!(parser.jump_to('`')); // not a code block, just inline code
311 if try!(check_block!(parser, '~', new_line)) {
315 // ~ does not introduce inline code, but two of them introduce
316 // strikethrough. Too bad for the consistency but we don't care about
320 // Check for a reference definition `[foo]:` at the beginning of a line
324 let mut lookup_parser = parser.clone();
325 if lookup_parser.any(|(_, c)| c == ']') {
326 if let Some((_, ':')) = lookup_parser.next() {
327 lookup_parser.next_line();
328 parser = lookup_parser;
334 parser.advance_begin();
337 ']' if parser.link => {
340 match parser.peek() {
342 try!(parser.jump_to(')'));
345 try!(parser.jump_to(']'));
348 None => return Err(()),
351 c if !is_path_char(c) => {
352 parser.advance_begin();
355 if let Some((_, c)) = parser.find(|&(_, c)| !is_path_char(c)) {
359 let (word, span) = parser.word();
360 check_word(cx, valid_idents, word, span);
361 parser.advance_begin();
373 fn check_word(cx: &EarlyContext, valid_idents: &[String], word: &str, span: Span) {
374 /// Checks if a string a camel-case, ie. contains at least two uppercase letter (`Clippy` is
375 /// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
376 fn is_camel_case(s: &str) -> bool {
377 if s.starts_with(|c: char| c.is_digit(10)) {
381 let s = if s.ends_with('s') {
387 s.chars().all(char::is_alphanumeric) && s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
388 s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
391 fn has_underscore(s: &str) -> bool {
392 s != "_" && !s.contains("\\_") && s.contains('_')
395 // Trim punctuation as in `some comment (see foo::bar).`
397 // Or even as in `_foo bar_` which is emphasized.
398 let word = word.trim_matches(|c: char| !c.is_alphanumeric());
400 if valid_idents.iter().any(|i| i == word) {
404 if has_underscore(word) || word.contains("::") || is_camel_case(word) {
408 &format!("you should put `{}` between ticks in the documentation", word));