Skip to content

Commit 61b19a1

Browse files
committed
Auto merge of #4884 - RobbieClarken:warn-missing-errors, r=llogiq
Add lint for pub fns returning a `Result` without documenting errors The Rust Book recommends that functions that return a `Result` type have [a doc comment with an `# Errors` section](https://doc.rust-lang.org/book/ch14-02-publishing-to-crates-io.html#commonly-used-sections) describing the kind of errors that can be returned. This change adds a lint to enforce this. The lint is allow by default; it can be enabled with `#![warn(clippy::missing_errors_doc)]`. Closes #4854. changelog: adds lint for `missing_errors_doc`
2 parents ff1607e + f5d0a45 commit 61b19a1

File tree

7 files changed

+202
-47
lines changed

7 files changed

+202
-47
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,7 @@ Released 2018-09-13
10921092
[`misrefactored_assign_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#misrefactored_assign_op
10931093
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
10941094
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
1095+
[`missing_errors_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_errors_doc
10951096
[`missing_inline_in_public_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_inline_in_public_items
10961097
[`missing_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_safety_doc
10971098
[`mistyped_literal_suffixes`]: https://rust-lang.github.io/rust-clippy/master/index.html#mistyped_literal_suffixes

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 338 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
9+
[There are 339 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/src/doc.rs

+92-45
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::utils::span_lint;
1+
use crate::utils::{match_type, paths, return_ty, span_lint};
22
use itertools::Itertools;
33
use pulldown_cmark;
44
use rustc::hir;
@@ -8,7 +8,7 @@ use rustc_data_structures::fx::FxHashSet;
88
use rustc_session::declare_tool_lint;
99
use std::ops::Range;
1010
use syntax::ast::{AttrKind, Attribute};
11-
use syntax::source_map::{BytePos, Span};
11+
use syntax::source_map::{BytePos, MultiSpan, Span};
1212
use syntax_pos::Pos;
1313
use url::Url;
1414

@@ -45,7 +45,7 @@ declare_clippy_lint! {
4545
///
4646
/// **Known problems:** None.
4747
///
48-
/// **Examples**:
48+
/// **Examples:**
4949
/// ```rust
5050
///# type Universe = ();
5151
/// /// This function should really be documented
@@ -70,6 +70,35 @@ declare_clippy_lint! {
7070
"`pub unsafe fn` without `# Safety` docs"
7171
}
7272

73+
declare_clippy_lint! {
74+
/// **What it does:** Checks the doc comments of publicly visible functions that
75+
/// return a `Result` type and warns if there is no `# Errors` section.
76+
///
77+
/// **Why is this bad?** Documenting the type of errors that can be returned from a
78+
/// function can help callers write code to handle the errors appropriately.
79+
///
80+
/// **Known problems:** None.
81+
///
82+
/// **Examples:**
83+
///
84+
/// Since the following function returns a `Result` it has an `# Errors` section in
85+
/// its doc comment:
86+
///
87+
/// ```rust
88+
///# use std::io;
89+
/// /// # Errors
90+
/// ///
91+
/// /// Will return `Err` if `filename` does not exist or the user does not have
92+
/// /// permission to read it.
93+
/// pub fn read(filename: String) -> io::Result<String> {
94+
/// unimplemented!();
95+
/// }
96+
/// ```
97+
pub MISSING_ERRORS_DOC,
98+
pedantic,
99+
"`pub fn` returns `Result` without `# Errors` in doc comment"
100+
}
101+
73102
declare_clippy_lint! {
74103
/// **What it does:** Checks for `fn main() { .. }` in doctests
75104
///
@@ -114,28 +143,18 @@ impl DocMarkdown {
114143
}
115144
}
116145

117-
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC, NEEDLESS_DOCTEST_MAIN]);
146+
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC, MISSING_ERRORS_DOC, NEEDLESS_DOCTEST_MAIN]);
118147

119148
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
120149
fn check_crate(&mut self, cx: &LateContext<'a, 'tcx>, krate: &'tcx hir::Crate) {
121150
check_attrs(cx, &self.valid_idents, &krate.attrs);
122151
}
123152

124153
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::Item) {
125-
if check_attrs(cx, &self.valid_idents, &item.attrs) {
126-
return;
127-
}
128-
// no safety header
154+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
129155
match item.kind {
130156
hir::ItemKind::Fn(ref sig, ..) => {
131-
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
132-
span_lint(
133-
cx,
134-
MISSING_SAFETY_DOC,
135-
item.span,
136-
"unsafe function's docs miss `# Safety` section",
137-
);
138-
}
157+
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
139158
},
140159
hir::ItemKind::Impl(_, _, _, _, ref trait_ref, ..) => {
141160
self.in_trait_impl = trait_ref.is_some();
@@ -151,40 +170,51 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
151170
}
152171

153172
fn check_trait_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::TraitItem) {
154-
if check_attrs(cx, &self.valid_idents, &item.attrs) {
155-
return;
156-
}
157-
// no safety header
173+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
158174
if let hir::TraitItemKind::Method(ref sig, ..) = item.kind {
159-
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
160-
span_lint(
161-
cx,
162-
MISSING_SAFETY_DOC,
163-
item.span,
164-
"unsafe function's docs miss `# Safety` section",
165-
);
166-
}
175+
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
167176
}
168177
}
169178

