Since Clippy uses the `let_chains` feature, there are many occasions to collapse `if` and `if let` statements.
152 lines
5.2 KiB
Rust
152 lines
5.2 KiB
Rust
use clippy_utils::diagnostics::{span_lint_and_sugg, span_lint_and_then};
|
|
use clippy_utils::source::snippet_with_applicability;
|
|
use rustc_data_structures::fx::FxHashSet;
|
|
use rustc_errors::Applicability;
|
|
use rustc_lint::LateContext;
|
|
use rustc_span::{BytePos, Pos, Span};
|
|
use url::Url;
|
|
|
|
use crate::doc::DOC_MARKDOWN;
|
|
|
|
pub fn check(
|
|
cx: &LateContext<'_>,
|
|
valid_idents: &FxHashSet<String>,
|
|
text: &str,
|
|
span: Span,
|
|
code_level: isize,
|
|
blockquote_level: isize,
|
|
) {
|
|
for orig_word in text.split(|c: char| c.is_whitespace() || c == '\'') {
|
|
// Trim punctuation as in `some comment (see foo::bar).`
|
|
// ^^
|
|
// Or even as in `_foo bar_` which is emphasized. Also preserve `::` as a prefix/suffix.
|
|
let trim_pattern = |c: char| !c.is_alphanumeric() && c != ':';
|
|
let mut word = orig_word.trim_end_matches(trim_pattern);
|
|
|
|
// If word is immediately followed by `()`, claw it back.
|
|
if let Some(tmp_word) = orig_word.get(..word.len() + 2)
|
|
&& tmp_word.ends_with("()")
|
|
{
|
|
word = tmp_word;
|
|
}
|
|
|
|
let original_len = word.len();
|
|
word = word.trim_start_matches(trim_pattern);
|
|
|
|
// Remove leading or trailing single `:` which may be part of a sentence.
|
|
if word.starts_with(':') && !word.starts_with("::") {
|
|
word = word.trim_start_matches(':');
|
|
}
|
|
if word.ends_with(':') && !word.ends_with("::") {
|
|
word = word.trim_end_matches(':');
|
|
}
|
|
|
|
if valid_idents.contains(word) || word.chars().all(|c| c == ':') {
|
|
continue;
|
|
}
|
|
|
|
// Ensure that all reachable matching closing parens are included as well.
|
|
let size_diff = original_len - word.len();
|
|
let mut open_parens = 0;
|
|
let mut close_parens = 0;
|
|
for c in word.chars() {
|
|
if c == '(' {
|
|
open_parens += 1;
|
|
} else if c == ')' {
|
|
close_parens += 1;
|
|
}
|
|
}
|
|
while close_parens < open_parens
|
|
&& let Some(tmp_word) = orig_word.get(size_diff..=(word.len() + size_diff))
|
|
&& tmp_word.ends_with(')')
|
|
{
|
|
word = tmp_word;
|
|
close_parens += 1;
|
|
}
|
|
|
|
// Adjust for the current word
|
|
let offset = word.as_ptr() as usize - text.as_ptr() as usize;
|
|
let span = Span::new(
|
|
span.lo() + BytePos::from_usize(offset),
|
|
span.lo() + BytePos::from_usize(offset + word.len()),
|
|
span.ctxt(),
|
|
span.parent(),
|
|
);
|
|
|
|
check_word(cx, word, span, code_level, blockquote_level);
|
|
}
|
|
}
|
|
|
|
fn check_word(cx: &LateContext<'_>, word: &str, span: Span, code_level: isize, blockquote_level: isize) {
|
|
/// Checks if a string is upper-camel-case, i.e., starts with an uppercase and
|
|
/// contains at least two uppercase letters (`Clippy` is ok) and one lower-case
|
|
/// letter (`NASA` is ok).
|
|
/// Plurals are also excluded (`IDs` is ok).
|
|
fn is_camel_case(s: &str) -> bool {
|
|
if s.starts_with(|c: char| c.is_ascii_digit() | c.is_ascii_lowercase()) {
|
|
return false;
|
|
}
|
|
|
|
let s = if let Some(prefix) = s.strip_suffix("es")
|
|
&& prefix.chars().all(|c| c.is_ascii_uppercase())
|
|
&& matches!(prefix.chars().last(), Some('S' | 'X'))
|
|
{
|
|
prefix
|
|
} else if let Some(prefix) = s.strip_suffix("ified")
|
|
&& prefix.chars().all(|c| c.is_ascii_uppercase())
|
|
{
|
|
prefix
|
|
} else {
|
|
s.strip_suffix('s').unwrap_or(s)
|
|
};
|
|
|
|
s.chars().all(char::is_alphanumeric)
|
|
&& s.chars().filter(|&c| c.is_uppercase()).take(2).count() > 1
|
|
&& s.chars().filter(|&c| c.is_lowercase()).take(1).count() > 0
|
|
}
|
|
|
|
fn has_underscore(s: &str) -> bool {
|
|
s != "_" && !s.contains("\\_") && s.contains('_')
|
|
}
|
|
|
|
fn has_hyphen(s: &str) -> bool {
|
|
s != "-" && s.contains('-')
|
|
}
|
|
|
|
if let Ok(url) = Url::parse(word)
|
|
// try to get around the fact that `foo::bar` parses as a valid URL
|
|
&& !url.cannot_be_a_base()
|
|
{
|
|
span_lint_and_sugg(
|
|
cx,
|
|
DOC_MARKDOWN,
|
|
span,
|
|
"you should put bare URLs between `<`/`>` or make a proper Markdown link",
|
|
"try",
|
|
format!("<{word}>"),
|
|
Applicability::MachineApplicable,
|
|
);
|
|
return;
|
|
}
|
|
|
|
// We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343)
|
|
//
|
|
// We also assume that backticks are not necessary if inside a quote. (Issue #10262)
|
|
if code_level > 0 || blockquote_level > 0 || (has_underscore(word) && has_hyphen(word)) {
|
|
return;
|
|
}
|
|
|
|
if has_underscore(word) || word.contains("::") || is_camel_case(word) || word.ends_with("()") {
|
|
span_lint_and_then(
|
|
cx,
|
|
DOC_MARKDOWN,
|
|
span,
|
|
"item in documentation is missing backticks",
|
|
|diag| {
|
|
let mut applicability = Applicability::MachineApplicable;
|
|
let snippet = snippet_with_applicability(cx, span, "..", &mut applicability);
|
|
diag.span_suggestion_verbose(span, "try", format!("`{snippet}`"), applicability);
|
|
},
|
|
);
|
|
}
|
|
}
|