Skip to content

Commit f783ff3

Browse files
committed
Auto merge of #4535 - rust-lang:unsafe-doc, r=flip1995
New lint: Require `# Safety` section in pub unsafe fn docs changelog: add `missing_safety_doc` lint This fixes #2207
2 parents a5e568b + 70a7dab commit f783ff3

File tree

8 files changed

+133
-31
lines changed

8 files changed

+133
-31
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1056,6 +1056,7 @@ Released 2018-09-13
10561056
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
10571057
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
10581058
[`missing_inline_in_public_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_inline_in_public_items
1059+
[`missing_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_safety_doc
10591060
[`mistyped_literal_suffixes`]: https://rust-lang.github.io/rust-clippy/master/index.html#mistyped_literal_suffixes
10601061
[`mixed_case_hex_literals`]: https://rust-lang.github.io/rust-clippy/master/index.html#mixed_case_hex_literals
10611062
[`module_inception`]: https://rust-lang.github.io/rust-clippy/master/index.html#module_inception

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
88

9-
[There are 313 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are 314 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
1010

1111
We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:
1212

clippy_lints/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ semver = "0.9.0"
2727
serde = { version = "1.0", features = ["derive"] }
2828
toml = "0.5.3"
2929
unicode-normalization = "0.1"
30-
pulldown-cmark = "0.5.3"
30+
pulldown-cmark = "0.6.0"
3131
url = { version = "2.1.0", features = ["serde"] } # cargo requires serde feat in its url dep
3232
# see https://github.com/rust-lang/rust/pull/63587#issuecomment-522343864
3333
if_chain = "1.0.0"

clippy_lints/src/doc.rs

+83-28
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ declare_clippy_lint! {
3434
"presence of `_`, `::` or camel-case outside backticks in documentation"
3535
}
3636

37+
declare_clippy_lint! {
38+
/// **What it does:** Checks for the doc comments of publicly visible
39+
/// unsafe functions and warns if there is no `# Safety` section.
40+
///
41+
/// **Why is this bad?** Unsafe functions should document their safety
42+
/// preconditions, so that users can be sure they are using them safely.
43+
///
44+
/// **Known problems:** None.
45+
///
46+
/// **Examples**:
47+
/// ```rust
48+
///# type Universe = ();
49+
/// /// This function should really be documented
50+
/// pub unsafe fn start_apocalypse(u: &mut Universe) {
51+
/// unimplemented!();
52+
/// }
53+
/// ```
54+
///
55+
/// At least write a line about safety:
56+
///
57+
/// ```rust
58+
///# type Universe = ();
59+
/// /// # Safety
60+
/// ///
61+
/// /// This function should not be called before the horsemen are ready.
62+
/// pub unsafe fn start_apocalypse(u: &mut Universe) {
63+
/// unimplemented!();
64+
/// }
65+
/// ```
66+
pub MISSING_SAFETY_DOC,
67+
style,
68+
"`pub unsafe fn` without `# Safety` docs"
69+
}
70+
3771
#[allow(clippy::module_name_repetitions)]
3872
#[derive(Clone)]
3973
pub struct DocMarkdown {
@@ -46,15 +80,28 @@ impl DocMarkdown {
4680
}
4781
}
4882

49-
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN]);
83+
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC]);
5084

5185
impl EarlyLintPass for DocMarkdown {
5286
fn check_crate(&mut self, cx: &EarlyContext<'_>, krate: &ast::Crate) {
5387
check_attrs(cx, &self.valid_idents, &krate.attrs);
5488
}
5589

5690
fn check_item(&mut self, cx: &EarlyContext<'_>, item: &ast::Item) {
57-
check_attrs(cx, &self.valid_idents, &item.attrs);
91+
if check_attrs(cx, &self.valid_idents, &item.attrs) {
92+
return;
93+
}
94+
// no safety header
95+
if let ast::ItemKind::Fn(_, ref header, ..) = item.node {
96+
if item.vis.node.is_pub() && header.unsafety == ast::Unsafety::Unsafe {
97+
span_lint(
98+
cx,
99+
MISSING_SAFETY_DOC,
100+
item.span,
101+
"unsafe function's docs miss `# Safety` section",
102+
);
103+
}
104+
}
58105
}
59106
}
60107

