Idea: Suggesting Trait impls guarded by feature flags

I recently struggled with using serde in conjunction with other crates whose types I wanted to serialize, and the rabbit hole I found myself going down would have been averted if rustc could have alerted me to the crates' feature flags that defined the appropriate traits that E0277 was complaining about.

It seems to me like feature flags are often not documented or poorly documented, including on docs.rs where they're hidden behind a small link in the top bar. Meanwhile the #[unstable] attribute and similar automatically annotate docs. Could that also apply to the #[cfg()] attribute, even when it applies at the mod level? Or add something to #[doc()], or a new attribute entirely? Autodetecting the relationship between feature flags and Trait impls would, I think, enable compiler suggestions as well as make better automatic docs for each feature flag a crate exports.

Integrating cfg into rustdoc is a WIP feature:

As an example chrono uses this on its serde implementations when building on docs.rs, resulting in the "Available on crate feature serde only" annotation, DateTime in chrono - Rust

2 Likes

Feature flags are fundamentally a pain to use like this, because they're conditional compilation -- the compiler often can't know that there's a trait under there that could match, since it can't compile it enough to even get the traits, since it needs types and such that don't exist.

Anything that can be phrased as a trait bound instead will be much easier for rustc to give useful suggestions about. But I guess the serde problems were on types from an optional dependency?

Yeah, it might have to be an annotation put somewhere used by default that the compiler can see without actually having types, maybe a more verbose #[cfg] entry like the doc(cfg) WIP is doing--the compiler could see information there about what it's not compiling. Or some other metadata attribute/information just for feature details, say

feature serde {
    impl<'a, T> Serialize for MyType<'a, T> where T: Serialize;
}

(...ugh, I just reinvented C header files...)

I guess the serde problems were on types from an optional dependency?

The "serde" feature hid an entire module that was implementing types from the optional dependency, so yes.

The specific case of serde impls might also be more directly served by some sort of "peer dependency feature," where the serde dependency+feature can be enabled in crates automatically if the serde crate is somehow otherwise included in the resolved dependency graph.

Making this a realistically solvable problem relies on three things:

  • Such a "peer dependency features" on the crate immediately and always results in the dependency crate version being included in the version resolution tree, even if it gets stripped out again at the end without a proper dependency edge.
  • The feature exclusively enables the peer dependency and no other crate dependencies. (This means you wouldn't be able to conditionally include e.g. serde_with off of your serde feature, unfortunately, or if you're still using serde_derive instead of through the serde reexport.)
  • The entire tree is resolved before compiling any crates, such that compilation of the crate with a peer dependency can look at whether the dependency was provided to decide if the feature is enabled rather than being told by the build tree's set feature flags.
    • I'm not certain how this would impact incremental updates to the lockfile tree; we'd likely have to keep a list of possible, unfulfilled peer dependencies, and then requeue rebuilds as if the version changed even if it didn't and just went from soft/disabled to hard/enabled.

Some of those might be partially relaxable with clever version solving, but this makes it somewhat realistic to add without too much adjustment to the existing version resolution to handle the information backflow.

It should also be noted the ecosystem cost added by rolling adoption of such a feature, where some crates will automatically enable their serde impls when appropriate and some will still require a deliberate feature flag toggle.

Additionally, there's the downside of crates in the tree with serde features but not actually accessible to any use of serde downstream which will end up compiling their serde impls that might not've in the compilation tree with explicit flag toggles. The implicit assumption is that the cost of the gated code tends to be relatively small, and the dependency is optional to cut the dependency, not specifically the code using the dependency.

I can imagine a way we can handle this in one common case: as long as the syntax underneath the cfg is something that rustc can parse and handle (e.g. you're not using cfg to hide new features from old rust, or to hide code that won't compile on your target), we could have something like #[cfg_transparent(...)]. rustc can treat that as "even if the cfg doesn't match, parse what's inside and remember it but don't allow it to be used except by diagnostic suggestions".

This still doesn't quite work on its own though, because the serde crate isn't available since the feature is inactive, so the best interpretation of the inactive code will be the use of some unresolved item, unless you're going to optimistically provide the serde crate anyway since it's in tree even though the dependency isn't active.

You could even theoretically do the speculative interpretation for regular #[cfg] as well up to any module or macro boundaries, since those are already still required[1] to be syntax valid. This might actually be useful if something's accidentally been defined in the typical #[cfg(test)] mod test.

A transparent variant of cfg (and cfg_attr, realistically) would only be necessary if descending into modules or (derive) macros is desired. (Inactive non-inert attribute macros are probably impossible to do this with, though.)

The "perfect" solution is of course to invent some new conditional compilation system which handles optional dependencies (when as an assumed-to-be-valid dependency; in local development it's actually more ideal to always include the dependency if both configurations with and without are verified simultaneously) without relying just on basic textual exclusion of the inactive code. But that's a Hard problem that I don't know if any language has fully solved. (Some have made meaningful progress when optional dependencies aren't involved, but the involvement of optional dependencies makes the problems more involved. A hypothetical where const { cfg!(feature="serde")) } bound gets a good start into the former and should nicely illustrate the additional problems involved with the latter.)


  1. To be pedantic, there's a small set of unstable syntax errors which are only future compatibly warnings when cfg'd out. I was the one who made them warn :slightly_smiling_face: ↩︎

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.