2016-03-19 16:59:12 +00:00
|
|
|
|
use rustc::lint::*;
|
|
|
|
|
use syntax::ast;
|
2016-05-02 12:36:33 +00:00
|
|
|
|
use syntax::codemap::{Span, BytePos};
|
2016-03-19 16:59:12 +00:00
|
|
|
|
use utils::span_lint;
|
|
|
|
|
|
2016-03-28 16:00:24 +00:00
|
|
|
|
/// **What it does:** This lint checks for the presence of `_`, `::` or camel-case words outside
|
|
|
|
|
/// ticks in documentation.
|
2016-03-19 16:59:12 +00:00
|
|
|
|
///
|
2016-03-28 16:00:24 +00:00
|
|
|
|
/// **Why is this bad?** *Rustdoc* supports markdown formatting, `_`, `::` and camel-case probably
|
2016-03-28 19:23:21 +00:00
|
|
|
|
/// indicates some code which should be included between ticks. `_` can also be used for empasis in
|
|
|
|
|
/// markdown, this lint tries to consider that.
|
2016-03-19 16:59:12 +00:00
|
|
|
|
///
|
2016-03-28 16:00:24 +00:00
|
|
|
|
/// **Known problems:** Lots of bad docs won’t be fixed, what the lint checks for is limited.
|
2016-03-19 16:59:12 +00:00
|
|
|
|
///
|
|
|
|
|
/// **Examples:**
|
|
|
|
|
/// ```rust
|
2016-03-28 16:00:24 +00:00
|
|
|
|
/// /// Do something with the foo_bar parameter. See also that::other::module::foo.
|
|
|
|
|
/// // ^ `foo_bar` and `that::other::module::foo` should be ticked.
|
2016-03-19 16:59:12 +00:00
|
|
|
|
/// fn doit(foo_bar) { .. }
|
|
|
|
|
/// ```
|
|
|
|
|
declare_lint! {
|
|
|
|
|
pub DOC_MARKDOWN, Warn,
|
2016-03-28 16:00:24 +00:00
|
|
|
|
"checks for the presence of `_`, `::` or camel-case outside ticks in documentation"
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
2016-04-04 18:18:17 +00:00
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct Doc {
|
|
|
|
|
valid_idents: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Doc {
|
|
|
|
|
pub fn new(valid_idents: Vec<String>) -> Self {
|
|
|
|
|
Doc { valid_idents: valid_idents }
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-03-19 16:59:12 +00:00
|
|
|
|
|
|
|
|
|
impl LintPass for Doc {
|
|
|
|
|
fn get_lints(&self) -> LintArray {
|
|
|
|
|
lint_array![DOC_MARKDOWN]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl EarlyLintPass for Doc {
|
|
|
|
|
fn check_crate(&mut self, cx: &EarlyContext, krate: &ast::Crate) {
|
2016-05-02 12:36:33 +00:00
|
|
|
|
check_attrs(cx, &self.valid_idents, &krate.attrs);
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn check_item(&mut self, cx: &EarlyContext, item: &ast::Item) {
|
2016-05-02 12:36:33 +00:00
|
|
|
|
check_attrs(cx, &self.valid_idents, &item.attrs);
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-05-02 12:36:33 +00:00
|
|
|
|
pub fn check_attrs<'a>(cx: &EarlyContext, valid_idents: &[String], attrs: &'a [ast::Attribute]) {
|
|
|
|
|
let mut in_multiline = false;
|
|
|
|
|
for attr in attrs {
|
2016-03-19 16:59:12 +00:00
|
|
|
|
if attr.node.is_sugared_doc {
|
|
|
|
|
if let ast::MetaItemKind::NameValue(_, ref doc) = attr.node.value.node {
|
|
|
|
|
if let ast::LitKind::Str(ref doc, _) = doc.node {
|
2016-05-02 12:36:33 +00:00
|
|
|
|
// doc comments start with `///` or `//!`
|
|
|
|
|
let real_doc = &doc[3..];
|
|
|
|
|
let mut span = attr.span;
|
|
|
|
|
span.lo = span.lo + BytePos(3);
|
|
|
|
|
|
|
|
|
|
// check for multiline code blocks
|
|
|
|
|
if real_doc.trim_left().starts_with("```") {
|
|
|
|
|
in_multiline = !in_multiline;
|
|
|
|
|
}
|
|
|
|
|
if !in_multiline {
|
|
|
|
|
check_doc(cx, valid_idents, real_doc, span);
|
|
|
|
|
}
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-04-11 21:22:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
macro_rules! jump_to {
|
|
|
|
|
// Get the next character’s first byte UTF-8 friendlyly.
|
|
|
|
|
(@next_char, $chars: expr, $len: expr) => {{
|
|
|
|
|
if let Some(&(pos, _)) = $chars.peek() {
|
|
|
|
|
pos
|
|
|
|
|
} else {
|
|
|
|
|
$len
|
|
|
|
|
}
|
|
|
|
|
}};
|
|
|
|
|
|
|
|
|
|
// Jump to the next `$c`. If no such character is found, give up.
|
|
|
|
|
($chars: expr, $c: expr, $len: expr) => {{
|
|
|
|
|
if $chars.find(|&(_, c)| c == $c).is_some() {
|
|
|
|
|
jump_to!(@next_char, $chars, $len)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}};
|
|
|
|
|
}
|
2016-03-19 16:59:12 +00:00
|
|
|
|
|
2016-04-11 21:22:30 +00:00
|
|
|
|
#[allow(while_let_loop)] // #362
|
|
|
|
|
pub fn check_doc(cx: &EarlyContext, valid_idents: &[String], doc: &str, span: Span) {
|
2016-03-28 16:00:24 +00:00
|
|
|
|
// In markdown, `_` can be used to emphasize something, or, is a raw `_` depending on context.
|
|
|
|
|
// There really is no markdown specification that would disambiguate this properly. This is
|
|
|
|
|
// what GitHub and Rustdoc do:
|
|
|
|
|
//
|
|
|
|
|
// foo_bar test_quz → foo_bar test_quz
|
|
|
|
|
// foo_bar_baz → foo_bar_baz (note that the “official” spec says this should be emphasized)
|
|
|
|
|
// _foo bar_ test_quz_ → <em>foo bar</em> test_quz_
|
|
|
|
|
// \_foo bar\_ → _foo bar_
|
|
|
|
|
// (_baz_) → (<em>baz</em>)
|
|
|
|
|
// foo _ bar _ baz → foo _ bar _ baz
|
|
|
|
|
|
2016-04-11 21:22:30 +00:00
|
|
|
|
/// Character that can appear in a word
|
|
|
|
|
fn is_word_char(c: char) -> bool {
|
|
|
|
|
match c {
|
|
|
|
|
t if t.is_alphanumeric() => true,
|
|
|
|
|
':' | '_' => true,
|
|
|
|
|
_ => false,
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
2016-04-11 21:22:30 +00:00
|
|
|
|
}
|
2016-03-19 16:59:12 +00:00
|
|
|
|
|
2016-04-11 21:22:30 +00:00
|
|
|
|
let len = doc.len();
|
|
|
|
|
let mut chars = doc.char_indices().peekable();
|
|
|
|
|
let mut current_word_begin = 0;
|
|
|
|
|
loop {
|
|
|
|
|
match chars.next() {
|
|
|
|
|
Some((_, c)) => {
|
|
|
|
|
match c {
|
|
|
|
|
c if c.is_whitespace() => {
|
|
|
|
|
current_word_begin = jump_to!(@next_char, chars, len);
|
|
|
|
|
}
|
|
|
|
|
'`' => {
|
|
|
|
|
current_word_begin = jump_to!(chars, '`', len);
|
2016-04-14 18:14:03 +00:00
|
|
|
|
}
|
2016-04-11 21:22:30 +00:00
|
|
|
|
'[' => {
|
|
|
|
|
let end = jump_to!(chars, ']', len);
|
2016-04-14 18:14:03 +00:00
|
|
|
|
let link_text = &doc[current_word_begin + 1..end];
|
2016-04-11 21:22:30 +00:00
|
|
|
|
|
|
|
|
|
match chars.peek() {
|
|
|
|
|
Some(&(_, c)) => {
|
|
|
|
|
// Trying to parse a link. Let’s ignore the link.
|
|
|
|
|
|
|
|
|
|
// FIXME: how does markdown handles such link?
|
|
|
|
|
// https://en.wikipedia.org/w/index.php?title=)
|
|
|
|
|
match c {
|
|
|
|
|
'(' => { // inline link
|
|
|
|
|
current_word_begin = jump_to!(chars, ')', len);
|
|
|
|
|
check_doc(cx, valid_idents, link_text, span);
|
|
|
|
|
}
|
|
|
|
|
'[' => { // reference link
|
|
|
|
|
current_word_begin = jump_to!(chars, ']', len);
|
|
|
|
|
check_doc(cx, valid_idents, link_text, span);
|
|
|
|
|
}
|
|
|
|
|
':' => { // reference link
|
|
|
|
|
current_word_begin = jump_to!(chars, '\n', len);
|
|
|
|
|
}
|
2016-04-13 14:02:44 +00:00
|
|
|
|
_ => { // automatic reference link
|
|
|
|
|
current_word_begin = jump_to!(@next_char, chars, len);
|
|
|
|
|
check_doc(cx, valid_idents, link_text, span);
|
|
|
|
|
}
|
2016-04-11 21:22:30 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => return,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => {
|
|
|
|
|
let end = match chars.find(|&(_, c)| !is_word_char(c)) {
|
|
|
|
|
Some((end, _)) => end,
|
|
|
|
|
None => len,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
check_word(cx, valid_idents, &doc[current_word_begin..end], span);
|
|
|
|
|
current_word_begin = jump_to!(@next_char, chars, len);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => break,
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-04-04 18:18:17 +00:00
|
|
|
|
fn check_word(cx: &EarlyContext, valid_idents: &[String], word: &str, span: Span) {
|
2016-03-19 16:59:12 +00:00
|
|
|
|
/// Checks if a string a camel-case, ie. contains at least two uppercase letter (`Clippy` is
|
|
|
|
|
/// ok) and one lower-case letter (`NASA` is ok). Plural are also excluded (`IDs` is ok).
|
|
|
|
|
fn is_camel_case(s: &str) -> bool {
|
2016-04-04 18:18:17 +00:00
|
|
|
|
if s.starts_with(|c: char| c.is_digit(10)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-19 16:59:12 +00:00
|
|
|
|
let s = if s.ends_with('s') {
|
2016-04-14 18:14:03 +00:00
|
|
|
|
&s[..s.len() - 1]
|
2016-03-19 16:59:12 +00:00
|
|
|
|
} else {
|
|
|
|
|
s
|
|
|
|
|
};
|
|
|
|
|
|
2016-03-28 19:23:21 +00:00
|
|
|
|
s.chars().all(char::is_alphanumeric) &&
|
2016-03-19 16:59:12 +00:00
|
|
|
|
s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1 &&
|
|
|
|
|
s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-28 16:00:24 +00:00
|
|
|
|
fn has_underscore(s: &str) -> bool {
|
|
|
|
|
s != "_" && !s.contains("\\_") && s.contains('_')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Trim punctuation as in `some comment (see foo::bar).`
|
|
|
|
|
// ^^
|
2016-04-01 15:24:55 +00:00
|
|
|
|
// Or even as in `_foo bar_` which is emphasized.
|
2016-03-28 16:00:24 +00:00
|
|
|
|
let word = word.trim_matches(|c: char| !c.is_alphanumeric());
|
|
|
|
|
|
2016-04-04 18:18:17 +00:00
|
|
|
|
if valid_idents.iter().any(|i| i == word) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2016-03-28 16:00:24 +00:00
|
|
|
|
if has_underscore(word) || word.contains("::") || is_camel_case(word) {
|
2016-04-14 18:14:03 +00:00
|
|
|
|
span_lint(cx,
|
|
|
|
|
DOC_MARKDOWN,
|
|
|
|
|
span,
|
|
|
|
|
&format!("you should put `{}` between ticks in the documentation", word));
|
2016-03-19 16:59:12 +00:00
|
|
|
|
}
|
|
|
|
|
}
|