Skip to content

Commit

Permalink
Add lint for pub fns returning a Result without documenting errors
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
RobbieClarken committed Dec 6, 2019
1 parent ff1607e commit f5d0a45
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 47 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
137 changes: 92 additions & 45 deletions clippy_lints/src/doc.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -45,7 +45,7 @@ declare_clippy_lint! {
///
/// **Known problems:** None.
///
/// **Examples**:
/// **Examples:**
/// ```rust
///# type Universe = ();
/// /// This function should really be documented
Expand All @@ -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
///
Expand Down Expand Up @@ -114,28 +143,18 @@ 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) {
check_attrs(cx, &self.valid_idents, &krate.attrs);
}

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();
Expand All @@ -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
Expand Down Expand Up @@ -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![];

Expand All @@ -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,
};
}
}

Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions clippy_lints/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
Expand Down
9 changes: 8 additions & 1 deletion src/lintlist/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub use lint::Lint;
pub use lint::LINT_LEVELS;

// begin lint list, do not remove this comment, it’s used in `update_lints`
pub const ALL_LINTS: [Lint; 338] = [
pub const ALL_LINTS: [Lint; 339] = [
Lint {
name: "absurd_extreme_comparisons",
group: "correctness",
Expand Down Expand Up @@ -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",
Expand Down
64 changes: 64 additions & 0 deletions tests/ui/doc_errors.rs
Original file line number Diff line number Diff line change
@@ -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() {}
34 changes: 34 additions & 0 deletions tests/ui/doc_errors.stderr
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit f5d0a45

Please sign in to comment.