mirror of
https://github.com/rust-lang/rust-clippy
synced 2025-02-17 06:28:42 +00:00
move doc.rs
to its own subdirectory
This commit is contained in:
parent
edb720b199
commit
56cee3c587
8 changed files with 423 additions and 343 deletions
20
clippy_lints/src/doc/link_with_quotes.rs
Normal file
20
clippy_lints/src/doc/link_with_quotes.rs
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
109
clippy_lints/src/doc/markdown.rs
Normal file
109
clippy_lints/src/doc/markdown.rs
Normal 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,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
clippy_lints/src/doc/missing_headers.rs
Normal file
84
clippy_lints/src/doc/missing_headers.rs
Normal 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,16 @@
|
||||||
use clippy_utils::attrs::is_doc_hidden;
|
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::macros::{is_panic, root_macro_call_first_node};
|
||||||
use clippy_utils::source::snippet_with_applicability;
|
use clippy_utils::ty::is_type_diagnostic_item;
|
||||||
use clippy_utils::ty::{implements_trait, is_type_diagnostic_item};
|
use clippy_utils::visitors::Visitable;
|
||||||
use clippy_utils::{is_entrypoint_fn, method_chain_args, return_ty};
|
use clippy_utils::{is_entrypoint_fn, method_chain_args};
|
||||||
use pulldown_cmark::Event::{
|
use pulldown_cmark::Event::{
|
||||||
Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text,
|
Code, End, FootnoteReference, HardBreak, Html, Rule, SoftBreak, Start, TaskListMarker, Text,
|
||||||
};
|
};
|
||||||
use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph};
|
use pulldown_cmark::Tag::{CodeBlock, Heading, Item, Link, Paragraph};
|
||||||
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options};
|
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options};
|
||||||
use rustc_ast::ast::{Async, Attribute, Fn, FnRetTy, ItemKind};
|
use rustc_ast::ast::Attribute;
|
||||||
use rustc_ast::token::CommentKind;
|
|
||||||
use rustc_ast::{AttrKind, AttrStyle};
|
|
||||||
use rustc_data_structures::fx::FxHashSet;
|
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 as hir;
|
||||||
use rustc_hir::intravisit::{self, Visitor};
|
use rustc_hir::intravisit::{self, Visitor};
|
||||||
use rustc_hir::{AnonConst, Expr};
|
use rustc_hir::{AnonConst, Expr};
|
||||||
|
@ -23,20 +18,21 @@ use rustc_lint::{LateContext, LateLintPass};
|
||||||
use rustc_middle::hir::nested_filter;
|
use rustc_middle::hir::nested_filter;
|
||||||
use rustc_middle::lint::in_external_macro;
|
use rustc_middle::lint::in_external_macro;
|
||||||
use rustc_middle::ty;
|
use rustc_middle::ty;
|
||||||
use rustc_parse::maybe_new_parser_from_source_str;
|
|
||||||
use rustc_parse::parser::ForceCollect;
|
|
||||||
use rustc_resolve::rustdoc::{
|
use rustc_resolve::rustdoc::{
|
||||||
add_doc_fragment, attrs_to_doc_fragments, main_body_opts, source_span_for_markdown_range, DocFragment,
|
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_session::{declare_tool_lint, impl_lint_pass};
|
||||||
use rustc_span::edition::Edition;
|
use rustc_span::edition::Edition;
|
||||||
use rustc_span::source_map::{FilePathMapping, SourceMap};
|
use rustc_span::{sym, Span};
|
||||||
use rustc_span::{sym, BytePos, FileName, Pos, Span};
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::{io, thread};
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
mod link_with_quotes;
|
||||||
|
mod markdown;
|
||||||
|
mod missing_headers;
|
||||||
|
mod needless_doctest_main;
|
||||||
|
mod suspicious_doc_comments;
|
||||||
|
|
||||||
declare_clippy_lint! {
|
declare_clippy_lint! {
|
||||||
/// ### What it does
|
/// ### What it does
|
||||||
/// Checks for the presence of `_`, `::` or camel-case words
|
/// 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) => {
|
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)) {
|
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 body = cx.tcx.hir().body(body_id);
|
||||||
let mut fpu = FindPanicUnwrap {
|
|
||||||
cx,
|
let panic_span = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
|
||||||
typeck_results: cx.tcx.typeck(item.owner_id.def_id),
|
missing_headers::check(cx, item.owner_id, sig, headers, Some(body_id), panic_span);
|
||||||
panic_span: None,
|
|
||||||
};
|
|
||||||
fpu.visit_expr(body.value);
|
|
||||||
lint_for_missing_headers(cx, item.owner_id, sig, headers, Some(body_id), fpu.panic_span);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
hir::ItemKind::Impl(impl_) => {
|
hir::ItemKind::Impl(impl_) => {
|
||||||
|
@ -395,7 +387,7 @@ impl<'tcx> LateLintPass<'tcx> for DocMarkdown {
|
||||||
};
|
};
|
||||||
if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind {
|
if let hir::TraitItemKind::Fn(ref sig, ..) = item.kind {
|
||||||
if !in_external_macro(cx.tcx.sess, item.span) {
|
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 {
|
if let hir::ImplItemKind::Fn(ref sig, body_id) = item.kind {
|
||||||
let body = cx.tcx.hir().body(body_id);
|
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(
|
let panic_span = FindPanicUnwrap::find_span(cx, cx.tcx.typeck(item.owner_id), body.value);
|
||||||
cx: &LateContext<'_>,
|
missing_headers::check(cx, item.owner_id, sig, headers, Some(body_id), panic_span);
|
||||||
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",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -515,6 +428,13 @@ struct DocHeaders {
|
||||||
panics: bool,
|
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> {
|
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
|
/// 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
|
/// 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;
|
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 (fragments, _) = attrs_to_doc_fragments(attrs.iter().map(|attr| (attr, None)), true);
|
||||||
let mut doc = String::new();
|
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"];
|
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
|
#[allow(clippy::too_many_lines)] // Only a big match statement
|
||||||
fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
|
fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
|
||||||
cx: &LateContext<'_>,
|
cx: &LateContext<'_>,
|
||||||
|
@ -665,7 +552,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
||||||
} else {
|
} else {
|
||||||
for (text, range) in text_to_check {
|
for (text, range) in text_to_check {
|
||||||
if let Some(span) = fragments.span(cx, range) {
|
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 in_code {
|
||||||
if is_rust && !no_test {
|
if is_rust && !no_test {
|
||||||
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());
|
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 {
|
} else {
|
||||||
if in_link.is_some() {
|
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()
|
if let Some(link) = in_link.as_ref()
|
||||||
&& let Ok(url) = Url::parse(link)
|
&& let Ok(url) = Url::parse(link)
|
||||||
|
@ -713,208 +600,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
|
||||||
headers
|
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> {
|
struct FindPanicUnwrap<'a, 'tcx> {
|
||||||
cx: &'a LateContext<'tcx>,
|
cx: &'a LateContext<'tcx>,
|
||||||
panic_span: Option<Span>,
|
panic_span: Option<Span>,
|
||||||
typeck_results: &'tcx ty::TypeckResults<'tcx>,
|
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> {
|
impl<'a, 'tcx> Visitor<'tcx> for FindPanicUnwrap<'a, 'tcx> {
|
||||||
type NestedFilter = nested_filter::OnlyBodies;
|
type NestedFilter = nested_filter::OnlyBodies;
|
||||||
|
|
100
clippy_lints/src/doc/needless_doctest_main.rs
Normal file
100
clippy_lints/src/doc/needless_doctest_main.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
48
clippy_lints/src/doc/suspicious_doc_comments.rs
Normal file
48
clippy_lints/src/doc/suspicious_doc_comments.rs
Normal 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()
|
||||||
|
}
|
|
@ -11,6 +11,12 @@ pub fn foo() {
|
||||||
bar()
|
bar()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Calls ["bar"] uselessly
|
||||||
|
//~^ ERROR: possible intra-doc link using quotes instead of backticks
|
||||||
|
pub fn foo2() {
|
||||||
|
bar()
|
||||||
|
}
|
||||||
|
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// This demonstrates issue \#8961
|
/// This demonstrates issue \#8961
|
||||||
/// ```
|
/// ```
|
||||||
|
|
|
@ -7,5 +7,11 @@ LL | /// Calls ['bar'] uselessly
|
||||||
= note: `-D clippy::doc-link-with-quotes` implied by `-D warnings`
|
= note: `-D clippy::doc-link-with-quotes` implied by `-D warnings`
|
||||||
= help: to override `-D warnings` add `#[allow(clippy::doc_link_with_quotes)]`
|
= 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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue