Skip to content

Commit a9dc65c

Browse files
committed
missing_safety_doc: guide them out of an HTML block with a header
1 parent 9563a5c commit a9dc65c

9 files changed

+1432
-45
lines changed

clippy_lints/src/doc/doc_suspicious_footnotes.rs

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
use clippy_utils::diagnostics::span_lint_and_then;
2-
use rustc_ast::attr::AttributeExt as _;
32
use rustc_ast::token::CommentKind;
43
use rustc_errors::Applicability;
54
use rustc_hir::{AttrStyle, Attribute};
@@ -8,7 +7,7 @@ use rustc_resolve::rustdoc::DocFragmentKind;
87

98
use std::ops::Range;
109

11-
use super::{DOC_SUSPICIOUS_FOOTNOTES, Fragments};
10+
use super::{DOC_SUSPICIOUS_FOOTNOTES, Fragments, find_doc_attr_by_span};
1211

1312
pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &Fragments<'_>, attrs: &[Attribute]) {
1413
for i in doc[range.clone()]
@@ -44,14 +43,8 @@ pub fn check(cx: &LateContext<'_>, doc: &str, range: Range<usize>, fragments: &F
4443
"looks like a footnote ref, but has no matching footnote",
4544
|diag| {
4645
if this_fragment.kind == DocFragmentKind::SugaredDoc {
47-
let (doc_attr, (_, doc_attr_comment_kind), attr_style) = attrs
48-
.iter()
49-
.filter(|attr| attr.span().overlaps(this_fragment.span))
50-
.rev()
51-
.find_map(|attr| {
52-
Some((attr, attr.doc_str_and_comment_kind()?, attr.doc_resolution_scope()?))
53-
})
54-
.unwrap();
46+
let (doc_attr, doc_attr_comment_kind, attr_style) =
47+
find_doc_attr_by_span(attrs, this_fragment.span).unwrap();
5548
let (to_add, terminator) = match (doc_attr_comment_kind, attr_style) {
5649
(CommentKind::Line, AttrStyle::Outer) => ("\n///\n/// ", ""),
5750
(CommentKind::Line, AttrStyle::Inner) => ("\n//!\n//! ", ""),

clippy_lints/src/doc/lazy_continuation.rs

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,6 @@ use std::ops::Range;
77

88
use super::{DOC_LAZY_CONTINUATION, DOC_OVERINDENTED_LIST_ITEMS, Fragments};
99

10-
fn map_container_to_text(c: &super::Container) -> &'static str {
11-
match c {
12-
super::Container::Blockquote => "> ",
13-
// numbered list can have up to nine digits, plus the dot, plus four spaces on either side
14-
super::Container::List(indent) => &" "[0..*indent],
15-
}
16-
}
17-
1810
pub(super) fn check(
1911
cx: &LateContext<'_>,
2012
doc: &str,
@@ -41,7 +33,7 @@ pub(super) fn check(
4133
let mut doc_start_range = &doc[cooked_range];
4234
let mut suggested = String::new();
4335
for c in containers {
44-
let text = map_container_to_text(c);
36+
let text = c.map_to_text();
4537
if doc_start_range.starts_with(text) {
4638
doc_start_range = &doc_start_range[text.len()..];
4739
span = span.with_lo(

clippy_lints/src/doc/missing_headers.rs

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{DocHeaders, MISSING_ERRORS_DOC, MISSING_PANICS_DOC, MISSING_SAFETY_DOC, UNNECESSARY_SAFETY_DOC};
2-
use clippy_utils::diagnostics::{span_lint, span_lint_and_note};
2+
use clippy_utils::diagnostics::span_lint;
33
use clippy_utils::macros::{is_panic, root_macro_call_first_node};
44
use clippy_utils::ty::{get_type_diagnostic_name, implements_trait_with_env, is_type_diagnostic_item};
55
use clippy_utils::visitors::for_each_expr;
@@ -33,37 +33,38 @@ pub fn check(
3333
}
3434

3535
let span = cx.tcx.def_span(owner_id);
36-
match (headers.safety, sig.header.safety()) {
37-
(false, Safety::Unsafe) => span_lint(
36+
match (headers.safety.is_missing(), sig.header.safety()) {
37+
(true, Safety::Unsafe) => headers.safety.lint(
3838
cx,
3939
MISSING_SAFETY_DOC,
4040
span,
4141
"unsafe function's docs are missing a `# Safety` section",
4242
),
43-
(true, Safety::Safe) => span_lint(
43+
(false, Safety::Safe) => span_lint(
4444
cx,
4545
UNNECESSARY_SAFETY_DOC,
4646
span,
4747
"safe function's docs have unnecessary `# Safety` section",
4848
),
4949
_ => (),
5050
}
51-
if !headers.panics
51+
if headers.panics.is_missing()
5252
&& let Some(body_id) = body_id
5353
&& let Some(panic_span) = find_panic(cx, body_id)
5454
{
55-
span_lint_and_note(
55+
headers.panics.lint_and_then(
5656
cx,
5757
MISSING_PANICS_DOC,
5858
span,
5959
"docs for function which may panic missing `# Panics` section",
60-
Some(panic_span),
61-
"first possible panic found here",
60+
|diag| {
61+
diag.span_note(panic_span, "first possible panic found here");
62+
},
6263
);
6364
}
64-
if !headers.errors {
65+
if headers.errors.is_missing() {
6566
if is_type_diagnostic_item(cx, return_ty(cx, owner_id), sym::Result) {
66-
span_lint(
67+
headers.errors.lint(
6768
cx,
6869
MISSING_ERRORS_DOC,
6970
span,
@@ -85,7 +86,7 @@ pub fn check(
8586
&& let ty::Coroutine(_, subs) = ret_ty.kind()
8687
&& is_type_diagnostic_item(cx, subs.as_coroutine().return_ty(), sym::Result)
8788
{
88-
span_lint(
89+
headers.errors.lint(
8990
cx,
9091
MISSING_ERRORS_DOC,
9192
span,

clippy_lints/src/doc/mod.rs

Lines changed: 179 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@ use pulldown_cmark::Event::{
1010
};
1111
use pulldown_cmark::Tag::{BlockQuote, CodeBlock, FootnoteDefinition, Heading, Item, Link, Paragraph};
1212
use pulldown_cmark::{BrokenLink, CodeBlockKind, CowStr, Options, TagEnd};
13+
use rustc_ast::attr::AttributeExt;
14+
use rustc_ast::token::CommentKind;
1315
use rustc_data_structures::fx::FxHashSet;
14-
use rustc_errors::Applicability;
15-
use rustc_hir::{Attribute, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};
16-
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, LintContext};
16+
use rustc_errors::{Applicability, Diag, DiagMessage, MultiSpan};
17+
use rustc_hir::{AttrStyle, Attribute, ImplItemKind, ItemKind, Node, Safety, TraitItemKind};
18+
use rustc_lint::{EarlyContext, EarlyLintPass, LateContext, LateLintPass, Lint, LintContext};
1719
use rustc_resolve::rustdoc::{
1820
DocFragment, add_doc_fragment, attrs_to_doc_fragments, main_body_opts, source_span_for_markdown_range,
1921
span_of_fragments,
@@ -740,14 +742,14 @@ impl<'tcx> LateLintPass<'tcx> for Documentation {
740742
);
741743
}
742744
},
743-
ItemKind::Trait(_, _, unsafety, ..) => match (headers.safety, unsafety) {
745+
ItemKind::Trait(_, _, unsafety, ..) => match (headers.safety.is_missing(), unsafety) {
744746
(false, Safety::Unsafe) => span_lint(
745747
cx,
746748
MISSING_SAFETY_DOC,
747749
cx.tcx.def_span(item.owner_id),
748750
"docs for unsafe trait missing `# Safety` section",
749751
),
750-
(true, Safety::Safe) => span_lint(
752+
(true, Safety::Safe) => headers.safety.lint(
751753
cx,
752754
UNNECESSARY_SAFETY_DOC,
753755
cx.tcx.def_span(item.owner_id),
@@ -800,11 +802,101 @@ impl Fragments<'_> {
800802
}
801803
}
802804

803-
#[derive(Copy, Clone, Default)]
805+
#[derive(Clone, Default)]
806+
enum DocHeaderInfo {
807+
#[default]
808+
None,
809+
Found,
810+
SuspiciousHtml(Option<Span>, &'static str, Vec<Container>),
811+
}
812+
813+
impl DocHeaderInfo {
814+
fn suspicious_html(
815+
cx: &LateContext<'_>,
816+
fragments: Fragments<'_>,
817+
idx: usize,
818+
attrs: &[Attribute],
819+
containers: &[Container],
820+
) -> DocHeaderInfo {
821+
use rustc_ast::token::CommentKind;
822+
use rustc_hir::AttrStyle;
823+
let span = fragments.span(cx, idx..idx);
824+
let indent = span
825+
.and_then(|span| fragments.fragments.iter().find(|fragment| fragment.span.overlaps(span)))
826+
.map(|fragment| fragment.indent)
827+
.unwrap_or(0);
828+
DocHeaderInfo::SuspiciousHtml(
829+
span,
830+
span.and_then(|span| find_doc_attr_by_span(attrs, span)).map_or(
831+
"",
832+
|(_doc_attr, doc_attr_comment_kind, attr_style)| match (doc_attr_comment_kind, attr_style) {
833+
(CommentKind::Block, _) => &" "[..indent],
834+
(CommentKind::Line, AttrStyle::Outer) => &"/// "[..indent + 3],
835+
(CommentKind::Line, AttrStyle::Inner) => &"//! "[..indent + 3],
836+
},
837+
),
838+
containers.to_vec(),
839+
)
840+
}
841+
fn is_missing(&self) -> bool {
842+
matches!(self, DocHeaderInfo::None | DocHeaderInfo::SuspiciousHtml(..))
843+
}
844+
fn lint(&self, cx: &LateContext<'_>, lint: &'static Lint, sp: impl Into<MultiSpan>, msg: impl Into<DiagMessage>) {
845+
self.lint_and_then(cx, lint, sp, msg, |_| {})
846+
}
847+
fn lint_and_then(
848+
&self,
849+
cx: &LateContext<'_>,
850+
lint: &'static Lint,
851+
sp: impl Into<MultiSpan>,
852+
msg: impl Into<DiagMessage>,
853+
f: impl FnOnce(&mut Diag<'_, ()>),
854+
) {
855+
if let DocHeaderInfo::SuspiciousHtml(html_span, comment_prefix, containers) = self {
856+
span_lint_and_then(cx, lint, sp, msg, |diag| {
857+
f(diag);
858+
diag.note("markdown syntax is not recognized within a block of raw HTML code");
859+
if let Some(html_span) = html_span {
860+
diag.span_suggestion(
861+
*html_span,
862+
"to recognize this text as a header, add a blank line",
863+
format!(
864+
"\n{comment_prefix}{containers}",
865+
containers = containers
866+
.iter()
867+
.map(Container::map_to_text)
868+
.collect::<Vec<&str>>()
869+
.join("")
870+
),
871+
Applicability::Unspecified,
872+
);
873+
} else {
874+
diag.help(
875+
"to recognize a markdown header nested within an HTML element,\
876+
add a blank line between the `#` and the HTML",
877+
);
878+
}
879+
});
880+
} else {
881+
span_lint(cx, lint, sp, msg);
882+
}
883+
}
884+
}
885+
886+
fn find_doc_attr_by_span(attrs: &[Attribute], span: Span) -> Option<(&Attribute, CommentKind, AttrStyle)> {
887+
let (doc_attr, (_, doc_attr_comment_kind), attr_style) = attrs
888+
.iter()
889+
.filter(|attr| attr.span().overlaps(span))
890+
.rev()
891+
.find_map(|attr| Some((attr, attr.doc_str_and_comment_kind()?, attr.doc_resolution_scope()?)))?;
892+
Some((doc_attr, doc_attr_comment_kind, attr_style))
893+
}
894+
895+
#[derive(Clone, Default)]
804896
struct DocHeaders {
805-
safety: bool,
806-
errors: bool,
807-
panics: bool,
897+
safety: DocHeaderInfo,
898+
errors: DocHeaderInfo,
899+
panics: DocHeaderInfo,
808900
first_paragraph_len: usize,
809901
}
810902

@@ -900,11 +992,22 @@ fn check_attrs(cx: &LateContext<'_>, valid_idents: &FxHashSet<String>, attrs: &[
900992

901993
const RUST_CODE: &[&str] = &["rust", "no_run", "should_panic", "compile_fail"];
902994

995+
#[derive(Clone)]
903996
enum Container {
904997
Blockquote,
905998
List(usize),
906999
}
9071000

1001+
impl Container {
1002+
fn map_to_text(&self) -> &'static str {
1003+
match self {
1004+
Container::Blockquote => "> ",
1005+
// numbered list can have up to nine digits, plus the dot, plus four spaces on either side
1006+
Container::List(indent) => &" "[0..*indent],
1007+
}
1008+
}
1009+
}
1010+
9081011
/// Scan the documentation for code links that are back-to-back with code spans.
9091012
///
9101013
/// This is done separately from the rest of the docs, because that makes it easier to produce
@@ -1011,6 +1114,51 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
10111114
} else if tag.starts_with("</blockquote") || tag.starts_with("</q") {
10121115
blockquote_level -= 1;
10131116
}
1117+
if headers.safety.is_missing() &&
1118+
let Some(idx) = tag.find("# Safety").filter(|idx| tag[idx + 8..].trim().bytes().all(|c| c == b'#'))
1119+
.or_else(|| tag.find("Safety\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-'))))
1120+
.or_else(|| tag.find("# SAFETY").filter(|idx| tag[idx + 8..].trim().bytes().all(|c| c == b'#')))
1121+
.or_else(|| tag.find("SAFETY\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-'))))
1122+
.or_else(|| tag.find("# Implementation safety").filter(|idx| tag[idx + 23..].trim().bytes().all(|c| c == b'#')))
1123+
.or_else(|| tag.find("Implementation safety\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-'))))
1124+
.or_else(|| tag.find("# Implementation Safety").filter(|idx| tag[idx + 23..].trim().bytes().all(|c| c == b'#')))
1125+
.or_else(|| tag.find("Implementation Safety\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-')))) &&
1126+
tag[..idx].trim().bytes().all(|c| c == b'#')
1127+
{
1128+
headers.safety = DocHeaderInfo::suspicious_html(
1129+
cx,
1130+
fragments,
1131+
range.start,
1132+
attrs,
1133+
&containers,
1134+
);
1135+
}
1136+
if headers.errors.is_missing() &&
1137+
let Some(idx) = tag.find("# Errors").filter(|idx| tag[idx + 8..].trim().bytes().all(|c| c == b'#'))
1138+
.or_else(|| tag.find("Errors\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-')))) &&
1139+
tag[..idx].trim().bytes().all(|c| c == b'#')
1140+
{
1141+
headers.errors = DocHeaderInfo::suspicious_html(
1142+
cx,
1143+
fragments,
1144+
range.start,
1145+
attrs,
1146+
&containers,
1147+
);
1148+
}
1149+
if headers.panics.is_missing() &&
1150+
let Some(idx) = tag.find("# Panics").filter(|idx| tag[idx + 8..].trim().bytes().all(|c| c == b'#'))
1151+
.or_else(|| tag.find("Panics\n").filter(|_| matches!(events.peek(), Some((Html(ln) | InlineHtml(ln), _)) if ln.trim().bytes().all(|c| c == b'=') || ln.trim().bytes().all(|c| c == b'-')))) &&
1152+
tag[..idx].trim().bytes().all(|c| c == b'#')
1153+
{
1154+
headers.panics = DocHeaderInfo::suspicious_html(
1155+
cx,
1156+
fragments,
1157+
range.start,
1158+
attrs,
1159+
&containers,
1160+
);
1161+
}
10141162
},
10151163
Start(BlockQuote(_)) => {
10161164
blockquote_level += 1;
@@ -1197,12 +1345,28 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
11971345
continue;
11981346
}
11991347
let trimmed_text = text.trim();
1200-
headers.safety |= in_heading && trimmed_text == "Safety";
1201-
headers.safety |= in_heading && trimmed_text == "SAFETY";
1202-
headers.safety |= in_heading && trimmed_text == "Implementation safety";
1203-
headers.safety |= in_heading && trimmed_text == "Implementation Safety";
1204-
headers.errors |= in_heading && trimmed_text == "Errors";
1205-
headers.panics |= in_heading && trimmed_text == "Panics";
1348+
if in_heading {
1349+
if headers.safety.is_missing()
1350+
&& (
1351+
trimmed_text == "Safety"
1352+
|| trimmed_text == "SAFETY"
1353+
|| trimmed_text == "Implementation safety"
1354+
|| trimmed_text == "Implementation Safety"
1355+
)
1356+
{
1357+
headers.safety = DocHeaderInfo::Found;
1358+
}
1359+
if headers.errors.is_missing()
1360+
&& trimmed_text == "Errors"
1361+
{
1362+
headers.errors = DocHeaderInfo::Found;
1363+
}
1364+
if headers.panics.is_missing()
1365+
&& trimmed_text == "Panics"
1366+
{
1367+
headers.panics = DocHeaderInfo::Found;
1368+
}
1369+
}
12061370
if in_code {
12071371
if is_rust && !no_test {
12081372
let edition = edition.unwrap_or_else(|| cx.tcx.sess.edition());

0 commit comments

Comments
 (0)