170179
fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::ImplItem) {
171-
if check_attrs(cx, &self.valid_idents, &item.attrs) || self.in_trait_impl {
180+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
181+
if self.in_trait_impl {
172182
return;
173183
}
174-
// no safety header
175184
if let hir::ImplItemKind::Method(ref sig, ..) = item.kind {
176-
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
177-
span_lint(
178-
cx,
179-
MISSING_SAFETY_DOC,
180-
item.span,
181-
"unsafe function's docs miss `# Safety` section",
182-
);
183-
}
185+
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
184186
}
185187
}
186188
}
187189

190+
fn lint_for_missing_headers<'a, 'tcx>(
191+
cx: &LateContext<'a, 'tcx>,
192+
hir_id: hir::HirId,
193+
span: impl Into<MultiSpan> + Copy,
194+
sig: &hir::FnSig,
195+
headers: DocHeaders,
196+
) {
197+
if !cx.access_levels.is_exported(hir_id) {
198+
return; // Private functions do not require doc comments
199+
}
200+
if !headers.safety && sig.header.unsafety == hir::Unsafety::Unsafe {
201+
span_lint(
202+
cx,
203+
MISSING_SAFETY_DOC,
204+
span,
205+
"unsafe function's docs miss `# Safety` section",
206+
);
207+
}
208+
if !headers.errors && match_type(cx, return_ty(cx, hir_id), &paths::RESULT) {
209+
span_lint(
210+
cx,
211+
MISSING_ERRORS_DOC,
212+
span,
213+
"docs for function returning `Result` missing `# Errors` section",
214+
);
215+
}
216+
}
217+
188218
/// Cleanup documentation decoration (`///` and such).
189219
///
190220
/// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
@@ -243,7 +273,13 @@ pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(
243273
panic!("not a doc-comment: {}", comment);
244274
}
245275

246-
pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> bool {
276+
#[derive(Copy, Clone)]
277+
struct DocHeaders {
278+
safety: bool,
279+
errors: bool,
280+
}
281+
282+
fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders {
247283
let mut doc = String::new();
248284
let mut spans = vec![];
249285

@@ -255,7 +291,11 @@ pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String
255291
doc.push_str(&comment);
256292
} else if attr.check_name(sym!(doc)) {
257293
// ignore mix of sugared and non-sugared doc
258-
return true; // don't trigger the safety check
294+
// don't trigger the safety or errors check
295+
return DocHeaders {
296+
safety: true,
297+
errors: true,
298+
};
259299
}
260300
}
261301

@@ -267,7 +307,10 @@ pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String
267307
}
268308

269309
if doc.is_empty() {
270-
return false;
310+
return DocHeaders {
311+
safety: false,
312+
errors: false,
313+
};
271314
}
272315

273316
let parser = pulldown_cmark::Parser::new(&doc).into_offset_iter();
@@ -295,12 +338,15 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
295338
valid_idents: &FxHashSet<String>,
296339
events: Events,
297340
spans: &[(usize, Span)],
298-
) -> bool {
341+
) -> DocHeaders {
299342
// true if a safety header was found
300343
use pulldown_cmark::Event::*;
301344
use pulldown_cmark::Tag::*;
302345

303-
let mut safety_header = false;
346+
let mut headers = DocHeaders {
347+
safety: false,
348+
errors: false,
349+
};
304350
let mut in_code = false;
305351
let mut in_link = None;
306352
let mut in_heading = false;
@@ -323,7 +369,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
323369
// text "http://example.com" by pulldown-cmark
324370
continue;
325371
}
326-
safety_header |= in_heading && text.trim() == "Safety";
372+
headers.safety |= in_heading && text.trim() == "Safety";
373+
headers.errors |= in_heading && text.trim() == "Errors";
327374
let index = match spans.binary_search_by(|c| c.0.cmp(&range.start)) {
328375
Ok(o) => o,
329376
Err(e) => e - 1,
@@ -340,7 +387,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
340387
},
341388
}
342389
}
343-
safety_header
390+
headers
344391
}
345392

