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

Generate From impls #2

Closed
dtolnay opened this issue Oct 9, 2019 · 18 comments · Fixed by #30
Closed

Generate From impls #2

dtolnay opened this issue Oct 9, 2019 · 18 comments · Fixed by #30

Comments

@dtolnay
Copy link
Owner

dtolnay commented Oct 9, 2019

Error types commonly provide From impls so that they can be constructed by ?.

I don't want to become a fully general From derive, but I would be okay with providing a convenient From impl just for sources. Any From impl other than from the source should be handwritten for visibility or using something like derive_more.

#[derive(Error, Debug)]
enum EnumError {
    Example(#[from] io::Error),
}
@dtolnay
Copy link
Owner Author

dtolnay commented Oct 9, 2019

For the case of lots of variants that all want a From impl there could be an enum-level attribute to opt in to all of them.

#[derive(Error, Debug)]
#[error(from_source)]
pub enum E {
    E0(#[source] io::Error),
    E1(#[source] serde_json::Error),
    E2(#[source] regex::Error),
    E3(#[source] hyper::Error),
    E4(#[source] anyhow::Error),
}

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 9, 2019

@hwchen
Copy link

hwchen commented Oct 10, 2019

I've been poking at this a little; for structs, should this only work if there's one field (which is source)? Also curious if you're open to a PR for this.

@mathstuf
Copy link
Contributor

The behavior of the attributes (AFAICT) is that "first found wins", so the first found with the attribute or with the right name would be found as the source bit.

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 10, 2019

"First found wins" is not intended behavior, I filed #6 to fix it. The From should work if there is exactly one field which is the source, or two fields source and backtrace in which case a backtrace should be captured from within the From impl.

@hwchen
Copy link

hwchen commented Oct 10, 2019

Thanks for the clarification @dtolnay. I know there's some issues to triage, but I do plan to try my hand at implementing some of these fixes anyhow to learn more about macros, so let me know if I can be of help.

@mathstuf my question above was not directed as much at knowing which field to to use; but rather about how in the From impl it's hard to know how to fill in all the other non-source (and non-backtrace) fields.

@mathstuf
Copy link
Contributor

Oh, that's a good point. Maybe From should only be implemented for source-as-only-field. Probably easier for enum than struct too, but you have to be careful that there's only a single instance of that source type in any variant. That's another reason to not do From by default. I'd be surprised if this kind of stuff doesn't end up as another case study in https://github.com/dtolnay/case-studies in all the ways Rust code can tickle assumptions for macro writers ;) .

@hwchen
Copy link

hwchen commented Oct 10, 2019

Just a note that there may be multiple enum variants with the same source error type. In this case, I guess the ergonomic thing to do is:

  • when doing per-variant opt-in, double check all variant source types to make sure there's no overlap.
  • when doing a 'global' opt-in for all variants, have an additional attr that opts-out (#[no_from]), or just error out.

Or, the derive could just report an error and ask the user to manually impl From

It looks like err-derive tends towards allowing more macro machinery to work it out ergonomically. https://old.reddit.com/r/rust/comments/dfkwfo/announcing_thiserror_a_convenient_modern/f34n4hs/

It seems like you're trying to keep thiserror minimal, so I'm curious to know if adding all this logic is part of the project's scope.

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 10, 2019

  • when doing per-variant opt-in, double check all variant source types to make sure there's no overlap.

+1, it should produce a targeted error underneath one of the pair of overlapping source types.

  • when doing a 'global' opt-in for all variants, have an additional attr that opts-out (#[no_from]), or just error out.

I would lean toward erroring out. I don't want any no_from attribute for now.

@hwchen
Copy link

hwchen commented Oct 11, 2019

@dtolnay, two questions:

  1. I'm having some trouble working out how to do the #[source, from] syntax. Unless I'm missing something, you can't automatically squash multiple attributes together. Can you point me in the right direction?

  2. Would you want to use trybuild now? I've added some new test structs to tests/test_error.rs, and taken a peek using cargo-expand, but I'm starting to think about how to test some of the failure modes.

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 11, 2019

  1. Regarding #[source, from], you are right, it doesn't seem possible to parse this with rustc currently. I think since we plan to only support From for sources, this can be written as just #[from], i.e. #[from] will always imply #[source] implicitly.

    #[derive(Error, Debug)]
    pub struct Error(#[from] io::Error); // io::Error is implicitly the source
  2. Yes absolutely. I filed Set up some trybuild tests to test compile-time diagnostics #11 to track this as it can happen independently.

@MOZGIII
Copy link

MOZGIII commented Oct 11, 2019

If we want #[source, from], how about #[from_source]/#[source_from] to keep #[from] unused for possible better use case in the future?

@JelteF
Copy link

JelteF commented Oct 12, 2019

I'm the author of derive_more. I saw this crate popping up on reddit and I like the way this crate derives the Error trait. Then I saw this issue and I was thinking that another solution would be to combine forces on this. The current master branch of derive_more supports the following From derive:

// Default not from
#[derive(From, Debug)]
enum EnumError {
    #[from] 
    Example(io::Error),
    NotFromable(i32),
}

// Default from
#[derive(From, Debug)]
enum EnumError {
    Example(io::Error),
    #[from(ignore)]
    NotFromable(i32),
}

and together with the Display derive from the crate

#[derive(From, Display)]
#[display(fmt="E prefix: {}")
pub enum E {
    #[display(fmt='io error')]
    E0(io::Error),
    E1(serde_json::Error),
    E2(regex::Error),
    E3(hyper::Error),
    E4(anyhow::Error),
    #[from(ignore)]
    #[display(fmt="wrong count {}", count)]
    SomeCustomError{count: i32},
}

I think it would be very nice if we could combine the good parts of this library and derive_more.

#[derive(From, Display, Error)]
#[display("E prefix: {}")
pub enum E {
    #[display('io error')]
    E0(io::Error),
    E1(serde_json::Error),
    E2(regex::Error),
    E3(hyper::Error),
    E4(anyhow::Error),
    #[from(ignore)]
    #[source(ignore)] // maybe #[error(ignore)]
    #[display("wrong count {count}")]
    SomeCustomError{count: i32, backtrace: Backtrace},
}

@dtolnay what do you think?

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 12, 2019

@JelteF that does not sound promising to me because error objects require a pretty error-specific impl of From when it comes to dealing with backtraces.

@JelteF
Copy link

JelteF commented Oct 12, 2019

I'm not entirely sure what you mean, wouldn't you want to simply forward the backtrace call to the source error?

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 12, 2019

The From impl needs to capture a backtrace if the source error doesn't already have one.

@JelteF
Copy link

JelteF commented Oct 12, 2019

That's definitely a good point. Some options I can think of:

// Specific solution for error, that does auto detection of backtrace field
#[derive(From, Debug)]
enum EnumError {
    #[from(error)] 
    Example{error: io::Error, backtrace: Backtrace),
    NotFromable(i32),
}
// More general, but more verbose
#[derive(From, Debug)]
enum EnumError {
    Example{
       error: io::Error, 
       #[from(default="Backtrace::capture()")] 
       backtrace: Backtrace,
    },
    NotFromable(i32),
}

Because of the way I've implemented attribute handling, the first one would also work easily for the big struct by putting the from attribute on the enum:

#[derive(From, Display, Error)]
#[display("E prefix: {}")
#[from(error)]
pub enum E {
    #[display('io error')]
    E0{source: io::Error, backtrace: Backtrace},
    E1{source: serde_json::Error, backtrace: Backtrace},
    E2{source: regex::Error, backtrace: Backtrace},
    E3{source: hyper::Error, backtrace: Backtrace},
    E4{source: anyhow::Error, backtrace: Backtrace},
    #[from(ignore)]
    #[source(ignore)] // maybe #[error(ignore)]
    #[display("wrong count {count}")]
    SomeCustomError{count: i32, backtrace: Backtrace},
}

I'll think a bit more about this, but I actually like the option above quite a bit.

@dtolnay
Copy link
Owner Author

dtolnay commented Oct 13, 2019

FYI to the people with 👍 reaction above -- @drrlvn @Alexendoo @hwchen @joseluis @CAD97 @NAlexPear @jplatte @mitchmindtree @twistedfall @Jezza @MikailBag @MOZGIII

From impls have been implemented in thiserror 1.0.2!
Here are the release notes: https://github.com/dtolnay/thiserror/releases/tag/1.0.2

dtolnay added a commit that referenced this issue Sep 13, 2022
Currently fails with:

    error[E0034]: multiple applicable items in scope
       --> tests/test_backtrace.rs:165:13
        |
    165 |             x: std::io::Error,
        |             ^ multiple `provide` found
        |
        = note: candidate #1 is defined in an impl of the trait `Provider` for the type `E`
        = note: candidate #2 is defined in an impl of the trait `std::error::Error` for the type `std::io::Error`
    help: disambiguate the associated function for candidate #1
        |
    165 |             Provider::provide(&x, Error): std::io::Error,
        |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    help: disambiguate the associated function for candidate #2
        |
    165 |             std::error::Error::provide(&x, Error): std::io::Error,
        |             ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants