From 56cee3c58776d6af6e00fb035310b294392582b4 Mon Sep 17 00:00:00 2001 From: y21 <30553356+y21@users.noreply.github.com> Date: Wed, 15 Nov 2023 22:38:50 +0100 Subject: [PATCH] move `doc.rs` to its own subdirectory --- clippy_lints/src/doc/link_with_quotes.rs | 20 + clippy_lints/src/doc/markdown.rs | 109 +++++ clippy_lints/src/doc/missing_headers.rs | 84 ++++ clippy_lints/src/{doc.rs => doc/mod.rs} | 391 +++--------------- clippy_lints/src/doc/needless_doctest_main.rs | 100 +++++ .../src/doc/suspicious_doc_comments.rs | 48 +++ tests/ui/doc_link_with_quotes.rs | 6 + tests/ui/doc_link_with_quotes.stderr | 8 +- 8 files changed, 423 insertions(+), 343 deletions(-) create mode 100644 clippy_lints/src/doc/link_with_quotes.rs create mode 100644 clippy_lints/src/doc/markdown.rs create mode 100644 clippy_lints/src/doc/missing_headers.rs rename clippy_lints/src/{doc.rs => doc/mod.rs} (60%) create mode 100644 clippy_lints/src/doc/needless_doctest_main.rs create mode 100644 clippy_lints/src/doc/suspicious_doc_comments.rs diff --git a/clippy_lints/src/doc/link_with_quotes.rs b/clippy_lints/src/doc/link_with_quotes.rs new file mode 100644 index 00000000000..01191e811b0 --- /dev/null +++ b/clippy_lints/src/doc/link_with_quotes.rs @@ -0,0 +1,20 @@ +use std::ops::Range; + +use clippy_utils::diagnostics::span_lint; +use rustc_lint::LateContext; + +use super::{Fragments, DOC_LINK_WITH_QUOTES}; + +pub fn check(cx: &LateContext<'_>, trimmed_text: &str, range: Range, fragments: Fragments<'_>) { + if ((trimmed_text.starts_with('\'') && trimmed_text.ends_with('\'')) + || (trimmed_text.starts_with('"') && trimmed_text.ends_with('"'))) + && let Some(span) = fragments.span(cx, range) + { + span_lint( + cx, + DOC_LINK_WITH_QUOTES, + span, + "possible intra-doc link using quotes instead of backticks", + ); + } +} diff --git a/clippy_lints/src/doc/markdown.rs b/clippy_lints/src/doc/markdown.rs new file mode 100644 index 00000000000..c0b11eb0dd1 --- /dev/null +++ b/clippy_lints/src/doc/markdown.rs @@ -0,0 +1,109 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_then}; +use clippy_utils::source::snippet_with_applicability; +use rustc_data_structures::fx::FxHashSet; +use rustc_errors::{Applicability, SuggestionStyle}; +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) { + for 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 mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':'); + + // 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; + } + + // 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); + } +} + +fn check_word(cx: &LateContext<'_>, word: &str, span: Span) { + /// 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 = 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( + cx, + DOC_MARKDOWN, + span, + "you should put bare URLs between `<`/`>` or make a proper Markdown link", + ); + + return; + } + } + + // We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343) + if has_underscore(word) && has_hyphen(word) { + return; + } + + if has_underscore(word) || word.contains("::") || is_camel_case(word) { + let mut applicability = Applicability::MachineApplicable; + + span_lint_and_then( + cx, + DOC_MARKDOWN, + span, + "item in documentation is missing backticks", + |diag| { + let snippet = snippet_with_applicability(cx, span, "..", &mut applicability); + diag.span_suggestion_with_style( + span, + "try", + format!("`{snippet}`"), + applicability, + // always show the suggestion in a separate line, since the + // inline presentation adds another pair of backticks + SuggestionStyle::ShowAlways, + ); + }, + ); + } +} diff --git a/clippy_lints/src/doc/missing_headers.rs b/clippy_lints/src/doc/missing_headers.rs new file mode 100644 index 00000000000..703a100ef65 --- /dev/null +++ b/clippy_lints/src/doc/missing_headers.rs @@ -0,0 +1,84 @@ +use clippy_utils::diagnostics::{span_lint, span_lint_and_note}; +use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; +use clippy_utils::{is_doc_hidden, return_ty}; +use rustc_hir::{BodyId, FnSig, OwnerId, Unsafety}; +use rustc_lint::LateContext; +use rustc_middle::ty; +use rustc_span::{sym, Span}; + +use super::{DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC}; + +pub fn check( + cx: &LateContext<'_>, + owner_id: OwnerId, + sig: &FnSig<'_>, + headers: DocHeaders, + body_id: Option, + panic_span: Option, +) { + if !cx.effective_visibilities.is_exported(owner_id.def_id) { + return; // Private functions do not require doc comments + } + + // do not lint if any parent has `#[doc(hidden)]` attribute (#7347) + if cx + .tcx + .hir() + .parent_iter(owner_id.into()) + .any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id))) + { + return; + } + + let span = cx.tcx.def_span(owner_id); + match (headers.safety, sig.header.unsafety) { + (false, Unsafety::Unsafe) => span_lint( + cx, + MISSING_SAFETY_DOC, + span, + "unsafe function's docs miss `# Safety` section", + ), + (true, Unsafety::Normal) => span_lint( + cx, + UNNECESSARY_SAFETY_DOC, + span, + "safe function's docs have unnecessary `# Safety` section", + ), + _ => (), + } + if !headers.panics && panic_span.is_some() { + span_lint_and_note( + cx, + MISSING_PANICS_DOC, + span, + "docs for function which may panic missing `# Panics` section", + panic_span, + "first possible panic found here", + ); + } + if !headers.errors { + if is_type_diagnostic_item(cx, return_ty(cx, owner_id), sym::Result) { + span_lint( + cx, + MISSING_ERRORS_DOC, + span, + "docs for function returning `Result` missing `# Errors` section", + ); + } else if let Some(body_id) = body_id + && let Some(future) = cx.tcx.lang_items().future_trait() + && let typeck = cx.tcx.typeck_body(body_id) + && let body = cx.tcx.hir().body(body_id) + && let ret_ty = typeck.expr_ty(body.value) + && implements_trait(cx, ret_ty, future, &[]) + && let ty::Coroutine(_, subs, _) = ret_ty.kind() + && is_type_diagnostic_item(cx, subs.as_coroutine().return_ty(), sym::Result) + { + span_lint( + cx, + MISSING_ERRORS_DOC, + span, + "docs for function returning `Result` missing `# Errors` section", + ); + } + } +} diff --git a/clippy_lints/src/doc.rs b/clippy_lints/src/doc/mod.rs similarity index 60% rename from clippy_lints/src/doc.rs rename to clippy_lints/src/doc/mod.rs index ca277e7eded..2b8f0bc503b 100644 --- a/clippy_lints/src/doc.rs +++ b/clippy_lints/src/doc/mod.rs @@ -1,21 +1,16 @@ use clippy_utils::attrs::is_doc_hidden; -use clippy_utils::diagnostics::{span_lint, span_lint_and_help, span_lint_and_note, span_lint_and_then}; +use clippy_utils::diagnostics::{span_lint, span_lint_and_help}; use clippy_utils::macros::{is_panic, root_macro_call_first_node}; -use clippy_utils::source::snippet_with_applicability; -use clippy_utils::ty::{implements_trait, is_type_diagnostic_item}; -use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty}; +use clippy_utils::ty::is_type_diagnostic_item; +use clippy_utils::visitors::Visitable; +use clippy_utils::{is_entrypoint_fn, method_chain_args}; use pulldown_cmark::Event::{ Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text, }; use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph}; use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options}; -use rustc_ast::ast::{Async, Attribute, Fn, FnRetTy, ItemKind}; -use rustc_ast::token::CommentKind; -use rustc_ast::{AttrKind, AttrStyle}; +use rustc_ast::ast::Attribute; use rustc_data_structures::fx::FxHashSet; -use rustc_data_structures::sync::Lrc; -use rustc_errors::emitter::EmitterWriter; -use rustc_errors::{Applicability, Handler, SuggestionStyle}; use rustc_hir as hir; use rustc_hir::intravisit::{self, Visitor}; use rustc_hir::{AnonConst, Expr}; @@ -23,20 +18,21 @@ use rustc_lint::{LateContext, LateLintPass}; use rustc_middle::hir::nested_filter; use rustc_middle::lint::in_external_macro; use rustc_middle::ty; -use rustc_parse::maybe_new_parser_from_source_str; -use rustc_parse::parser::ForceCollect; use rustc_resolve::rustdoc::{ add_doc_fragment, attrs_to_doc_fragments, main_body_opts, source_span_for_markdown_range, DocFragment, }; -use rustc_session::parse::ParseSess; use rustc_session::{declare_tool_lint, impl_lint_pass}; use rustc_span::edition::Edition; -use rustc_span::source_map::{FilePathMapping, SourceMap}; -use rustc_span::{sym, BytePos, FileName, Pos, Span}; +use rustc_span::{sym, Span}; use std::ops::Range; -use std::{io, thread}; use url::Url; +mod link_with_quotes; +mod markdown; +mod missing_headers; +mod needless_doctest_main; +mod suspicious_doc_comments; + declare_clippy_lint! { /// ### What it does /// Checks for the presence of `_`, `::` or camel-case words @@ -351,13 +347,9 @@ impl<'tcx> LateLintPass<'tcx> for DocMarkdown { hir::ItemKind::Fn(ref sig, _, body_id) => { if !(is_entrypoint_fn(cx, item.owner_id.to_def_id()) || in_external_macro(cx.tcx.sess, item.span)) { let body = cx.tcx.hir().body(body_id); - let mut fpu = FindPanicUnwrap { - cx, - typeck_results: cx.tcx.typeck(item.owner_id.def_id), - panic_span: None, - }; - fpu.visit_expr(body.value); - lint_for_missing_headers(cx, item.owner_id, sig, headers, Some(body_id), fpu.panic_span); + + let panic_span = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value); + missing_headers::check(cx, item.owner_id, sig, headers, Some(body_id), panic_span); } }, hir::ItemKind::Impl(impl_) => { @@ -395,7 +387,7 @@ impl<'tcx> LateLintPass<'tcx> for DocMarkdown { }; if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind { if !in_external_macro(cx.tcx.sess, item.span) { - lint_for_missing_headers(cx, item.owner_id, sig, headers, None, None); + missing_headers::check(cx, item.owner_id, sig, headers, None, None); } } } @@ -410,88 +402,9 @@ impl<'tcx> LateLintPass<'tcx> for DocMarkdown { } if let hir::ImplItemKind::Fn(ref sig, body_id) = item.kind { let body = cx.tcx.hir().body(body_id); - let mut fpu = FindPanicUnwrap { - cx, - typeck_results: cx.tcx.typeck(item.owner_id.def_id), - panic_span: None, - }; - fpu.visit_expr(body.value); - lint_for_missing_headers(cx, item.owner_id, sig, headers, Some(body_id), fpu.panic_span); - } - } -} -fn lint_for_missing_headers( - cx: &LateContext<'_>, - owner_id: hir::OwnerId, - sig: &hir::FnSig<'_>, - headers: DocHeaders, - body_id: Option, - panic_span: Option, -) { - if !cx.effective_visibilities.is_exported(owner_id.def_id) { - return; // Private functions do not require doc comments - } - - // do not lint if any parent has `#[doc(hidden)]` attribute (#7347) - if cx - .tcx - .hir() - .parent_iter(owner_id.into()) - .any(|(id, _node)| is_doc_hidden(cx.tcx.hir().attrs(id))) - { - return; - } - - let span = cx.tcx.def_span(owner_id); - match (headers.safety, sig.header.unsafety) { - (false, hir::Unsafety::Unsafe) => span_lint( - cx, - MISSING_SAFETY_DOC, - span, - "unsafe function's docs miss `# Safety` section", - ), - (true, hir::Unsafety::Normal) => span_lint( - cx, - UNNECESSARY_SAFETY_DOC, - span, - "safe function's docs have unnecessary `# Safety` section", - ), - _ => (), - } - if !headers.panics && panic_span.is_some() { - span_lint_and_note( - cx, - MISSING_PANICS_DOC, - span, - "docs for function which may panic missing `# Panics` section", - panic_span, - "first possible panic found here", - ); - } - if !headers.errors { - if is_type_diagnostic_item(cx, return_ty(cx, owner_id), sym::Result) { - span_lint( - cx, - MISSING_ERRORS_DOC, - span, - "docs for function returning `Result` missing `# Errors` section", - ); - } else if let Some(body_id) = body_id - && let Some(future) = cx.tcx.lang_items().future_trait() - && let typeck = cx.tcx.typeck_body(body_id) - && let body = cx.tcx.hir().body(body_id) - && let ret_ty = typeck.expr_ty(body.value) - && implements_trait(cx, ret_ty, future, &[]) - && let ty::Coroutine(_, subs, _) = ret_ty.kind() - && is_type_diagnostic_item(cx, subs.as_coroutine().return_ty(), sym::Result) - { - span_lint( - cx, - MISSING_ERRORS_DOC, - span, - "docs for function returning `Result` missing `# Errors` section", - ); + let panic_span = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value); + missing_headers::check(cx, item.owner_id, sig, headers, Some(body_id), panic_span); } } } @@ -515,6 +428,13 @@ struct DocHeaders { panics: bool, } +/// Does some pre-processing on raw, desugared `#[doc]` attributes such as parsing them and +/// then delegates to `check_doc`. +/// Some lints are already checked here if they can work with attributes directly and don't need +/// to work with markdown. +/// Others are checked elsewhere, e.g. in `check_doc` if they need access to markdown, or +/// back in the various late lint pass methods if they need the final doc headers, like "Safety" or +/// "Panics" sections. fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[Attribute]) -> Option { /// We don't want the parser to choke on intra doc links. Since we don't /// actually care about rendering them, just pretend that all broken links @@ -528,7 +448,7 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ return None; } - check_almost_inner_doc(cx, attrs); + suspicious_doc_comments::check(cx, attrs); let (fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true); let mut doc = String::new(); @@ -558,45 +478,12 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet, attrs: &[ )) } -/// Looks for `///!` and `/**!` comments, which were probably meant to be `//!` and `/*!` -fn check_almost_inner_doc(cx: &LateContext<'_>, attrs: &[Attribute]) { - let replacements: Vec<_> = attrs - .iter() - .filter_map(|attr| { - if let AttrKind::DocComment(com_kind, sym) = attr.kind - && let AttrStyle::Outer = attr.style - && let Some(com) = sym.as_str().strip_prefix('!') - { - let sugg = match com_kind { - CommentKind::Line => format!("//!{com}"), - CommentKind::Block => format!("/*!{com}*/"), - }; - Some((attr.span, sugg)) - } else { - None - } - }) - .collect(); - - if let Some((&(lo_span, _), &(hi_span, _))) = replacements.first().zip(replacements.last()) { - span_lint_and_then( - cx, - SUSPICIOUS_DOC_COMMENTS, - lo_span.to(hi_span), - "this is an outer doc comment and does not apply to the parent module or crate", - |diag| { - diag.multipart_suggestion( - "use an inner doc comment to document the parent module or crate", - replacements, - Applicability::MaybeIncorrect, - ); - }, - ); - } -} - const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"]; +/// Checks parsed documentation. +/// This walks the "events" (think sections of markdown) produced by `pulldown_cmark`, +/// so lints here will generally access that information. +/// Returns documentation headers -- whether a "Safety", "Errors", "Panic" section was found #[allow(clippy::too_many_lines)] // Only a big match statement fn check_doc<'a, Events: Iterator, Range)>>( cx: &LateContext<'_>, @@ -665,7 +552,7 @@ fn check_doc<'a, Events: Iterator, Range, Range, Range, trimmed_text: &str, range: Range, fragments: Fragments<'_>) { - if trimmed_text.starts_with('\'') - && trimmed_text.ends_with('\'') - && let Some(span) = fragments.span(cx, range) - { - span_lint( - cx, - DOC_LINK_WITH_QUOTES, - span, - "possible intra-doc link using quotes instead of backticks", - ); - } -} - -fn check_code(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range, fragments: Fragments<'_>) { - fn has_needless_main(code: String, edition: Edition) -> bool { - rustc_driver::catch_fatal_errors(|| { - rustc_span::create_session_globals_then(edition, || { - let filename = FileName::anon_source_code(&code); - - let fallback_bundle = - rustc_errors::fallback_fluent_bundle(rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(), false); - let emitter = EmitterWriter::new(Box::new(io::sink()), fallback_bundle); - let handler = Handler::with_emitter(Box::new(emitter)).disable_warnings(); - #[expect(clippy::arc_with_non_send_sync)] // `Lrc` is expected by with_span_handler - let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); - let sess = ParseSess::with_span_handler(handler, sm); - - let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code) { - Ok(p) => p, - Err(errs) => { - drop(errs); - return false; - }, - }; - - let mut relevant_main_found = false; - loop { - match parser.parse_item(ForceCollect::No) { - Ok(Some(item)) => match &item.kind { - ItemKind::Fn(box Fn { - sig, body: Some(block), .. - }) if item.ident.name == sym::main => { - let is_async = matches!(sig.header.asyncness, Async::Yes { .. }); - let returns_nothing = match &sig.decl.output { - FnRetTy::Default(..) => true, - FnRetTy::Ty(ty) if ty.kind.is_unit() => true, - FnRetTy::Ty(_) => false, - }; - - if returns_nothing && !is_async && !block.stmts.is_empty() { - // This main function should be linted, but only if there are no other functions - relevant_main_found = true; - } else { - // This main function should not be linted, we're done - return false; - } - }, - // Tests with one of these items are ignored - ItemKind::Static(..) - | ItemKind::Const(..) - | ItemKind::ExternCrate(..) - | ItemKind::ForeignMod(..) - // Another function was found; this case is ignored - | ItemKind::Fn(..) => return false, - _ => {}, - }, - Ok(None) => break, - Err(e) => { - e.cancel(); - return false; - }, - } - } - - relevant_main_found - }) - }) - .ok() - .unwrap_or_default() - } - - let trailing_whitespace = text.len() - text.trim_end().len(); - - // Because of the global session, we need to create a new session in a different thread with - // the edition we need. - let text = text.to_owned(); - if thread::spawn(move || has_needless_main(text, edition)) - .join() - .expect("thread::spawn failed") - && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) - { - span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest"); - } -} - -fn check_text(cx: &LateContext<'_>, valid_idents: &FxHashSet, text: &str, span: Span) { - for 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 mut word = word.trim_matches(|c: char| !c.is_alphanumeric() && c != ':'); - - // 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; - } - - // 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); - } -} - -fn check_word(cx: &LateContext<'_>, word: &str, span: Span) { - /// 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 = 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( - cx, - DOC_MARKDOWN, - span, - "you should put bare URLs between `<`/`>` or make a proper Markdown link", - ); - - return; - } - } - - // We assume that mixed-case words are not meant to be put inside backticks. (Issue #2343) - if has_underscore(word) && has_hyphen(word) { - return; - } - - if has_underscore(word) || word.contains("::") || is_camel_case(word) { - let mut applicability = Applicability::MachineApplicable; - - span_lint_and_then( - cx, - DOC_MARKDOWN, - span, - "item in documentation is missing backticks", - |diag| { - let snippet = snippet_with_applicability(cx, span, "..", &mut applicability); - diag.span_suggestion_with_style( - span, - "try", - format!("`{snippet}`"), - applicability, - // always show the suggestion in a separate line, since the - // inline presentation adds another pair of backticks - SuggestionStyle::ShowAlways, - ); - }, - ); - } -} - struct FindPanicUnwrap<'a, 'tcx> { cx: &'a LateContext<'tcx>, panic_span: Option, typeck_results: &'tcx ty::TypeckResults<'tcx>, } +impl<'a, 'tcx> FindPanicUnwrap<'a, 'tcx> { + pub fn find_span( + cx: &'a LateContext<'tcx>, + typeck_results: &'tcx ty::TypeckResults<'tcx>, + body: impl Visitable<'tcx>, + ) -> Option { + let mut vis = Self { + cx, + panic_span: None, + typeck_results, + }; + body.visit(&mut vis); + vis.panic_span + } +} + impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> { type NestedFilter = nested_filter::OnlyBodies; diff --git a/clippy_lints/src/doc/needless_doctest_main.rs b/clippy_lints/src/doc/needless_doctest_main.rs new file mode 100644 index 00000000000..4d7d7611dd8 --- /dev/null +++ b/clippy_lints/src/doc/needless_doctest_main.rs @@ -0,0 +1,100 @@ +use std::ops::Range; +use std::{io, thread}; + +use crate::doc::NEEDLESS_DOCTEST_MAIN; +use clippy_utils::diagnostics::span_lint; +use rustc_ast::{Async, Fn, FnRetTy, ItemKind}; +use rustc_data_structures::sync::Lrc; +use rustc_errors::emitter::EmitterWriter; +use rustc_errors::Handler; +use rustc_lint::LateContext; +use rustc_parse::maybe_new_parser_from_source_str; +use rustc_parse::parser::ForceCollect; +use rustc_session::parse::ParseSess; +use rustc_span::edition::Edition; +use rustc_span::source_map::{FilePathMapping, SourceMap}; +use rustc_span::{sym, FileName}; + +use super::Fragments; + +pub fn check(cx: &LateContext<'_>, text: &str, edition: Edition, range: Range, fragments: Fragments<'_>) { + fn has_needless_main(code: String, edition: Edition) -> bool { + rustc_driver::catch_fatal_errors(|| { + rustc_span::create_session_globals_then(edition, || { + let filename = FileName::anon_source_code(&code); + + let fallback_bundle = + rustc_errors::fallback_fluent_bundle(rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(), false); + let emitter = EmitterWriter::new(Box::new(io::sink()), fallback_bundle); + let handler = Handler::with_emitter(Box::new(emitter)).disable_warnings(); + #[expect(clippy::arc_with_non_send_sync)] // `Lrc` is expected by with_span_handler + let sm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + let sess = ParseSess::with_span_handler(handler, sm); + + let mut parser = match maybe_new_parser_from_source_str(&sess, filename, code) { + Ok(p) => p, + Err(errs) => { + drop(errs); + return false; + }, + }; + + let mut relevant_main_found = false; + loop { + match parser.parse_item(ForceCollect::No) { + Ok(Some(item)) => match &item.kind { + ItemKind::Fn(box Fn { + sig, body: Some(block), .. + }) if item.ident.name == sym::main => { + let is_async = matches!(sig.header.asyncness, Async::Yes { .. }); + let returns_nothing = match &sig.decl.output { + FnRetTy::Default(..) => true, + FnRetTy::Ty(ty) if ty.kind.is_unit() => true, + FnRetTy::Ty(_) => false, + }; + + if returns_nothing && !is_async && !block.stmts.is_empty() { + // This main function should be linted, but only if there are no other functions + relevant_main_found = true; + } else { + // This main function should not be linted, we're done + return false; + } + }, + // Tests with one of these items are ignored + ItemKind::Static(..) + | ItemKind::Const(..) + | ItemKind::ExternCrate(..) + | ItemKind::ForeignMod(..) + // Another function was found; this case is ignored + | ItemKind::Fn(..) => return false, + _ => {}, + }, + Ok(None) => break, + Err(e) => { + e.cancel(); + return false; + }, + } + } + + relevant_main_found + }) + }) + .ok() + .unwrap_or_default() + } + + let trailing_whitespace = text.len() - text.trim_end().len(); + + // Because of the global session, we need to create a new session in a different thread with + // the edition we need. + let text = text.to_owned(); + if thread::spawn(move || has_needless_main(text, edition)) + .join() + .expect("thread::spawn failed") + && let Some(span) = fragments.span(cx, range.start..range.end - trailing_whitespace) + { + span_lint(cx, NEEDLESS_DOCTEST_MAIN, span, "needless `fn main` in doctest"); + } +} diff --git a/clippy_lints/src/doc/suspicious_doc_comments.rs b/clippy_lints/src/doc/suspicious_doc_comments.rs new file mode 100644 index 00000000000..d7ad30efec3 --- /dev/null +++ b/clippy_lints/src/doc/suspicious_doc_comments.rs @@ -0,0 +1,48 @@ +use clippy_utils::diagnostics::span_lint_and_then; +use rustc_ast::token::CommentKind; +use rustc_ast::{AttrKind, AttrStyle, Attribute}; +use rustc_errors::Applicability; +use rustc_lint::LateContext; +use rustc_span::Span; + +use super::SUSPICIOUS_DOC_COMMENTS; + +pub fn check(cx: &LateContext<'_>, attrs: &[Attribute]) { + let replacements: Vec<_> = collect_doc_replacements(attrs); + + if let Some((&(lo_span, _), &(hi_span, _))) = replacements.first().zip(replacements.last()) { + span_lint_and_then( + cx, + SUSPICIOUS_DOC_COMMENTS, + lo_span.to(hi_span), + "this is an outer doc comment and does not apply to the parent module or crate", + |diag| { + diag.multipart_suggestion( + "use an inner doc comment to document the parent module or crate", + replacements, + Applicability::MaybeIncorrect, + ); + }, + ); + } +} + +fn collect_doc_replacements(attrs: &[Attribute]) -> Vec<(Span, String)> { + attrs + .iter() + .filter_map(|attr| { + if let AttrKind::DocComment(com_kind, sym) = attr.kind + && let AttrStyle::Outer = attr.style + && let Some(com) = sym.as_str().strip_prefix('!') + { + let sugg = match com_kind { + CommentKind::Line => format!("//!{com}"), + CommentKind::Block => format!("/*!{com}*/"), + }; + Some((attr.span, sugg)) + } else { + None + } + }) + .collect() +} diff --git a/tests/ui/doc_link_with_quotes.rs b/tests/ui/doc_link_with_quotes.rs index 37d0d135957..48e1b1819c6 100644 --- a/tests/ui/doc_link_with_quotes.rs +++ b/tests/ui/doc_link_with_quotes.rs @@ -11,6 +11,12 @@ pub fn foo() { bar() } +/// Calls ["bar"] uselessly +//~^ ERROR: possible intra-doc link using quotes instead of backticks +pub fn foo2() { + bar() +} + /// # Examples /// This demonstrates issue \#8961 /// ``` diff --git a/tests/ui/doc_link_with_quotes.stderr b/tests/ui/doc_link_with_quotes.stderr index 2db1bc09289..cd4f87c56b4 100644 --- a/tests/ui/doc_link_with_quotes.stderr +++ b/tests/ui/doc_link_with_quotes.stderr @@ -7,5 +7,11 @@ LL | /// Calls ['bar'] uselessly = note: `-D clippy::doc-link-with-quotes` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::doc_link_with_quotes)]` -error: aborting due to previous error +error: possible intra-doc link using quotes instead of backticks + --> $DIR/doc_link_with_quotes.rs:14:12 + | +LL | /// Calls ["bar"] uselessly + | ^^^^^ + +error: aborting due to 2 previous errors