crates/nornir-feature-graph/src/facts.rs
//! The parsed shape of one Cargo manifest, and the warehouse ROW it emits.
//!
//! Everything here is derived from `Cargo.toml` text alone. No cargo invocation,
//! no lockfile, no network — so a `PackageFacts` is cheap, deterministic, and
//! testable from a string literal.
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
/// Which dependency section a dep came from.
///
/// `cargo tree -e no-dev` — the oracle this crate is measured against — walks
/// `normal` + `build` and stops at `dev`. Carrying the kind on the row is what
/// lets a consumer reproduce that exact filter instead of guessing.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DepKind {
Normal,
Build,
Dev,
}
impl DepKind {
pub fn as_str(self) -> &'static str {
match self {
DepKind::Normal => "normal",
DepKind::Build => "build",
DepKind::Dev => "dev",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"normal" => Some(DepKind::Normal),
"build" => Some(DepKind::Build),
"dev" => Some(DepKind::Dev),
_ => None,
}
}
}
/// **What one entry in a `[features]` list does.**
///
/// This enum is the reason the crate exists. `cargo metadata` flattens all five
/// of these into "the feature is on", which loses the only distinction that
/// matters here: [`WeakDepFeature`](EnableKind::WeakDepFeature) is the `pkg?/feat`
/// form, and it is the ONLY one that creates an **AND-group** — a dependency
/// that arrives solely under a conjunction of two features and is therefore
/// invisible to any "which single feature pulls this?" query.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum EnableKind {
/// `"dep:x"` — activate the optional dependency `x`, and nothing else.
Dep,
/// `"g"` — turn on this package's own feature `g`.
Feature,
/// `"x/g"` — activate dependency `x` AND turn on `x`'s feature `g`.
/// Activating is implied: the strong form pulls the dep in.
DepFeature,
/// `"x?/g"` — turn on `x`'s feature `g` **only if `x` was already activated
/// by someone else**. Does NOT pull `x` in. This is the AND-group maker.
WeakDepFeature,
/// The feature cargo synthesises for an `optional = true` dependency that no
/// `dep:` syntax ever names. It is not written in the manifest, which is
/// precisely why a text-only reading that skipped it would under-report.
ImplicitDep,
}
impl EnableKind {
pub fn as_str(self) -> &'static str {
match self {
EnableKind::Dep => "dep",
EnableKind::Feature => "feature",
EnableKind::DepFeature => "dep-feature",
EnableKind::WeakDepFeature => "weak-dep-feature",
EnableKind::ImplicitDep => "implicit-dep",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"dep" => Some(EnableKind::Dep),
"feature" => Some(EnableKind::Feature),
"dep-feature" => Some(EnableKind::DepFeature),
"weak-dep-feature" => Some(EnableKind::WeakDepFeature),
"implicit-dep" => Some(EnableKind::ImplicitDep),
_ => None,
}
}
/// Whether this form pulls the dependency INTO the build. The weak form
/// famously does not, and conflating the two is the bug this crate fixes.
pub fn activates_dep(self) -> bool {
matches!(self, EnableKind::Dep | EnableKind::DepFeature | EnableKind::ImplicitDep)
}
}
/// **One row of the `feature_dep_edges` warehouse table** — one edge of the
/// feature activation graph.
///
/// `feature` is the empty string when the package pulls `enables`
/// UNCONDITIONALLY (a plain non-optional dependency). That sentinel is
/// load-bearing: it is how the UI knows a dependency is the ⊥ case — present in
/// every build, removable by no feature click at all — as opposed to merely
/// having no *known* cause, which would be a data gap wearing the same clothes.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FeatureDepEdgeRow {
/// The repo the manifest was found in.
pub repo: String,
/// The package DECLARING the feature.
pub crate_name: String,
/// The feature name, or `""` for an unconditional pull.
pub feature: String,
/// What it enables: a dependency name, a feature name, or `dep/feat`.
pub enables: String,
pub kind: EnableKind,
/// Whether `enables` names an `optional = true` dependency.
pub optional: bool,
/// The `[target.'cfg(...)']` predicate this edge sits under; `None` when
/// unconditional.
pub target_cfg: Option<String>,
pub dep_kind: DepKind,
}
/// One dependency as declared, after `package = ` renames are resolved.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DepSpec {
/// The name the manifest refers to it BY — the table key, which is the
/// rename when `package = "..."` is used. Feature syntax (`dep:x`, `x/g`)
/// always speaks this name, never the real one.
pub key: String,
/// The real package name on the registry. Equal to `key` unless renamed.
pub package: String,
pub optional: bool,
pub default_features: bool,
/// Features requested of the dependency at the declaration site.
pub features: Vec<String>,
pub kind: DepKind,
pub target_cfg: Option<String>,
}
/// **Everything one `Cargo.toml` says about features and dependencies.**
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PackageFacts {
/// **The universe key.** `name` alone cannot be the key: a lockfile holds
/// several versions of the same crate and their manifests disagree about
/// their own dependencies. This is `"<name> <version>"` for anything loaded
/// from a lockfile, and plain `name` otherwise.
pub id: String,
/// The package name, as a human and `cargo tree` say it. This is what goes
/// on a warehouse row — a row keyed by a version-qualified id would not join
/// against any other table in the warehouse.
pub name: String,
pub version: String,
pub repo: String,
/// The raw `[features]` table, verbatim.
pub features: BTreeMap<String, Vec<String>>,
/// Deps keyed by the name the manifest refers to them by. A `(key, kind)`
/// pair is the real identity — the same crate can be a normal dep and a
/// build dep with different feature requests.
pub deps: BTreeMap<(String, DepKind), DepSpec>,
}
impl PackageFacts {
/// The optional deps for which cargo synthesises an implicit feature: those
/// no `dep:` token anywhere in the feature table names.
///
/// Cargo's rule, exactly: the moment ANY feature says `dep:x`, the implicit
/// feature `x` stops existing. Getting this backwards invents a feature that
/// cannot be enabled, or drops one that can.
pub fn implicit_dep_features(&self) -> BTreeSet<String> {
let mut named_by_dep_syntax: BTreeSet<&str> = BTreeSet::new();
for list in self.features.values() {
for tok in list {
if let Some(rest) = tok.strip_prefix("dep:") {
named_by_dep_syntax.insert(rest);
}
}
}
self.deps
.values()
.filter(|d| d.optional && !named_by_dep_syntax.contains(d.key.as_str()))
.map(|d| d.key.clone())
.collect()
}
/// Every feature that can be turned on from outside: the declared ones plus
/// the implicit optional-dep ones.
pub fn all_feature_names(&self) -> BTreeSet<String> {
let mut out: BTreeSet<String> = self.features.keys().cloned().collect();
out.extend(self.implicit_dep_features());
out
}
/// Look a dep up by the name feature syntax uses, preferring `normal` — the
/// section a `dep:`/`x/g` token means when the same key appears twice.
pub fn dep_by_key(&self, key: &str) -> Option<&DepSpec> {
for k in [DepKind::Normal, DepKind::Build, DepKind::Dev] {
if let Some(d) = self.deps.get(&(key.to_string(), k)) {
return Some(d);
}
}
None
}
/// **The rows this manifest contributes to `feature_dep_edges`.**
///
/// This is the ONE writer: the warehouse rows and the in-memory graph are
/// both built from this, so a table row and a rendered edge cannot disagree
/// about what the manifest said.
pub fn edges(&self) -> Vec<FeatureDepEdgeRow> {
let mut out = Vec::new();
let implicit = self.implicit_dep_features();
// ── Feature-conditional edges ────────────────────────────────────────
for (feature, list) in &self.features {
for tok in list {
let (kind, enables) = classify(tok);
let dep_key = dep_key_of(kind, &enables);
let dep = dep_key.as_deref().and_then(|k| self.dep_by_key(k));
out.push(FeatureDepEdgeRow {
repo: self.repo.clone(),
crate_name: self.name.clone(),
feature: feature.clone(),
enables,
kind,
optional: dep.map(|d| d.optional).unwrap_or(false),
target_cfg: dep.and_then(|d| d.target_cfg.clone()),
dep_kind: dep.map(|d| d.kind).unwrap_or(DepKind::Normal),
});
}
}
// ── The implicit optional-dep features ───────────────────────────────
for key in &implicit {
let dep = self.dep_by_key(key);
out.push(FeatureDepEdgeRow {
repo: self.repo.clone(),
crate_name: self.name.clone(),
feature: key.clone(),
enables: key.clone(),
kind: EnableKind::ImplicitDep,
optional: true,
target_cfg: dep.and_then(|d| d.target_cfg.clone()),
dep_kind: dep.map(|d| d.kind).unwrap_or(DepKind::Normal),
});
}
// ── Unconditional pulls: `feature = ""`, the ⊥ case ──────────────────
for dep in self.deps.values() {
if dep.optional {
continue;
}
out.push(FeatureDepEdgeRow {
repo: self.repo.clone(),
crate_name: self.name.clone(),
feature: String::new(),
enables: dep.key.clone(),
kind: EnableKind::Dep,
optional: false,
target_cfg: dep.target_cfg.clone(),
dep_kind: dep.kind,
});
}
out.sort();
out.dedup();
out
}
}
/// Split a `[features]` token into its kind and its payload.
///
/// The order of these checks is the semantics: `x?/g` must be tested before
/// `x/g`, and `dep:x` before the bare-feature fallback.
pub fn classify(tok: &str) -> (EnableKind, String) {
if let Some(rest) = tok.strip_prefix("dep:") {
return (EnableKind::Dep, rest.to_string());
}
if let Some((lhs, rhs)) = tok.split_once('/') {
if let Some(dep) = lhs.strip_suffix('?') {
return (EnableKind::WeakDepFeature, format!("{dep}/{rhs}"));
}
return (EnableKind::DepFeature, format!("{lhs}/{rhs}"));
}
(EnableKind::Feature, tok.to_string())
}
/// The dependency name inside an `enables` payload, if the kind names one.
pub fn dep_key_of(kind: EnableKind, enables: &str) -> Option<String> {
match kind {
EnableKind::Dep | EnableKind::ImplicitDep => Some(enables.to_string()),
EnableKind::DepFeature | EnableKind::WeakDepFeature => {
enables.split_once('/').map(|(d, _)| d.to_string())
}
EnableKind::Feature => None,
}
}
/// The feature name inside a `dep/feat` payload.
pub fn dep_feature_of(enables: &str) -> Option<&str> {
enables.split_once('/').map(|(_, f)| f)
}