Skip to content

Disagreement with missing_panics_doc changes regarding expect #11436

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

Open
ecton opened this issue Aug 30, 2023 · 12 comments
Open

Disagreement with missing_panics_doc changes regarding expect #11436

ecton opened this issue Aug 30, 2023 · 12 comments
Labels
C-enhancement Category: Enhancement of lints, like adding more cases or adding help messages

Comments

@ecton
Copy link

ecton commented Aug 30, 2023

Summary

In #10953, missing_panics_doc was updated to include reporting on Option::expect and Result::expect. This has caused a large number of warnings across my projects, and I wanted to start a discussion over the changes.

To me, these two approaches to asserting that an option shouldn't be None are identical from a programmer's intent:

pub fn example(opt: Option<()>) {
    match opt {
        Some(_) => {}
        None => unreachable!("invalid"),
    }

    opt.expect("invalid")
}

Despite the first match statement possibly panicking, no warning is given. Yet, with the new behavior, opt.expect() now warns about possible panics. To me, my intention as a programmer between these two approaches is identical.

The net result of this change is that I'm going to be forced into an ugly pattern in my code to avoid this false positive. Could we split the warnings for panicking on expect into its own lint so that we can keep the previous behavior?

Lint Name

missing_panics_doc

Reproducer

I tried this code:

pub fn example(opt: Option<()>) {
    opt.expect("invalid")
}

I saw this happen:

warning: docs for function which may panic missing `# Panics` section
   --> src/shapes.rs:442:5
    |
