Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

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)
}