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.
This commit is contained in:
RobbieClarken 2019-12-06 10:30:23 +10:30
parent ff1607ea4a
commit f5d0a452ba
7 changed files with 202 additions and 47 deletions

View file

@ -1092,6 +1092,7 @@ Released 2018-09-13
[`misrefactored_assign_op`]: https://rust-lang.github.io/rust-clippy/master/index.html#misrefactored_assign_op
[`missing_const_for_fn`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_const_for_fn
[`missing_docs_in_private_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_docs_in_private_items
[`missing_errors_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_errors_doc
[`missing_inline_in_public_items`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_inline_in_public_items
[`missing_safety_doc`]: https://rust-lang.github.io/rust-clippy/master/index.html#missing_safety_doc
[`mistyped_literal_suffixes`]: https://rust-lang.github.io/rust-clippy/master/index.html#mistyped_literal_suffixes

View file

@ -6,7 +6,7 @@
A collection of lints to catch common mistakes and improve your [Rust](https://github.com/rust-lang/rust) code.
[There are 338 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
[There are 339 lints included in this crate!](https://rust-lang.github.io/rust-clippy/master/index.html)
We have a bunch of lint categories to allow you to choose how much Clippy is supposed to ~~annoy~~ help you:

View file

@ -1,4 +1,4 @@
use crate::utils::span_lint;
use crate::utils::{match_type, paths, return_ty, span_lint};
use itertools::Itertools;
use pulldown_cmark;
use rustc::hir;
@ -8,7 +8,7 @@ use rustc_data_structures::fx::FxHashSet;
use rustc_session::declare_tool_lint;
use std::ops::Range;
use syntax::ast::{AttrKind, Attribute};
use syntax::source_map::{BytePos, Span};
use syntax::source_map::{BytePos, MultiSpan, Span};
use syntax_pos::Pos;
use url::Url;
@ -45,7 +45,7 @@ declare_clippy_lint! {
///
/// **Known problems:** None.
///
/// **Examples**:
/// **Examples:**
/// ```rust
///# type Universe = ();
/// /// This function should really be documented
@ -70,6 +70,35 @@ declare_clippy_lint! {
"`pub unsafe fn` without `# Safety` docs"
}
declare_clippy_lint! {
/// **What it does:** Checks the doc comments of publicly visible functions that
/// return a `Result` type and warns if there is no `# Errors` section.
///
/// **Why is this bad?** Documenting the type of errors that can be returned from a
/// function can help callers write code to handle the errors appropriately.
///
/// **Known problems:** None.
///
/// **Examples:**
///
/// Since the following function returns a `Result` it has an `# Errors` section in
/// its doc comment:
///
/// ```rust
///# use std::io;
/// /// # Errors
/// ///
/// /// Will return `Err` if `filename` does not exist or the user does not have
/// /// permission to read it.
/// pub fn read(filename: String) -> io::Result<String> {
/// unimplemented!();
/// }
/// ```
pub MISSING_ERRORS_DOC,
pedantic,
"`pub fn` returns `Result` without `# Errors` in doc comment"
}
declare_clippy_lint! {
/// **What it does:** Checks for `fn main() { .. }` in doctests
///
@ -114,7 +143,7 @@ impl DocMarkdown {
}
}
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC, NEEDLESS_DOCTEST_MAIN]);
impl_lint_pass!(DocMarkdown => [DOC_MARKDOWN, MISSING_SAFETY_DOC, MISSING_ERRORS_DOC, NEEDLESS_DOCTEST_MAIN]);
impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
fn check_crate(&mut self, cx: &LateContext<'a, 'tcx>, krate: &'tcx hir::Crate) {
@ -122,20 +151,10 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
}
fn check_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::Item) {
if check_attrs(cx, &self.valid_idents, &item.attrs) {
return;
}
// no safety header
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
match item.kind {
hir::ItemKind::Fn(ref sig, ..) => {
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
span_lint(
cx,
MISSING_SAFETY_DOC,
item.span,
"unsafe function's docs miss `# Safety` section",
);
}
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
},
hir::ItemKind::Impl(_, _, _, _, ref trait_ref, ..) => {
self.in_trait_impl = trait_ref.is_some();
@ -151,40 +170,51 @@ impl<'a, 'tcx> LateLintPass<'a, 'tcx> for DocMarkdown {
}
fn check_trait_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::TraitItem) {
if check_attrs(cx, &self.valid_idents, &item.attrs) {
return;
}
// no safety header
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
if let hir::TraitItemKind::Method(ref sig, ..) = item.kind {
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
span_lint(
cx,
MISSING_SAFETY_DOC,
item.span,
"unsafe function's docs miss `# Safety` section",
);
}
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
}
}
fn check_impl_item(&mut self, cx: &LateContext<'a, 'tcx>, item: &'tcx hir::ImplItem) {
if check_attrs(cx, &self.valid_idents, &item.attrs) || self.in_trait_impl {
let headers = check_attrs(cx, &self.valid_idents, &item.attrs);
if self.in_trait_impl {
return;
}
// no safety header
if let hir::ImplItemKind::Method(ref sig, ..) = item.kind {
if cx.access_levels.is_exported(item.hir_id) && sig.header.unsafety == hir::Unsafety::Unsafe {
span_lint(
cx,
MISSING_SAFETY_DOC,
item.span,
"unsafe function's docs miss `# Safety` section",
);
}
lint_for_missing_headers(cx, item.hir_id, item.span, sig, headers);
}
}
}
fn lint_for_missing_headers<'a, 'tcx>(
cx: &LateContext<'a, 'tcx>,
hir_id: hir::HirId,
span: impl Into<MultiSpan> + Copy,
sig: &hir::FnSig,
headers: DocHeaders,
) {
if !cx.access_levels.is_exported(hir_id) {
return; // Private functions do not require doc comments
}
if !headers.safety && sig.header.unsafety == hir::Unsafety::Unsafe {
span_lint(
cx,
MISSING_SAFETY_DOC,
span,
"unsafe function's docs miss `# Safety` section",
);
}
if !headers.errors && match_type(cx, return_ty(cx, hir_id), &paths::RESULT) {
span_lint(
cx,
MISSING_ERRORS_DOC,
span,
"docs for function returning `Result` missing `# Errors` section",
);
}
}
/// Cleanup documentation decoration (`///` and such).
///
/// 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<(
panic!("not a doc-comment: {}", comment);
}
pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> bool {
#[derive(Copy, Clone)]
struct DocHeaders {
safety: bool,
errors: bool,
}
fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String>, attrs: &'a [Attribute]) -> DocHeaders {
let mut doc = String::new();
let mut spans = vec![];
@ -255,7 +291,11 @@ pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String
doc.push_str(&comment);
} else if attr.check_name(sym!(doc)) {
// ignore mix of sugared and non-sugared doc
return true; // don't trigger the safety check
// don't trigger the safety or errors check
return DocHeaders {
safety: true,
errors: true,
};
}
}
@ -267,7 +307,10 @@ pub fn check_attrs<'a>(cx: &LateContext<'_, '_>, valid_idents: &FxHashSet<String
}
if doc.is_empty() {
return false;
return DocHeaders {
safety: false,
errors: false,
};
}
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
valid_idents: &FxHashSet<String>,
events: Events,
spans: &[(usize, Span)],
) -> bool {
) -> DocHeaders {
// true if a safety header was found
use pulldown_cmark::Event::*;
use pulldown_cmark::Tag::*;
let mut safety_header = false;
let mut headers = DocHeaders {
safety: false,
errors: false,
};
let mut in_code = false;
let mut in_link = None;
let mut in_heading = false;
@ -323,7 +369,8 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
// text "http://example.com" by pulldown-cmark
continue;
}
safety_header |= in_heading && text.trim() == "Safety";
headers.safety |= in_heading && text.trim() == "Safety";
headers.errors |= in_heading && text.trim() == "Errors";
let index = match spans.binary_search_by(|c| c.0.cmp(&range.start)) {
Ok(o) => o,
Err(e) => e - 1,
@ -340,7 +387,7 @@ fn check_doc<'a, Events: Iterator<Item = (pulldown_cmark::Event<'a>, Range<usize
},
}
}
safety_header
headers
}
fn check_code(cx: &LateContext<'_, '_>, text: &str, span: Span) {

View file

@ -488,6 +488,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
&derive::DERIVE_HASH_XOR_EQ,
&derive::EXPL_IMPL_CLONE_ON_COPY,
&doc::DOC_MARKDOWN,
&doc::MISSING_ERRORS_DOC,
&doc::MISSING_SAFETY_DOC,
&doc::NEEDLESS_DOCTEST_MAIN,
&double_comparison::DOUBLE_COMPARISONS,
@ -1013,6 +1014,7 @@ pub fn register_plugins(store: &mut lint::LintStore, sess: &Session, conf: &Conf
LintId::of(&default_trait_access::DEFAULT_TRAIT_ACCESS),
LintId::of(&derive::EXPL_IMPL_CLONE_ON_COPY),
LintId::of(&doc::DOC_MARKDOWN),
LintId::of(&doc::MISSING_ERRORS_DOC),
LintId::of(&empty_enum::EMPTY_ENUM),
LintId::of(&enum_glob_use::ENUM_GLOB_USE),
LintId::of(&enum_variants::MODULE_NAME_REPETITIONS),

View file

@ -6,7 +6,7 @@ pub use lint::Lint;
pub use lint::LINT_LEVELS;
// begin lint list, do not remove this comment, its used in `update_lints`
pub const ALL_LINTS: [Lint; 338] = [
pub const ALL_LINTS: [Lint; 339] = [
Lint {
name: "absurd_extreme_comparisons",
group: "correctness",
@ -1127,6 +1127,13 @@ pub const ALL_LINTS: [Lint; 338] = [
deprecation: None,
module: "missing_doc",
},
Lint {
name: "missing_errors_doc",
group: "pedantic",
desc: "`pub fn` returns `Result` without `# Errors` in doc comment",
deprecation: None,
module: "doc",
},
Lint {
name: "missing_inline_in_public_items",
group: "restriction",

64
tests/ui/doc_errors.rs Normal file
View file

@ -0,0 +1,64 @@
#![warn(clippy::missing_errors_doc)]
use std::io;
pub fn pub_fn_missing_errors_header() -> Result<(), ()> {
unimplemented!();
}
/// This is not sufficiently documented.
pub fn pub_fn_returning_io_result() -> io::Result<()> {
unimplemented!();
}
/// # Errors
/// A description of the errors goes here.
pub fn pub_fn_with_errors_header() -> Result<(), ()> {
unimplemented!();
}
/// This function doesn't require the documentation because it is private
fn priv_fn_missing_errors_header() -> Result<(), ()> {
unimplemented!();
}
pub struct Struct1;
impl Struct1 {
/// This is not sufficiently documented.
pub fn pub_method_missing_errors_header() -> Result<(), ()> {
unimplemented!();
}
/// # Errors
/// A description of the errors goes here.
pub fn pub_method_with_errors_header() -> Result<(), ()> {
unimplemented!();
}
/// This function doesn't require the documentation because it is private.
fn priv_method_missing_errors_header() -> Result<(), ()> {
unimplemented!();
}
}
pub trait Trait1 {
/// This is not sufficiently documented.
fn trait_method_missing_errors_header() -> Result<(), ()>;
/// # Errors
/// A description of the errors goes here.
fn trait_method_with_errors_header() -> Result<(), ()>;
}
impl Trait1 for Struct1 {
fn trait_method_missing_errors_header() -> Result<(), ()> {
unimplemented!();
}
fn trait_method_with_errors_header() -> Result<(), ()> {
unimplemented!();
}
}
fn main() {}

View file

@ -0,0 +1,34 @@
error: docs for function returning `Result` missing `# Errors` section
--> $DIR/doc_errors.rs:5:1
|
LL | / pub fn pub_fn_missing_errors_header() -> Result<(), ()> {
LL | | unimplemented!();
LL | | }
| |_^
|
= note: `-D clippy::missing-errors-doc` implied by `-D warnings`
error: docs for function returning `Result` missing `# Errors` section
--> $DIR/doc_errors.rs:10:1
|
LL | / pub fn pub_fn_returning_io_result() -> io::Result<()> {
LL | | unimplemented!();
LL | | }
| |_^
error: docs for function returning `Result` missing `# Errors` section
--> $DIR/doc_errors.rs:29:5
|
LL | / pub fn pub_method_missing_errors_header() -> Result<(), ()> {
LL | | unimplemented!();
LL | | }
| |_____^
error: docs for function returning `Result` missing `# Errors` section
--> $DIR/doc_errors.rs:47:5
|
LL | fn trait_method_missing_errors_header() -> Result<(), ()>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to 4 previous errors