346393
fn check_code(cx: &LateContext<'_, '_>, text: &str, span: Span) {

clippy_lints/src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
488488
&derive::DERIVE_HASH_XOR_EQ,
489489
&derive::EXPL_IMPL_CLONE_ON_COPY,
490490
&doc::DOC_MARKDOWN,
491+
&doc::MISSING_ERRORS_DOC,
491492
&doc::MISSING_SAFETY_DOC,
492493
&doc::NEEDLESS_DOCTEST_MAIN,
493494
&double_comparison::DOUBLE_COMPARISONS,
@@ -1013,6 +1014,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
10131014
LintId::of(&default_trait_access::DEFAULT_TRAIT_ACCESS),
10141015
LintId::of(&derive::EXPL_IMPL_CLONE_ON_COPY),
10151016
LintId::of(&doc::DOC_MARKDOWN),
1017+
LintId::of(&doc::MISSING_ERRORS_DOC),
10161018
LintId::of(&empty_enum::EMPTY_ENUM),
10171019
LintId::of(&enum_glob_use::ENUM_GLOB_USE),
10181020
LintId::of(&enum_variants::MODULE_NAME_REPETITIONS),

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; 338] = [
9+
pub const ALL_LINTS: [Lint; 339] = [
1010
Lint {
1111
name: "absurd_extreme_comparisons",
1212
group: "correctness",
@@ -1127,6 +1127,13 @@ pub const ALL_LINTS: [Lint; 338] = [
11271127
deprecation: None,
11281128
module: "missing_doc",
11291129
},
1130+
Lint {
1131+
name: "missing_errors_doc",
1132+
group: "pedantic",
1133+
desc: "`pub fn` returns `Result` without `# Errors` in doc comment",
1134+
deprecation: None,
1135+
module: "doc",
1136+
},
11301137
Lint {
11311138
name: "missing_inline_in_public_items",
11321139
group: "restriction",

tests/ui/doc_errors.rs

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#![warn(clippy::missing_errors_doc)]
2+
3+
use std::io;
4+
5+
pub fn pub_fn_missing_errors_header() -> Result<(), ()> {
6+
unimplemented!();
7+
}
8+
9+
/// This is not sufficiently documented.
10+
pub fn pub_fn_returning_io_result() -> io::Result<()> {
11+
unimplemented!();
12+
}
13+
14+
/// # Errors
15+
/// A description of the errors goes here.
16+
pub fn pub_fn_with_errors_header() -> Result<(), ()> {
17+
unimplemented!();
18+
}
19+
20+
/// This function doesn't require the documentation because it is private
21+
fn priv_fn_missing_errors_header() -> Result<(), ()> {
22+
unimplemented!();
23+
}
24+
25+
pub struct Struct1;
26+
27+
impl Struct1 {
28+
/// This is not sufficiently documented.
29+
pub fn pub_method_missing_errors_header() -> Result<(), ()> {
30+
unimplemented!();
31+
}
32+
33+
/// # Errors
34+
/// A description of the errors goes here.
35+
pub fn pub_method_with_errors_header() -> Result<(), ()> {
36+
unimplemented!();
37+
}
38+
39+
/// This function doesn't require the documentation because it is private.
40+
fn priv_method_missing_errors_header() -> Result<(), ()> {
41+
unimplemented!();
42+
}
43+
}
44+
45+
pub trait Trait1 {
46+
/// This is not sufficiently documented.
47+
fn trait_method_missing_errors_header() -> Result<(), ()>;
48+
49+
/// # Errors
50+
/// A description of the errors goes here.
51+
fn trait_method_with_errors_header() -> Result<(), ()>;
52+
}
53+
54+
impl Trait1 for Struct1 {
55+
fn trait_method_missing_errors_header() -> Result<(), ()> {
56+
unimplemented!();
57+
}
58+
59+
fn trait_method_with_errors_header() -> Result<(), ()> {
60+
unimplemented!();
61+
}
62+
}
63+
64+
fn main() {}

tests/ui/doc_errors.stderr

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
error: docs for function returning `Result` missing `# Errors` section
2+
--> $DIR/doc_errors.rs:5:1
3+
|
4+
LL | / pub fn pub_fn_missing_errors_header() -> Result<(), ()> {
5+
LL | | unimplemented!();
6+
LL | | }
7+
| |_^
8+
|
9+
= note: `-D clippy::missing-errors-doc` implied by `-D warnings`
10+
11+
error: docs for function returning `Result` missing `# Errors` section
12+
--> $DIR/doc_errors.rs:10:1
13+
|
14+
LL | / pub fn pub_fn_returning_io_result() -> io::Result<()> {
15+
LL | | unimplemented!();
16+
LL | | }
17+
| |_^
18+
19+
error: docs for function returning `Result` missing `# Errors` section
20+
--> $DIR/doc_errors.rs:29:5
21+
|
22+
LL | / pub fn pub_method_missing_errors_header() -> Result<(), ()> {
23+
LL | | unimplemented!();
24+
LL | | }
25+
| |_____^
26+
27+
error: docs for function returning `Result` missing `# Errors` section
28+
--> $DIR/doc_errors.rs:47:5
29+
|
30+
LL | fn trait_method_missing_errors_header() -> Result<(), ()>;
31+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
32+
33+
error: aborting due to 4 previous errors
34+

0 commit comments

Comments
 (0)