Features are typically used to manage dependencies, or to incrementally enable API. Sometimes, however, feature flags are used to modify behavior of APIs otherwise enabled.
Here's why it can be problematic, on the example of time
and its feature serde-human-readable
which enables human-readable parsing and formatting in the serde impls in case when the Serializer
/Deserializer
advertises the format as human-readable.
Consider library a crate with this Cargo.toml
:
[package]
name = "a"
version = "0.1.0"
edition = "2021"
[dependencies]
time = { version = "0.3", features = ["serde"] }
serde_json = "1.0"
It relies on omission of the serde-human-readable
feature to format the output string as a JSON array:
use time::OffsetDateTime;
pub fn json_clock() -> String {
let t = OffsetDateTime::now_utc();
serde_json::to_string(&t).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_as_array() {
let json = json_clock();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(v.is_array());
}
}
Taken on its own, the crate passes the test. Now add it to a dependency tree of another crate that additionally enables serde-human-readable
in time:
[package]
name = "b"
version = "0.1.0"
edition = "2021"
[dependencies]
a = { path = "../a" }
time = { version = "0.3", features = ["serde-human-readable"] }
The binary of this crate prints a JSON string literal:
use a::json_clock;
fn main() {
println!("{}", json_clock());
}
If these crates are put together in a workspace, the test in a
also fails.
This example is a bit contrived, but there are likely other instances where behavior selected by feature omission is liable to be stealthily modified outside of the crate relying on this behavior.
Should such features be considered a footgun? Does it deserve a mention in the API guidelines?