cargo_hackerman/
feat_graph.rs

1use crate::hack::Collect;
2use crate::metadata::{DepKindInfo, Link};
3use cargo_metadata::{Metadata, Package, PackageId, Source};
4use cargo_platform::Cfg;
5use dot::{GraphWalk, Labeller};
6use petgraph::graph::{EdgeIndex, NodeIndex};
7use petgraph::visit::{Dfs, EdgeFiltered, EdgeRef};
8use petgraph::Graph;
9use std::borrow::Cow;
10use std::collections::{BTreeMap, BTreeSet};
11use std::ops::Index;
12use tracing::{debug, error, info, trace};
13
14#[derive(Copy, Clone, Ord, PartialEq, Eq, PartialOrd, Debug)]
15/// An node for feature graph
16pub enum Feature<'a> {
17    /// "root" node, contains links to all the workspace
18    Root,
19    /// Fid is a workspace member
20    Workspace(Fid<'a>),
21    /// Fid is not a workspace member
22    External(Fid<'a>),
23}
24
25impl std::fmt::Display for Feature<'_> {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Feature::Root => f.write_str("root"),
29            Feature::Workspace(fid) | Feature::External(fid) => fid.fmt(f),
30        }
31    }
32}
33
34impl<'a> Feature<'a> {
35    #[must_use]
36    pub const fn fid(&self) -> Option<Fid<'a>> {
37        match self {
38            Feature::Root => None,
39            Feature::Workspace(fid) | Feature::External(fid) => Some(*fid),
40        }
41    }
42
43    #[must_use]
44    pub fn pid(&self) -> Option<Pid<'a>> {
45        self.fid().map(|fid| fid.pid)
46    }
47
48    #[must_use]
49    pub fn package_id(&self) -> Option<&PackageId> {
50        let Pid(pid, meta) = self.pid()?;
51        Some(&meta.packages[pid].id)
52    }
53
54    #[must_use]
55    pub const fn is_workspace(&self) -> bool {
56        match self {
57            Feature::Root | Feature::Workspace(_) => true,
58            Feature::External(_) => false,
59        }
60    }
61}
62
63pub struct FeatGraph<'a> {
64    /// root node, should be 0
65    pub root: NodeIndex,
66    /// set of workspace members
67    pub workspace_members: BTreeSet<Pid<'a>>,
68    /// a dependency graph between features
69    /// Feature = Fid + decoration if it's external, internal or root
70    pub features: Graph<Feature<'a>, Link>,
71    /// A way to look up fids in features
72    fids: BTreeMap<Fid<'a>, NodeIndex>,
73    /// a lookup cache from cargo metadata's PackageId to hackerman's Pid
74    cache: BTreeMap<&'a PackageId, Pid<'a>>,
75
76    pub fid_cache: BTreeMap<Fid<'a>, NodeIndex>,
77
78    /// cargo metadata
79    meta: &'a Metadata,
80
81    pub platforms: Vec<&'a str>,
82    pub cfgs: Vec<Cfg>,
83    pub triggers: Vec<Trigger<'a>>,
84
85    pub focus_nodes: Option<BTreeSet<NodeIndex>>,
86    pub focus_edges: Option<BTreeSet<EdgeIndex>>,
87    pub focus_targets: Option<BTreeSet<NodeIndex>>,
88}
89
90impl<'a> Index<Pid<'a>> for FeatGraph<'a> {
91    type Output = NodeIndex;
92
93    fn index(&self, index: Pid<'a>) -> &Self::Output {
94        &self.fid_cache[&index.root()]
95    }
96}
97
98impl<'a> Index<NodeIndex> for FeatGraph<'a> {
99    type Output = Fid<'a>;
100
101    fn index(&self, index: NodeIndex) -> &Self::Output {
102        match &self.features[index] {
103            Feature::Root => panic!("root node fid"),
104            Feature::Workspace(f) => f,
105            Feature::External(f) => f,
106        }
107    }
108}
109
110#[derive(Debug)]
111pub struct Trigger<'a> {
112    // foo.toml:
113    // [features]
114    // serde = ["dep:serde", "rgb?/serde"]
115    // when both `feature` and `weak_dep` are present we must include `to_add`
116    //
117    // In this example, enabling the serde feature will enable the serde
118    // dependency. It will also enable the serde feature for the rgb
119    // dependency, but only if something else has enabled the rgb
120    // dependency.
121    //
122    pub package: Pid<'a>,   // foo
123    pub feature: Fid<'a>,   // serde
124    pub weak_dep: Fid<'a>,  // rgb
125    pub weak_feat: Fid<'a>, // rgb/serde
126}
127
128impl<'a> FeatGraph<'a> {
129    pub fn fid_index(&mut self, fid: Fid<'a>) -> NodeIndex {
130        *self.fids.entry(fid).or_insert_with(|| {
131            if self.workspace_members.contains(&fid.pid) {
132                self.features.add_node(Feature::Workspace(fid))
133            } else {
134                self.features.add_node(Feature::External(fid))
135            }
136        })
137    }
138
139    /// for any node find node for the base of this package
140    #[must_use]
141    pub fn base_node(&self, node: NodeIndex) -> Option<NodeIndex> {
142        self.fid_cache
143            .get(&self.features[node].fid()?.get_base())
144            .copied()
145    }
146
147    pub fn shrink_to_target(&mut self) -> anyhow::Result<()> {
148        info!("Shrinking to current target");
149        let g = EdgeFiltered::from_fn(&self.features, |e| {
150            e.weight().satisfies(
151                self.features[e.source()],
152                Collect::DevTarget,
153                &self.platforms,
154                &self.cfgs,
155            )
156        });
157        let mut dfs = Dfs::new(&g, self.root);
158        let mut this = BTreeSet::new();
159        while let Some(ix) = dfs.next(&g) {
160            this.insert(ix);
161        }
162
163        self.features.retain_nodes(|_, ix| this.contains(&ix));
164        self.rebuild_cache()?;
165
166        Ok(())
167    }
168
169    pub fn init(
170        meta: &'a Metadata,
171        platforms: Vec<&'a str>,
172        cfgs: Vec<Cfg>,
173    ) -> anyhow::Result<Self> {
174        if meta.resolve.is_none() {
175            anyhow::bail!("Cargo couldn't produce resolved dependencies")
176        }
177
178        let cache = meta
179            .packages
180            .iter()
181            .enumerate()
182            .map(|(ix, package)| (&package.id, Pid(ix, meta)))
183            .collect::<BTreeMap<_, _>>();
184
185        let mut features = Graph::new();
186        let root = features.add_node(Feature::Root);
187
188        let mut graph = Self {
189            workspace_members: meta
190                .workspace_members
191                .iter()
192                .filter_map(|pid| cache.get(pid))
193                .copied()
194                .collect::<BTreeSet<_>>(),
195            features,
196            root,
197            platforms,
198            fids: BTreeMap::new(),
199            triggers: Vec::new(),
200            fid_cache: BTreeMap::new(),
201            cache,
202            meta,
203            cfgs,
204            focus_nodes: None,
205            focus_edges: None,
206            focus_targets: None,
207        };
208
209        for (ix, package) in meta.packages.iter().enumerate() {
210            graph.add_package(ix, package, &meta.packages)?;
211        }
212
213        graph.rebuild_cache()?;
214
215        Ok(graph)
216    }
217
218    pub fn optimize(&mut self, no_transitive: bool) -> anyhow::Result<()> {
219        info!("Optimization pass: trim unused features");
220        self.trim_unused_features();
221
222        if !no_transitive {
223            info!("Optimization pass: transitive reduction");
224            self.transitive_reduction();
225        }
226
227        self.rebuild_cache()?;
228        Ok(())
229    }
230
231    pub fn rebuild_cache(&mut self) -> anyhow::Result<()> {
232        info!("Rebuilding feature id cache");
233        self.fids.clear();
234        self.fid_cache.clear();
235        for node in self.features.node_indices() {
236            if let Some(fid) = self.features[node].fid() {
237                self.fids.insert(fid, node);
238            }
239
240            if let Some(feat) = self.features[node].fid() {
241                self.fid_cache.insert(feat, node);
242            }
243        }
244        Ok(())
245    }
246
247    fn transitive_reduction(&mut self) {
248        use petgraph::algo::tred::dag_to_toposorted_adjacency_list;
249        let graph = &mut self.features;
250        let before = graph.edge_count();
251        let toposort = match petgraph::algo::toposort(&*graph, None) {
252            Ok(t) => t,
253            Err(err) => {
254                error!("Cyclic dependencies are detected {err:?}, skipping transitive reduction");
255                return;
256            }
257        };
258
259        let (adj_list, revmap) =
260            dag_to_toposorted_adjacency_list::<_, NodeIndex>(&*graph, &toposort);
261        let (reduction, _closure) =
262            petgraph::algo::tred::dag_transitive_reduction_closure(&adj_list);
263
264        graph.retain_edges(|x, y| {
265            if let Some((f, t)) = x.edge_endpoints(y) {
266                reduction.contains_edge(revmap[f.index()], revmap[t.index()])
267            } else {
268                false
269            }
270        });
271        let after = graph.edge_count();
272        debug!("Transitive reduction, edges {before} -> {after}");
273    }
274
275    /// Remove features not used by the workspace directly or indirectly
276    ///
277    /// should only be used for displaying
278    fn trim_unused_features(&mut self) {
279        let mut to_remove = Vec::new();
280        loop {
281            for f in self.features.externals(petgraph::EdgeDirection::Incoming) {
282                if let Feature::External(..) = self.features[f] {
283                    to_remove.push(f);
284                }
285            }
286            if to_remove.is_empty() {
287                break;
288            }
289            for f in to_remove.drain(..) {
290                self.features.remove_node(f);
291            }
292        }
293    }
294
295    fn add_package(
296        &mut self,
297        ix: usize,
298        package: &'a Package,
299        packages: &'a [Package],
300    ) -> anyhow::Result<()> {
301        debug!("== adding package {}", package.id);
302        let this = Pid(ix, self.meta);
303        let base_ix = self.fid_index(this.base());
304
305        let workspace_member = self.workspace_members.contains(&this);
306
307        // root contains links to all the workspace members
308        if workspace_member {
309            self.add_edge(self.root, this, false, DepKindInfo::NORMAL)?;
310        }
311
312        // resolve and cache crate dependencies and create a cache mapping name to dep
313        let mut deps = BTreeMap::new();
314        for dep in &package.dependencies {
315            if !workspace_member && dep.kind == cargo_metadata::DependencyKind::Development {
316                trace!("Skipping external dev dependency {dep:?}");
317                continue;
318            }
319
320            let source_matches = |a: Option<&Source>, b: Option<&String>| match (a, b) {
321                (None, None) => true,
322                (Some(a), Some(b)) => {
323                    if &a.repr == b || (a.repr.starts_with("git") && a.repr.starts_with(b)) {
324                        true
325                    } else {
326                        trace!("ignoring a candidate {package:?} for {dep:?} due to source mismatch: {a:?} != {b:?}");
327                        false
328                    }
329                }
330                _ => false,
331            };
332            // get resolved package - should be there in at most one matching copy...
333            let resolved = match packages.iter().find(|p| {
334                p.name == dep.name
335                    && dep.req.matches(&p.version)
336                    && source_matches(p.source.as_ref(), dep.source.as_ref())
337            }) {
338                Some(res) => res,
339                None => {
340                    debug!(
341                        "cargo metadta did not include optional dependency \"{} {}\" \
342                        requested by \"{} {}\", skipping",
343                        dep.name, dep.req, package.name, package.version
344                    );
345                    continue;
346                }
347            };
348
349            // feature dependencies:
350            //
351            // - optional dependencies are linked from named feature
352            // - requred dependenceis are linked fromb base
353            let this = if dep.optional {
354                match dep.rename.as_ref() {
355                    Some(name) => this.named(name).get_index(self)?,
356                    None => this.named(&dep.name).get_index(self)?,
357                }
358            } else {
359                base_ix
360            };
361
362            //  dependencies that have default target are linked to that target
363            //  otherwise dependencies are linked to
364            let remote = if dep.uses_default_features {
365                Some(self.add_edge(this, resolved, false, dep.into())?)
366            } else if let Some(pid) = self.cache.get(&resolved.id) {
367                let fid = pid.base();
368                Some(self.add_edge(this, fid, false, dep.into())?)
369            } else {
370                None
371            };
372            // if additional features on dependency are required - we add them all
373            for feat in &dep.features {
374                self.add_edge(this, (resolved, feat.as_str()), false, dep.into())?;
375            }
376
377            // for remote dependencies we store the resolved ifo in order to deal with renames
378            if let Some(remote) = remote {
379                let name = dep.rename.clone().unwrap_or_else(|| resolved.name.clone());
380                deps.insert(name, (resolved, dep, remote));
381            }
382        }
383
384        for (this_feat, feat_deps) in &package.features {
385            let feat_ix = self.fid_index(this.named(this_feat));
386            self.add_edge(feat_ix, base_ix, false, DepKindInfo::NORMAL)?;
387
388            for feat_dep in feat_deps.iter() {
389                match FeatTarget::from(feat_dep.as_str()) {
390                    FeatTarget::Named { name } => {
391                        self.add_edge(feat_ix, this.named(name), false, DepKindInfo::NORMAL)?;
392                    }
393                    FeatTarget::Dependency { krate } => {
394                        if let Some(&(_dep, link, remote)) = deps.get(krate) {
395                            self.add_edge(feat_ix, remote, true, link.into())?;
396                        } else {
397                            debug!("skipping disabled optional dependency {krate}");
398                        }
399                    }
400                    FeatTarget::Remote { krate, feat } => {
401                        if let Some(&(dep, link, _remote)) = deps.get(krate) {
402                            self.add_edge(feat_ix, (dep, feat), true, link.into())?;
403                        } else {
404                            debug!("skipping disabled optional dependency {krate}");
405                        }
406                    }
407                    FeatTarget::Cond { krate, feat } => {
408                        if let Some(dep) = deps
409                            .get(krate)
410                            .and_then(|&(dep, _link, _remote)| self.cache.get(&dep.id).copied())
411                        {
412                            let trigger = Trigger {
413                                package: this,
414                                feature: this.named(this_feat),
415                                weak_dep: this.named(krate),
416                                weak_feat: dep.named(feat),
417                            };
418                            self.triggers.push(trigger);
419                        } else {
420                            debug!("skipping disabled optional dependency {krate}");
421                        }
422                    }
423                }
424            }
425        }
426
427        Ok(())
428    }
429
430    pub fn add_edge<A, B>(
431        &mut self,
432        a: A,
433        b: B,
434        optional: bool,
435        kind: DepKindInfo,
436    ) -> anyhow::Result<NodeIndex>
437    where
438        A: HasIndex<'a>,
439        B: HasIndex<'a>,
440    {
441        let a = a.get_index(self)?;
442        let b = b.get_index(self)?;
443        trace!(
444            "adding {}edge {a:?} -> {b:?}: {kind:?}\n\t{:?}\n\t{:?}",
445            if optional { "optional " } else { "" },
446            self.features[a],
447            self.features[b]
448        );
449
450        if let Some(index) = self.features.find_edge(a, b) {
451            let old_link = &mut self.features[index];
452            if !old_link.kinds.contains(&kind) {
453                old_link.kinds.push(kind);
454            }
455            old_link.optional &= optional;
456        } else {
457            let link = Link {
458                optional,
459                kinds: vec![kind],
460            };
461            self.features.add_edge(a, b, link);
462        }
463        Ok(b)
464    }
465}
466
467#[derive(Copy, Clone)]
468pub struct Pid<'a>(usize, &'a Metadata);
469
470impl<'a> Pid<'a> {
471    #[must_use]
472    pub fn package(self) -> &'a cargo_metadata::Package {
473        &self.1.packages[self.0]
474    }
475}
476
477impl<'a> Pid<'a> {
478    #[must_use]
479    pub fn root(self) -> Fid<'a> {
480        if self.package().features.contains_key("default") {
481            self.named("default")
482        } else {
483            self.base()
484        }
485    }
486
487    #[must_use]
488    pub const fn base(self) -> Fid<'a> {
489        Fid {
490            pid: self,
491            dep: Feat::Base,
492        }
493    }
494    #[must_use]
495    pub const fn named(self, name: &'a str) -> Fid<'a> {
496        Fid {
497            pid: self,
498            dep: Feat::Named(name),
499        }
500    }
501}
502
503impl<'a> PartialEq for Pid<'a> {
504    fn eq(&self, other: &Self) -> bool {
505        self.0 == other.0
506    }
507}
508
509impl<'a> Eq for Pid<'a> {}
510
511impl<'a> PartialOrd for Pid<'a> {
512    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
513        Some(self.cmp(other))
514    }
515}
516
517impl<'a> Ord for Pid<'a> {
518    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
519        self.0.cmp(&other.0)
520    }
521}
522
523impl std::fmt::Debug for Pid<'_> {
524    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
525        let meta = &self.1.packages[self.0];
526        write!(f, "Pid({} / {})", self.0, meta.id)
527    }
528}
529
530#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
531pub struct Fid<'a> {
532    /// this feature originates from
533    pub pid: Pid<'a>,
534    pub dep: Feat<'a>,
535}
536
537impl std::fmt::Display for Fid<'_> {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        let id = &self.pid.package().id;
540        match self.dep {
541            Feat::Base => write!(f, "{id}"),
542            Feat::Named(name) => write!(f, "{id}:{name}"),
543        }
544    }
545}
546
547impl std::fmt::Display for Feat<'_> {
548    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
549        match self {
550            Feat::Base => f.write_str(":base:"),
551            Feat::Named(name) => f.write_str(name),
552        }
553    }
554}
555
556#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
557pub enum Feat<'a> {
558    /// Base package itself
559    Base,
560    /// internally defined named feature
561    Named(&'a str),
562}
563
564impl<'a> GraphWalk<'a, NodeIndex, EdgeIndex> for FeatGraph<'a> {
565    fn nodes(&'a self) -> dot::Nodes<'a, NodeIndex> {
566        Cow::from(match &self.focus_nodes {
567            Some(f) => f.iter().copied().collect::<Vec<_>>(),
568            None => self.features.node_indices().collect::<Vec<_>>(),
569        })
570    }
571
572    fn edges(&'a self) -> dot::Edges<'a, EdgeIndex> {
573        Cow::from(match &self.focus_edges {
574            Some(f) => f.iter().copied().collect::<Vec<_>>(),
575            None => self.features.edge_indices().collect::<Vec<_>>(),
576        })
577    }
578
579    fn source(&'a self, edge: &EdgeIndex) -> NodeIndex {
580        self.features.edge_endpoints(*edge).unwrap().0
581    }
582
583    fn target(&'a self, edge: &EdgeIndex) -> NodeIndex {
584        self.features.edge_endpoints(*edge).unwrap().1
585    }
586}
587
588impl<'a> Labeller<'a, NodeIndex, EdgeIndex> for FeatGraph<'a> {
589    fn graph_id(&'a self) -> dot::Id<'a> {
590        dot::Id::new("graphname").unwrap()
591    }
592
593    fn node_id(&'a self, n: &NodeIndex) -> dot::Id<'a> {
594        dot::Id::new(format!("n{}", n.index())).unwrap()
595    }
596
597    fn node_shape(&'a self, node: &NodeIndex) -> Option<dot::LabelText<'a>> {
598        let fid = self.features[*node].fid()?;
599        match fid.dep {
600            Feat::Base => Some(dot::LabelText::label("octagon")),
601            Feat::Named(_) => None,
602        }
603    }
604
605    fn node_label(&'a self, n: &NodeIndex) -> dot::LabelText<'a> {
606        let mut fmt = String::new();
607        match self.features[*n].fid() {
608            Some(fid) => {
609                let package = fid.pid.package();
610                fmt.push_str(&package.name);
611
612                if let Some(src) = package.source.as_ref() {
613                    if src.repr.starts_with("git") {
614                        fmt.push_str(" git");
615                    } else {
616                        fmt.push_str(&format!(" {}", package.version));
617                    }
618                }
619                match fid.dep {
620                    Feat::Base => {}
621                    Feat::Named(name) => {
622                        fmt.push('\n');
623                        fmt.push_str(name);
624                    }
625                }
626
627                dot::LabelText::LabelStr(fmt.into())
628            }
629            None => dot::LabelText::LabelStr("root".into()),
630        }
631    }
632
633    fn edge_label(&'a self, e: &EdgeIndex) -> dot::LabelText<'a> {
634        let _ = e;
635        dot::LabelText::LabelStr("".into())
636    }
637
638    fn node_style(&'a self, n: &NodeIndex) -> dot::Style {
639        if let Some(fid) = self.features[*n].fid() {
640            if self.workspace_members.contains(&fid.pid) {
641                dot::Style::None
642            } else {
643                dot::Style::Filled
644            }
645        } else {
646            dot::Style::None
647        }
648    }
649
650    fn node_color(&'a self, node: &NodeIndex) -> Option<dot::LabelText<'a>> {
651        self.focus_targets
652            .as_ref()?
653            .contains(node)
654            .then(|| dot::LabelText::LabelStr("pink".into()))
655    }
656
657    fn edge_end_arrow(&'a self, _e: &EdgeIndex) -> dot::Arrow {
658        dot::Arrow::default()
659    }
660
661    fn edge_start_arrow(&'a self, _e: &EdgeIndex) -> dot::Arrow {
662        dot::Arrow::default()
663    }
664
665    fn edge_style(&'a self, e: &EdgeIndex) -> dot::Style {
666        if self.features[*e].is_dev_only() {
667            dot::Style::Dashed
668        } else {
669            dot::Style::None
670        }
671    }
672
673    fn edge_color(&'a self, e: &EdgeIndex) -> Option<dot::LabelText<'a>> {
674        if self.features[*e].optional {
675            Some(dot::LabelText::label("grey"))
676        } else {
677            Some(dot::LabelText::label("black"))
678        }
679    }
680
681    fn kind(&self) -> dot::Kind {
682        dot::Kind::Digraph
683    }
684}
685
686pub trait HasIndex<'a> {
687    fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex>;
688}
689
690impl HasIndex<'_> for NodeIndex {
691    fn get_index(self, _graph: &mut FeatGraph) -> anyhow::Result<NodeIndex> {
692        Ok(self)
693    }
694}
695
696impl<'a> HasIndex<'a> for Fid<'a> {
697    fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
698        Ok(graph.fid_index(self))
699    }
700}
701
702impl<'a> HasIndex<'a> for Pid<'a> {
703    fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
704        if self.package().features.contains_key("default") {
705            Ok(graph.fid_index(self.named("default")))
706        } else {
707            Ok(graph.fid_index(self.base()))
708        }
709    }
710}
711
712impl<'a> HasIndex<'a> for &'a Package {
713    fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
714        (*graph
715            .cache
716            .get(&self.id)
717            .ok_or_else(|| anyhow::anyhow!("No cached value for {:?}", self.id))?)
718        .get_index(graph)
719    }
720}
721
722impl<'a> HasIndex<'a> for (&'a Package, &'a str) {
723    fn get_index(self, graph: &mut FeatGraph<'a>) -> anyhow::Result<NodeIndex> {
724        let package_id = &self.0.id;
725        let feat = self.1;
726        let pid = *graph
727            .cache
728            .get(package_id)
729            .ok_or_else(|| anyhow::anyhow!("No cached value for {package_id:?}"))?;
730        pid.named(feat).get_index(graph)
731    }
732}
733
734#[derive(Copy, Clone, Debug, Eq, PartialEq)]
735pub enum FeatTarget<'a> {
736    Named { name: &'a str },
737    Dependency { krate: &'a str },
738    Remote { krate: &'a str, feat: &'a str },
739    Cond { krate: &'a str, feat: &'a str },
740}
741
742impl<'a> From<&'a str> for FeatTarget<'a> {
743    fn from(s: &'a str) -> Self {
744        if let Some(krate) = s.strip_prefix("dep:") {
745            FeatTarget::Dependency { krate }
746        } else if let Some((krate, feat)) = s.split_once("?/") {
747            FeatTarget::Cond { krate, feat }
748        } else if let Some((krate, feat)) = s.split_once('/') {
749            FeatTarget::Remote { krate, feat }
750        } else {
751            FeatTarget::Named { name: s }
752        }
753    }
754}
755
756impl Fid<'_> {
757    #[must_use]
758    /// Create a base feature from possibly named one
759    pub const fn get_base(&self) -> Self {
760        Self {
761            dep: Feat::Base,
762            ..*self
763        }
764    }
765}
766
767#[cfg(test)]
768mod test {
769    use super::*;
770    #[test]
771    fn feat_target() {
772        use FeatTarget::*;
773        assert_eq!(FeatTarget::from("quote"), Named { name: "quote" });
774        assert_eq!(
775            FeatTarget::from("dep:serde_json"),
776            Dependency {
777                krate: "serde_json"
778            }
779        );
780        assert_eq!(
781            FeatTarget::from("syn/extra-tr"),
782            Remote {
783                krate: "syn",
784                feat: "extra-tr"
785            }
786        );
787        assert_eq!(
788            FeatTarget::from("rgb?/serde"),
789            Cond {
790                krate: "rgb",
791                feat: "serde"
792            }
793        );
794    }
795
796    fn get_demo_meta(ix: usize) -> anyhow::Result<Metadata> {
797        let path = format!(
798            "{}/test_workspaces/{ix}/metadata.json",
799            env!("CARGO_MANIFEST_DIR")
800        );
801        let data = std::fs::read_to_string(path)?;
802        Ok(cargo_metadata::MetadataCommand::parse(data)?)
803    }
804
805    fn process_fg_with<F>(ix: usize, op: F) -> anyhow::Result<()>
806    where
807        F: FnOnce(&mut FeatGraph) -> anyhow::Result<()>,
808    {
809        let meta = get_demo_meta(ix)?;
810        let platform = target_spec::Platform::current()?;
811        let triplets = vec![platform.triple_str()];
812        let mut fg = FeatGraph::init(&meta, triplets, Vec::new())?;
813        op(&mut fg)
814    }
815
816    #[test]
817    fn metadata_snapshot_2() -> anyhow::Result<()> {
818        process_fg_with(2, |_| Ok(()))?;
819        Ok(())
820    }
821
822    #[test]
823    fn metadata_snapshot_3() -> anyhow::Result<()> {
824        process_fg_with(3, |_| Ok(()))?;
825        Ok(())
826    }
827
828    #[test]
829    fn metadata_snapshot_4() -> anyhow::Result<()> {
830        process_fg_with(4, |_| Ok(()))?;
831        Ok(())
832    }
833
834    #[test]
835    fn metadata_snapshot_5() -> anyhow::Result<()> {
836        process_fg_with(5, |_fg| {
837            //            dump(fg)?;
838
839            Ok(())
840        })
841    }
842}