442 |     pub fn test(i: Option<()>) {
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^
    |
note: first possible panic found here
   --> src/shapes.rs:448:9
    |
448 |         i.expect("invalid");
    |         ^^^^^^^^^^^^^^^^^^^
    = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#missing_panics_doc

I expected to see this happen: No warnings

Version

rustc 1.72.0 (5680fa18f 2023-08-23)
binary: rustc
commit-hash: 5680fa18feaa87f3ff04063800aec256c3d4b4be
commit-date: 2023-08-23
host: x86_64-unknown-linux-gnu
release: 1.72.0
LLVM version: 16.0.5

Additional Labels

No response

@ecton ecton added C-bug Category: Clippy is not doing the correct thing I-false-positive Issue: The lint was triggered on code it shouldn't have labels Aug 30, 2023
@PKilcommons
Copy link

I have similar reservations about this change so I figure I'd chime in.

Recently saw it show up in CI and was a bit surprised. While my line of thought on this matter may be potentially unidiomatic, I consider usages of .except() to be similar to // SAFETY comments for unsafe blocks in the body of a safe function—dev-to-dev explanation of an invariant being upheld in a way the compile cannot infer. A panic is then a result of that invariant being broken inside the function body, not as a result of the input to the function. As such this is then a bug that needs to be fixed within the body of the function, not at the callsite.

In this case, the # Panics section ends up just becoming pointless noise in the doc comment simply repeating the message from the .except(), something the caller has zero control over.

To illustrate with a contrived function:

/// Return a `String` pulled out of the byte stream.
pub fn string_from_byte_stream() -> String {
    let bytes = get_valid_utf8();
    String::from_utf8(bytes).except("`get_valid_utf8()` always returns valid UTF-8")
}

fn get_valid_utf8() -> Vec<u8> {
    /* Gets some bytes that the dev knows is always valid UTF-8 */
}

To appease the lint now:

/// Return a `String` pulled out of the byte stream.
/// # Panics
/// Panics if the byte stream is not valid UTF-8.
pub fn string_from_byte_stream() -> String {
    /* ... */ 
}

This doesn't exactly help the caller prevent or avoid a panic in anyway.

Perhaps adding a clippy.toml config variable that allows the .except() case would also be a desirable alternative to a new lint?

@oxalica
Copy link
Contributor

oxalica commented Sep 8, 2023

Running into this recently. I use .expect() and/or .unwrap() interchangebly for already-checked-conditions or invariants that another library/code guarantee. So if it really panics, it's my fault or upstream library's fault. It's meaningless for downstream users to know in documentations that it may panic, because the end users cannot control or fix it at all.

cc the author of that change @KisaragiEffective

@Alexendoo Alexendoo added C-enhancement Category: Enhancement of lints, like adding more cases or adding help messages and removed I-false-positive Issue: The lint was triggered on code it shouldn't have C-bug Category: Clippy is not doing the correct thing labels Sep 8, 2023
@Alexendoo
Copy link
Member

A list of methods/macro names to ignore seems like a good config option to add. There have been several different incompatible conventions mentioned for what unwrap/expect mean so that seems like the best way forward

@ModProg
Copy link

ModProg commented Nov 4, 2023

@Alexendoo

A list of methods/macro names to ignore seems like a good config option to add.

What should the config be? How should it handle paths, e.g. std::*, core::*,

Initial idea, though one could also consider an allowlist instead:

panics_requiring_doc = ["Option::unwrap", "Option::expect", "Result::unwrap", "Result::expect", "panic!", "assert!", "assert_eq!", "assert_ne!"]

One issue is that the detection is implemented quite differently:

/// Is `def_id` of `std::panic`, `core::panic` or any inner implementation macros
pub fn is_panic(cx: &LateContext<'_>, def_id: DefId) -> bool {
let Some(name) = cx.tcx.get_diagnostic_name(def_id) else {
return false;
};
matches!(
name,
sym::core_panic_macro
| sym::std_panic_macro
| sym::core_panic_2015_macro
| sym::std_panic_2015_macro
| sym::core_panic_2021_macro
)
}

|| matches!(
self.cx.tcx.item_name(macro_call.def_id).as_str(),
"assert" | "assert_eq" | "assert_ne"
)

and

// check for `unwrap` and `expect` for both `Option` and `Result`
if let Some(arglists) = method_chain_args(expr, &["unwrap"]).or(method_chain_args(expr, &["expect"])) {
let receiver_ty = self.typeck_results.expr_ty(arglists[0].0).peel_refs();
if is_type_diagnostic_item(self.cx, receiver_ty, sym::Option)
|| is_type_diagnostic_item(self.cx, receiver_ty, sym::Result)

@Alexendoo
Copy link
Member

Personally I would go with a list of paths to ignore as that's more common for clippy configuration, you could take inspiration from disallowed_macros/disallowed_methods

@ModProg
Copy link

ModProg commented Nov 5, 2023

Personally I would go with a list of paths to ignore as that's more common for clippy configuration, you could take inspiration from disallowed_macros/disallowed_methods

my idea was that one could add their own functions/macros to the lint, but personally I don't have a usecase for that, so just having an ignore list is fine by me

@duelafn
Copy link

duelafn commented Mar 12, 2025

My deepest wish is that I could flag individual lines/blocks as not requiring panic docs. I haven't looked at the lint code and fully expect that what I want might be difficult to impossible, but in the world of dreams, I'd really like to be able to (reusing some above examples),

/// Do something
pub fn string_from_byte_stream() -> String {
    let bytes = get_valid_utf8();
    #[expect(clippy::missing_panics_doc_ok, reason="caller can't do anything about this")]
    String::from_utf8(bytes).expect("`get_valid_utf8()` always returns valid UTF-8")
}

Time passes, function modified,

/// Do something
pub fn string_from_byte_stream(input: i64) -> String {
    let input = u64::try_from(input).expect("non-negative");// Oops, added something that needs "# Panics"

    // ... stuff

    let bytes = get_valid_utf8();
    #[expect(clippy::missing_panics_doc_ok, reason="caller can't do anything about this")]
    String::from_utf8(bytes).expect("`get_valid_utf8()` always returns valid UTF-8")
}

In my dreams the first fn wouldn't lint, but the second one would since it has an untagged panic.

@KisaragiEffective
Copy link
Contributor

you can use the _unchecked variant to eliminate both warning and unnecessary control flow jump, aren't you?

@KisaragiEffective
Copy link
Contributor

Aside that, can we rename this as unexplained_panic and restore old behavior to exclude the expect method?

@ModProg
Copy link

ModProg commented Mar 13, 2025

I started using a crate (https://docs.rs/intentional/latest/intentional/trait.Assert.html) that provides some helper methods that do the same as expect & unwrap so I can differentiate between cases I want to document and cases I don't.

@duelafn
Copy link

duelafn commented Mar 13, 2025

Another example not involving .expect() or .unwrap(),

pub fn do_something(input: &[i64]) {
    if handle_small_vec(input) { return; }

    #[expect(clippy::missing_panics_doc_ok, reason="small vecs handled above")]
    assert!(input.len() > 3, "small vecs handled above, this assert satisfies clippy::missing_asserts_for_indexing");
    do_something_else((input[0] - input[1]) * (input[2] - input[3]));
}

@Alexendoo
Copy link
Member

Opened #14407 for allow/expect within functions

For assert/panic etc it would have to be on a parent expression as attributes directly on macro calls are currently ignored, which may mean an extra block has to be introduced

pub fn do_something(input: &[i64]) {
    if handle_small_vec(input) {
        return;
    }

    #[expect(clippy::missing_panics_doc_ok)]
    {
        assert!(
            input.len() > 3,
            "small vecs handled above, this assert satisfies clippy::missing_asserts_for_indexing"
        );
    }
    do_something_else((input[0] - input[1]) * (input[2] - input[3]));
}

github-merge-queue bot pushed a commit that referenced this issue Mar 31, 2025
…panics_doc` (#14407)

Implements
#11436 (comment)

> [...] I'd really like to be able to (reusing some above examples),
>
> ``` rust
> /// Do something
> pub fn string_from_byte_stream() -> String {
>     let bytes = get_valid_utf8();
> #[expect(clippy::missing_panics_doc_ok, reason="caller can't do
anything about this")]
> String::from_utf8(bytes).expect("`get_valid_utf8()` always returns
valid UTF-8")
> }
> ```

Also fixes an issue where if the first panic is in a `const` context
disables the lint for the rest of the function

The first commit is just moving code around

changelog: [`missing_panics_doc`]: `#[allow]` and `#[expect]` can now be
used within the function body to ignore individual panics
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-enhancement Category: Enhancement of lints, like adding more cases or adding help messages
Projects
None yet
Development

No branches or pull requests

7 participants