@@ -115,7 +162,7 @@ pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(
115162
panic!("not a doc-comment: {}", comment);
116163
}
117164

118-
pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>, attrs: &'a [ast::Attribute]) {
165+
pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>, attrs: &'a [ast::Attribute]) -> bool {
119166
let mut doc = String::new();
120167
let mut spans = vec![];
121168

@@ -129,7 +176,7 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
129176
}
130177
} else if attr.check_name(sym!(doc)) {
131178
// ignore mix of sugared and non-sugared doc
132-
return;
179+
return true; // don't trigger the safety check
133180
}
134181
}
135182

@@ -140,57 +187,64 @@ pub fn check_attrs<'a>(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>,
140187
current += offset_copy;
141188
}
142189

143-
if !doc.is_empty() {
144-
let parser = pulldown_cmark::Parser::new(&doc).into_offset_iter();
145-
// Iterate over all `Events` and combine consecutive events into one
146-
let events = parser.coalesce(|previous, current| {
147-
use pulldown_cmark::Event::*;
148-
149-
let previous_range = previous.1;
150-
let current_range = current.1;
151-
152-
match (previous.0, current.0) {
153-
(Text(previous), Text(current)) => {
154-
let mut previous = previous.to_string();
155-
previous.push_str(&current);
156-
Ok((Text(previous.into()), previous_range))
157-
},
158-
(previous, current) => Err(((previous, previous_range), (current, current_range))),
159-
}
160-
});
161-
check_doc(cx, valid_idents, events, &spans);
190+
if doc.is_empty() {
191+
return false;
162192
}
193+
194+
let parser = pulldown_cmark::Parser::new(&doc).into_offset_iter();
195+
// Iterate over all `Events` and combine consecutive events into one
196+
let events = parser.coalesce(|previous, current| {
197+
use pulldown_cmark::Event::*;
198+
199+
let previous_range = previous.1;
200+
let current_range = current.1;
201+
202+
match (previous.0, current.0) {
203+
(Text(previous), Text(current)) => {
204+
let mut previous = previous.to_string();
205+
previous.push_str(&current);
206+
Ok((Text(previous.into()), previous_range))
207+
},
208+
(previous, current) => Err(((previous, previous_range), (current, current_range))),
209+
}
210+
});
211+
check_doc(cx, valid_idents, events, &spans)
163212
}
164213

