Auto merge of #11801 - y21:split_doc_pass, r=blyxyas

Split `doc.rs` up into a subdirectory

So, first, sorry for the bad diff. 😅

In #11798, `@flip1995`  suggested splitting `doc.rs` up, much like how we have the `methods/`, `matches/`, `types/` subdirectories.
I agree with this, the file is getting bigger as we add more and more doc lints that it makes sense to do this refactoring.

This is purely an internal change that moves things around a bit.
(**EDIT:** depending on the outcome of https://github.com/rust-lang/rust-clippy/pull/11801#issuecomment-1816715615 , this may change the lint group name from `doc_markdoc` to `doc`).

I tried to not change any of the actual logic of the lints and as such some things weren't as easy to move to a separate file. So we still have some `span_lint*` calls in the `doc/mod.rs` file, which I think is fine. This is also the case in `methods/mod.rs`.

Also worth mentioning that the lints missing_errors_doc, missing_panics_doc, missing_safety_doc and unnecessary_safety_doc have a lot of the same logic so it didn't make much sense for each of these to be in their own file. Instead I just put them all in `missing_headers.rs`

I also added a bit of documentation to the involved `check_{attrs,doc}` methods.

changelog: none
This commit is contained in:
bors 2023-11-20 17:22:05 +00:00
commit 72c0d80f46
8 changed files with 423 additions and 343 deletions

View File

@ -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<usize>, 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",
);
}
}

View File

@ -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<String>, 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,
);
},
);
}
}

View File

@ -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<BodyId>,
panic_span: Option<Span>,
) {
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",
);
}
}
}

View File

@ -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<hir::BodyId>,
panic_span: Option<Span>,
) {
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<String>, attrs: &[Attribute]) -> Option<DocHeaders> {
/// 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<String>, 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<String>, 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<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
cx: &LateContext<'_>,
@ -665,7 +552,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
} else {
for (text, range) in text_to_check {
if let Some(span) = fragments.span(cx, range) {
check_text(cx, valid_idents, &text, span);
markdown::check(cx, valid_idents, &text, span);
}
}
}
@ -692,11 +579,11 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
if in_code {
if is_rust && !no_test {
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
check_code(cx, &text, edition, range.clone(), fragments);
needless_doctest_main::check(cx, &text, edition, range.clone(), fragments);
}
} else {
if in_link.is_some() {
check_link_quotes(cx, trimmed_text, range.clone(), fragments);
link_with_quotes::check(cx, trimmed_text, range.clone(), fragments);
}
if let Some(link) = in_link.as_ref()
&& let Ok(url) = Url::parse(link)
@ -713,208 +600,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
headers
}
fn check_link_quotes(cx: &LateContext<'_>, trimmed_text: &str, range: Range<usize>, 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<usize>, 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<String>, 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<Span>,
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<Span> {
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;

View File

@ -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<usize>, 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");
}
}

View File

@ -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()
}

View File

@ -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
/// ```

View File

@ -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