Skip to content

Commit 1d5f7aa

Browse files
committed
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 describing the kind of errors that can be returned (https://doc.rust-lang.org/book/ch14-02-publishing-to-crates-io.html#commonly-used-sections). 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.
1 parent ff1607e commit 1d5f7aa

File tree

7 files changed

+210
-47
lines changed

7 files changed

+210
-47
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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

Lines changed: 100 additions & 45 deletions
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,34 @@ 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+
/// /// # Errors
89+
/// ///
90+
/// /// Will return `Err` if `filename` does not exist or the user does not have
91+
/// /// permission to read it.
92+
/// pub fn read(filename: String) -> io::Result<String> {
93+
/// unimplemented!();
94+
/// }
95+
/// ```
96+
pub MISSING_ERRORS_DOC,
97+
pedantic,
98+
"`pub fn` returns `Result` without `# Errors` in doc comment"
99+
}
100+
73101
declare_clippy_lint! {
74102
/// **What it does:** Checks for `fn main() { .. }` in doctests
75103
///
@@ -114,28 +142,19 @@ impl DocMarkdown {
114142
}
115143
}
116144

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

119147
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
120148
fn check_crate(&mut self, cx: &LateContext<'a, 'tcx>, krate: &'tcx hir::Crate) {
121149
check_attrs(cx, &self.valid_idents, &krate.attrs);
122150
}
123151

124152
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
129153
match item.kind {
130154
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-
}
155+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
156+
lint_missing_safety_doc(cx, item.hir_id, item.span, sig, headers);
157+
lint_missing_errors_doc(cx, item.hir_id, item.span, headers);
139158
},
140159
hir::ItemKind::Impl(_, _, _, _, ref trait_ref, ..) => {
141160
self.in_trait_impl = trait_ref.is_some();
@@ -151,40 +170,59 @@ 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
158173
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-
}
174+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
175+
lint_missing_safety_doc(cx, item.hir_id, item.span, sig, headers);
176+
lint_missing_errors_doc(cx, item.hir_id, item.span, headers);
167177
}
168178
}
169179

170180
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 {
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+
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
186+
lint_missing_safety_doc(cx, item.hir_id, item.span, sig, headers);
187+
lint_missing_errors_doc(cx, item.hir_id, item.span, headers);
184188
}
185189
}
186190
}
187191

192+
fn lint_missing_safety_doc<'a, 'tcx>(
193+
cx: &LateContext<'a, 'tcx>,
194+
hir_id: hir::HirId,
195+
span: impl Into<MultiSpan>,
196+
sig: &hir::FnSig,
197+
headers: DocHeaders,
198+
) {
199+
if !headers.safety && cx.access_levels.is_exported(hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
200+
span_lint(
201+
cx,
202+
MISSING_SAFETY_DOC,
203+
span,
204+
"unsafe function's docs miss `# Safety` section",
205+
);
206+
}
207+
}
208+
209+
fn lint_missing_errors_doc<'a, 'tcx>(
210+
cx: &LateContext<'a, 'tcx>,
211+
hir_id: hir::HirId,
212+
span: impl Into<MultiSpan>,
213+
headers: DocHeaders,
214+
) {
215+
if !headers.errors && cx.access_levels.is_exported(hir_id) && match_type(cx, return_ty(cx, hir_id), &paths::RESULT)
216+
{
217+
span_lint(
218+
cx,
219+
MISSING_ERRORS_DOC,
220+
span,
221+
"docs for function returning `Result` missing `# Errors` section",
222+
);
223+
}
224+
}
225+
188226
/// Cleanup documentation decoration (`///` and such).
189227
///
190228
/// We can't use `syntax::attr::AttributeMethods::with_desugared_doc` or
@@ -243,7 +281,13 @@ pub fn strip_doc_comment_decoration(comment: &str, span: Span) -> (String, Vec<(
243281
panic!("not a doc-comment: {}", comment);
244282
}
245283

246-
pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> bool {
284+
#[derive(Copy, Clone)]
285+
struct DocHeaders {
286+
safety: bool,
287+
errors: bool,
288+
}
289+
290+
fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders {
247291
let mut doc = String::new();
248292
let mut spans = vec![];
249293

@@ -255,7 +299,11 @@ pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String
255299
doc.push_str(&comment);
256300
} else if attr.check_name(sym!(doc)) {
257301
// ignore mix of sugared and non-sugared doc
258-
return true; // don't trigger the safety check
302+
// don't trigger the safety or errors check
303+
return DocHeaders {
304+
safety: true,
305+
errors: true,
306+
};
259307
}
260308
}
261309

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

269317
if doc.is_empty() {
270-
return false;
318+
return DocHeaders {
319+
safety: false,
320+
errors: false,
321+
};
271322
}
272323

273324
let parser = pulldown_cmark::Parser::new(&doc).into_offset_iter();
@@ -295,12 +346,15 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
295346
valid_idents: &FxHashSet<String>,
296347
events: Events,
297348
spans: &[(usize, Span)],
298-
) -> bool {
349+
) -> DocHeaders {
299350
// true if a safety header was found
300351
use pulldown_cmark::Event::*;
301352
use pulldown_cmark::Tag::*;
302353

303-
let mut safety_header = false;
354+
let mut headers = DocHeaders {
355+
safety: false,
356+
errors: false,
357+
};
304358
let mut in_code = false;
305359
let mut in_link = None;
306360
let mut in_heading = false;
@@ -323,7 +377,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
323377
// text "http://example.com" by pulldown-cmark
324378
continue;
325379
}
326-
safety_header |= in_heading && text.trim() == "Safety";
380+
headers.safety |= in_heading && text.trim() == "Safety";
381+
headers.errors |= in_heading && text.trim() == "Errors";
327382
let index = match spans.binary_search_by(|c| c.0.cmp(&range.start)) {
328383
Ok(o) => o,
329384
Err(e) => e - 1,
@@ -340,7 +395,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
340395
},
341396
}
342397
}
343-
safety_header
398+
headers
344399
}
345400

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

clippy_lints/src/lib.rs

Lines changed: 2 additions & 0 deletions
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

Lines changed: 8 additions & 1 deletion
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

Lines changed: 64 additions & 0 deletions
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() {}

0 commit comments

Comments
 (0)