mirror of
https://github.com/rust-lang/rust-clippy
synced 2024-11-23 13:13:34 +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::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;
|
||||
|
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()
|
||||
}
|
||||
|
||||
/// Calls ["bar"] uselessly
|
||||
//~^ ERROR: possible intra-doc link using quotes instead of backticks
|
||||
pub fn foo2() {
|
||||
bar()
|
||||
}
|
||||
|
||||
/// # Examples
|
||||
/// This demonstrates issue \#8961
|
||||
/// ```
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in a new issue