A possible edge case for semver

Inspired by this SO question - rust - Is it bad practice to create a feature-gated enum variant? - Stack Overflow, I'd like to ask the community's opinion: what do you think about adding feature-gated breaking changes to the library?

For the specific example, let's see the (slightly modified) code from question. Imagine there's some library foo, which exports this enum:

pub enum Foo {
    Variant,
}

It's not marked as non-exhaustive, so any downstream crate can match on the value of this enum exhaustively by handling the (only) existing variant.

Now imagine, that foo's maintainers add another variant to Foo, but enable it conditionally based on newly-created (and non-default) feature flag:

pub enum Foo {
    Variant,
    #[cfg(feature = "gate")] GatedVariant,
}

Now, any downstream crates which were using the library before can update without breakage, since by default it will still have only the first variant, and foo's maintainers might assume that they may do semver-compatible version bump.

However, if it was used as a transitive dependency, here's the possibility for breakage to happen indirectly:

  • Library bar used foo before the change and has some code which matched on foo::Foo exhaustively.
  • There's also another library, baz.
  • Some application pulls in both bar and baz.
  • baz adds (possibly private) dependency on foo with feature.
  • bar in the application's dependencies suddenly stops building.

The question is, who is responsible for this breakage? foo, which should treat feature-gated breaking changes as still breaking and bump the major version? Or some of the downstream crates, which could anticipate the change?

It's explicitly not allowed:

A consequence of this is that features should be additive. That is, enabling a feature should not disable functionality, and it should usually be safe to enable any combination of features. A feature should not introduce a SemVer-incompatible change.

I think the downstream crate should not need to anticipate changes, and consequently it cannot be held responsible. A crate should be allowed to enable features of another, 2nd crate, without breaking a different, 3rd crate. If dependencies weren't locally compositional like that, then we would be chasing feature compatibility graphs and resolving conflicts manually all day…

4 Likes

I'd say foo. Consider this variation:

  • Library bar used foo before the change and has some code which matched on foo::Foo exhaustively.
  • There's also another library, baz.
  • Some application pulls in both bar and baz.
  • Some new library quuz adds a dependency on foo with feature. They have no downstream as of yet. It's impossible for them to break SemVer. They were too excited about the Quuz Project to consider this situation when adding the foo dep, they just had a use for the feature.
  • baz adds a dependency on quuz
  • bar in the application's dependencies suddenly stops building.

There's no way to use the feature without breaking something for somebody. The mechanism for avoiding breaking everybody is to bump major version, not add a feature with a warning sticker you hope everyone reads. Even if everyone does, the only way they could be non-breaking is to release their own major version bump, or to opt in to the feature scheme and make their own booby-trapped feature. And if you're a brand-new crate, presumably hoping for adoption, there is no practical version bump option.

That's not something to encourage in the ecosystem.

1 Like

I see, thanks, missed this point - the original question seems to be partially addressed this way, then.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.