3 use syntax::codemap::{Span, BytePos};
6 /// **What it does:** This lint checks for the presence of `_`, `::` or camel-case words outside
7 /// ticks in documentation.
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.
13 /// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks for is limited.
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) { .. }
22 pub DOC_MARKDOWN, Warn,
23 "checks for the presence of `_`, `::` or camel-case outside ticks in documentation"
28 valid_idents: Vec<String>,
32 pub fn new(valid_idents: Vec<String>) -> Self {
33 Doc { valid_idents: valid_idents }
37 impl LintPass for Doc {
38 fn get_lints(&self) -> LintArray {
39 lint_array![DOC_MARKDOWN]
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);
48 fn check_item(&mut self, cx: &EarlyContext, item: &ast::Item) {
49 check_attrs(cx, &self.valid_idents, &item.attrs);
53 pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
54 let mut docs = vec![];
56 let mut in_multiline = false;
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);
66 // check for multiline code blocks
67 if real_doc.trim_left().starts_with("```") {
68 in_multiline = !in_multiline;
71 docs.push((real_doc, span));
78 for (doc, span) in docs {
79 let _ = check_doc(cx, valid_idents, doc, span);
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:
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
96 /// Character that can appear in a path
97 fn is_path_char(c: char) -> bool {
99 t if t.is_alphanumeric() => true,
105 #[derive(Clone, Debug)]
110 current_word_begin: usize,
115 impl<'a> Parser<'a> {
116 fn advance_begin(&mut self) {
117 self.current_word_begin = self.pos;
120 fn peek(&self) -> Option<char> {
121 self.line[self.pos..].chars().next()
124 fn jump_to(&mut self, n: char) -> Result<(), ()> {
125 while let Some(c) = self.next() {
127 self.advance_begin();
135 fn put_back(&mut self, c: char) {
136 self.pos -= c.len_utf8();
139 #[allow(cast_possible_truncation)]
140 fn word(&self) -> (&'a str, Span) {
141 let begin = self.current_word_begin;
144 debug_assert_eq!(end as u32 as usize, end);
145 debug_assert_eq!(begin as u32 as usize, begin);
147 let mut span = self.span;
148 span.hi = span.lo + BytePos(end as u32);
149 span.lo = span.lo + BytePos(begin as u32);
151 (&self.line[begin..end], span)
155 impl<'a> Iterator for Parser<'a> {
158 fn next(&mut self) -> Option<char> {
159 let mut chars = self.line[self.pos..].chars();
160 let c = chars.next();
163 self.pos += c.len_utf8();
172 let mut parser = Parser {
176 current_word_begin: 0,
182 match parser.next() {
185 '#' if new_line => { // don’t warn on titles
186 try!(parser.jump_to('\n'));
189 try!(parser.jump_to('`'));
192 // Check for a reference definition `[foo]:` at the beginning of a 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;
205 parser.advance_begin();
208 ']' if parser.link => {
211 match parser.peek() {
212 Some('(') => try!(parser.jump_to(')')),
213 Some('[') => try!(parser.jump_to(']')),
215 None => return Err(()),
218 c if !is_path_char(c) => {
219 parser.advance_begin();
222 if let Some(c) = parser.find(|&c| !is_path_char(c)) {
226 let (word, span) = parser.word();
227 check_word(cx, valid_idents, word, span);
228 parser.advance_begin();
232 parser.new_line = c == '\n' || (parser.new_line && c.is_whitespace());
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)) {
249 let s = if s.ends_with('s') {
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
260 fn has_underscore(s: &str) -> bool {
261 s != "_" && !s.contains("\\_") && s.contains('_')
264 // Trim punctuation as in `some comment (see foo::bar).`
266 // Or even as in `_foo bar_` which is emphasized.
267 let word = word.trim_matches(|c: char| !c.is_alphanumeric());
269 if valid_idents.iter().any(|i| i == word) {
273 if has_underscore(word) || word.contains("::") || is_camel_case(word) {
277 &format!("you should put `{}` between ticks in the documentation", word));