Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add lint for pub fns returning a Result without documenting errors #4884

Merged
merged 1 commit into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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