165214
fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize>)>>(
166215
cx: &EarlyContext<'_>,
167216
valid_idents: &FxHashSet<String>,
168217
events: Events,
169218
spans: &[(usize, Span)],
170-
) {
219+
) -> bool {
220+
// true if a safety header was found
171221
use pulldown_cmark::Event::*;
172222
use pulldown_cmark::Tag::*;
173223

224+
let mut safety_header = false;
174225
let mut in_code = false;
175226
let mut in_link = None;
227+
let mut in_heading = false;
176228

177229
for (event, range) in events {
178230
match event {
179231
Start(CodeBlock(_)) => in_code = true,
180232
End(CodeBlock(_)) => in_code = false,
181233
Start(Link(_, url, _)) => in_link = Some(url),
182234
End(Link(..)) => in_link = None,
183-
Start(_tag) | End(_tag) => (), // We don't care about other tags
184-
Html(_html) | InlineHtml(_html) => (), // HTML is weird, just ignore it
185-
SoftBreak | HardBreak | TaskListMarker(_) | Code(_) => (),
235+
Start(Heading(_)) => in_heading = true,
236+
End(Heading(_)) => in_heading = false,
237+
Start(_tag) | End(_tag) => (), // We don't care about other tags
238+
Html(_html) => (), // HTML is weird, just ignore it
239+
SoftBreak | HardBreak | TaskListMarker(_) | Code(_) | Rule => (),
186240
FootnoteReference(text) | Text(text) => {
187241
if Some(&text) == in_link.as_ref() {
188242
// Probably a link of the form `<http://example.com>`
189243
// Which are represented as a link to "http://example.com" with
190244
// text "http://example.com" by pulldown-cmark
191245
continue;
192246
}
193-
247+
safety_header |= in_heading && text.trim() == "Safety";
194248
if !in_code {
195249
let index = match spans.binary_search_by(|c| c.0.cmp(&range.start)) {
196250
Ok(o) => o,
@@ -207,6 +261,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
207261
},
208262
}
209263
}
264+
safety_header
210265
}
211266

212267
fn check_text(cx: &EarlyContext<'_>, valid_idents: &FxHashSet<String>, text: &str, span: Span) {

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ pub fn register_plugins(reg: &mut rustc_driver::plugin::Registry<'_>, conf: &Con
708708
copies::IFS_SAME_COND,
709709
copies::IF_SAME_THEN_ELSE,
710710
derive::DERIVE_HASH_XOR_EQ,
711+
doc::MISSING_SAFETY_DOC,
711712
double_comparison::DOUBLE_COMPARISONS,
712713
double_parens::DOUBLE_PARENS,
713714
drop_bounds::DROP_BOUNDS,
@@ -929,6 +930,7 @@ pub fn register_plugins(reg: &mut rustc_driver::plugin::Registry<'_>, conf: &Con
929930
block_in_if_condition::BLOCK_IN_IF_CONDITION_EXPR,
930931
block_in_if_condition::BLOCK_IN_IF_CONDITION_STMT,
931932
collapsible_if::COLLAPSIBLE_IF,
933+
doc::MISSING_SAFETY_DOC,
932934
enum_variants::ENUM_VARIANT_NAMES,
933935
enum_variants::MODULE_INCEPTION,
934936
eq_op::OP_REF,

src/lintlist/mod.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ pub use lint::Lint;
66
pub use lint::LINT_LEVELS;
77

88
// begin lint list, do not remove this comment, it’s used in `update_lints`
9-
pub const ALL_LINTS: [Lint; 313] = [
9+
pub const ALL_LINTS: [Lint; 314] = [
1010
Lint {
1111
name: "absurd_extreme_comparisons",
1212
group: "correctness",
@@ -1078,6 +1078,13 @@ pub const ALL_LINTS: [Lint; 313] = [
10781078
deprecation: None,
10791079
module: "missing_inline",
10801080
},
1081+
Lint {
1082+
name: "missing_safety_doc",
1083+
group: "style",
1084+
desc: "`pub unsafe fn` without `# Safety` docs",
1085+
deprecation: None,
1086+
module: "doc",
1087+
},
10811088
Lint {
10821089
name: "mistyped_literal_suffixes",
10831090
group: "correctness",

tests/ui/doc_unsafe.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// This is not sufficiently documented
2+
pub unsafe fn destroy_the_planet() {
3+
unimplemented!();
4+
}
5+
6+
/// This one is
7+
///
8+
/// # Safety
9+
///
10+
/// This function shouldn't be called unless the horsemen are ready
11+
pub unsafe fn apocalypse(universe: &mut ()) {
12+
unimplemented!();
13+
}
14+
15+
/// This is a private function, so docs aren't necessary
16+
unsafe fn you_dont_see_me() {
17+
unimplemented!();
18+
}
19+
20+
fn main() {
21+
you_dont_see_me();
22+
destroy_the_planet();
23+
let mut universe = ();
24+
apocalypse(&mut universe);
25+
}

tests/ui/doc_unsafe.stderr

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
error: unsafe function's docs miss `# Safety` section
2+
--> $DIR/doc_unsafe.rs:2:1
3+
|
4+
LL | / pub unsafe fn destroy_the_planet() {
5+
LL | | unimplemented!();
6+
LL | | }
7+
| |_^
8+
|
9+
= note: `-D clippy::missing-safety-doc` implied by `-D warnings`
10+
11+
error: aborting due to previous error
12+

0 commit comments

Comments
 (0)