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, 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 if !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); }, ); } }