1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::manifest::ManifestCache;
4use crate::metadata::{Metadata, ResolveNode};
5
6#[derive(Debug, Clone, Default)]
8pub struct FeatureGraph {
9 pub workspace_members: BTreeSet<String>,
10 pub nodes: BTreeMap<String, FeatureNode>,
11}
12
13impl FeatureGraph {
14 pub fn sorted_nodes(&self) -> impl Iterator<Item = &FeatureNode> {
15 self.nodes.values()
16 }
17
18 pub fn get(&self, package_id: &str) -> Option<&FeatureNode> {
19 self.nodes.get(package_id)
20 }
21}
22
23#[derive(Debug, Clone, Default)]
25pub struct FeatureNode {
26 pub package_id: String,
27 pub name: String,
28 pub version: String,
29 pub manifest_path: String,
30 pub active_features: BTreeSet<String>,
31 pub available_features: BTreeMap<String, Vec<String>>,
32 pub optional_dependencies: BTreeSet<String>,
33 pub dependency_features: BTreeMap<String, Vec<String>>,
34 pub feature_sources: BTreeMap<String, Vec<FeatureSource>>,
35 pub dependencies: Vec<String>,
36}
37
38#[derive(Debug, Clone, Eq, PartialEq)]
40pub struct FeatureSource {
41 pub requested_by: String,
42 pub path: Vec<String>,
43}
44
45pub fn resolve(
47 metadata: &Metadata,
48 manifests: &mut ManifestCache,
49) -> Result<FeatureGraph, Box<dyn std::error::Error>> {
50 let mut graph = FeatureGraph {
51 workspace_members: metadata.workspace_members.iter().cloned().collect(),
52 ..FeatureGraph::default()
53 };
54
55 for package in &metadata.packages {
56 let manifest = manifests.get_or_parse(&package.manifest_path)?;
57 let package_id = package.id.clone();
58 let mut available_features = manifest.features;
59 merge_feature_map(&mut available_features, package.features.clone());
60 sort_feature_map(&mut available_features);
61 let mut optional_dependencies = manifest.optional_dependencies;
62 optional_dependencies.extend(package.optional_dependencies.clone());
63 let mut dependency_features = manifest.dependency_features;
64 merge_feature_map(
65 &mut dependency_features,
66 package.dependency_features.clone(),
67 );
68 sort_feature_map(&mut dependency_features);
69
70 graph.nodes.insert(
71 package_id.clone(),
72 FeatureNode {
73 package_id,
74 name: package.name.clone(),
75 version: package.version.clone(),
76 manifest_path: package.manifest_path.display().to_string(),
77 available_features,
78 optional_dependencies,
79 dependency_features,
80 ..FeatureNode::default()
81 },
82 );
83 }
84
85 for node in &metadata.resolve_nodes {
86 let package_id = node.id.clone();
87 if let Some(feature_node) = graph.nodes.get_mut(&package_id) {
88 feature_node
89 .active_features
90 .extend(node.features.iter().cloned());
91 feature_node.dependencies = sorted_unique(node.dependencies.clone());
92 let self_name = feature_node.name.clone();
93 let active_features = feature_node.active_features.clone();
94 for feature in active_features {
95 add_feature_source(
96 feature_node,
97 &feature,
98 FeatureSource {
99 requested_by: self_name.clone(),
100 path: vec![self_name.clone()],
101 },
102 );
103 }
104 }
105 }
106
107 record_dependency_sources(&mut graph, &metadata.resolve_nodes);
108 record_manifest_feature_sources(&mut graph, &metadata.resolve_nodes);
109 Ok(graph)
110}
111
112fn record_dependency_sources(graph: &mut FeatureGraph, nodes: &[ResolveNode]) {
113 let resolve_nodes = nodes
114 .iter()
115 .map(|node| (node.id.as_str(), node))
116 .collect::<BTreeMap<_, _>>();
117
118 for parent_id in graph.nodes.keys().cloned().collect::<Vec<_>>() {
119 let Some(metadata_node) = resolve_nodes.get(parent_id.as_str()).copied() else {
120 continue;
121 };
122 let Some(parent) = graph.get(&parent_id) else {
123 continue;
124 };
125 let parent_name = parent.name.clone();
126 let dependency_features = parent.dependency_features.clone();
127
128 for (dependency_name, child_id) in &metadata_node.dependency_names {
129 let child_name = graph
130 .nodes
131 .get(child_id)
132 .map(|child| child.name.clone())
133 .unwrap_or_else(|| dependency_name.clone());
134 let requested_features = dependency_features
135 .get(dependency_name)
136 .or_else(|| dependency_features.get(&child_name))
137 .cloned()
138 .unwrap_or_default();
139 if requested_features.is_empty() {
140 continue;
141 }
142
143 for feature in requested_features {
144 let Some(child) = graph.nodes.get_mut(child_id) else {
145 continue;
146 };
147 if child.active_features.contains(&feature) {
148 add_feature_source(
149 child,
150 &feature,
151 FeatureSource {
152 requested_by: format!("{parent_name}/{child_name}"),
153 path: vec![parent_name.clone(), child_name.clone(), feature.clone()],
154 },
155 );
156 }
157 }
158 }
159 }
160}
161
162fn record_manifest_feature_sources(graph: &mut FeatureGraph, nodes: &[ResolveNode]) {
163 let resolve_nodes = nodes
164 .iter()
165 .map(|node| (node.id.as_str(), node))
166 .collect::<BTreeMap<_, _>>();
167 let package_names = graph
168 .nodes
169 .iter()
170 .map(|(id, node)| (node.name.clone(), id.clone()))
171 .collect::<BTreeMap<_, _>>();
172
173 for package_id in graph.nodes.keys().cloned().collect::<Vec<_>>() {
174 let Some(node_snapshot) = graph.nodes.get(&package_id).cloned() else {
175 continue;
176 };
177 let metadata_node = resolve_nodes.get(package_id.as_str()).copied();
178
179 for feature in &node_snapshot.active_features {
180 let Some(expanded) = node_snapshot.available_features.get(feature) else {
181 continue;
182 };
183 for item in expanded {
184 if let Some((dependency_name, dependency_feature)) = item.split_once('/') {
185 let child_id = metadata_node
186 .and_then(|node| node.dependency_names.get(dependency_name))
187 .cloned()
188 .or_else(|| package_names.get(dependency_name).cloned());
189 let Some(child_id) = child_id else { continue };
190 let Some(child) = graph.nodes.get_mut(&child_id) else {
191 continue;
192 };
193 if child.active_features.contains(dependency_feature) {
194 add_feature_source(
195 child,
196 dependency_feature,
197 FeatureSource {
198 requested_by: format!("{}/{}", node_snapshot.name, feature),
199 path: vec![
200 node_snapshot.name.clone(),
201 feature.clone(),
202 dependency_name.to_string(),
203 dependency_feature.to_string(),
204 ],
205 },
206 );
207 }
208 continue;
209 }
210
211 let normalized = normalize_feature_reference(item);
212 if let Some(node) = graph.nodes.get_mut(&package_id) {
213 if node.active_features.contains(&normalized) {
214 add_feature_source(
215 node,
216 &normalized,
217 FeatureSource {
218 requested_by: format!("{}/{}", node_snapshot.name, feature),
219 path: vec![
220 node_snapshot.name.clone(),
221 feature.clone(),
222 normalized.clone(),
223 ],
224 },
225 );
226 }
227 }
228 }
229 }
230 }
231}
232
233fn add_feature_source(node: &mut FeatureNode, feature: &str, source: FeatureSource) {
234 let sources = node.feature_sources.entry(feature.to_string()).or_default();
235 if !sources.contains(&source) {
236 sources.push(source);
237 sources.sort_by(|left, right| {
238 left.requested_by
239 .cmp(&right.requested_by)
240 .then_with(|| left.path.cmp(&right.path))
241 });
242 }
243}
244
245fn merge_feature_map(
246 target: &mut BTreeMap<String, Vec<String>>,
247 source: BTreeMap<String, Vec<String>>,
248) {
249 for (feature, values) in source {
250 target.entry(feature).or_default().extend(values);
251 }
252}
253
254fn sort_feature_map(map: &mut BTreeMap<String, Vec<String>>) {
255 for values in map.values_mut() {
256 *values = sorted_unique(std::mem::take(values));
257 }
258}
259
260fn sorted_unique(mut values: Vec<String>) -> Vec<String> {
261 values.sort();
262 values.dedup();
263 values
264}
265
266fn normalize_feature_reference(feature: &str) -> String {
267 feature
268 .strip_prefix("dep:")
269 .unwrap_or(feature)
270 .split('/')
271 .next_back()
272 .unwrap_or(feature)
273 .to_string()
274}
275
276#[cfg(test)]
277mod tests {
278 use super::normalize_feature_reference;
279 use crate::manifest::ManifestCache;
280 use crate::{metadata, resolver};
281 use std::path::Path;
282
283 fn resolver_v2_graph() -> resolver::FeatureGraph {
284 let metadata = metadata::load_metadata(Path::new("tests/fixtures/resolver-v2"))
285 .expect("fixture metadata should load");
286 resolver::resolve(&metadata, &mut ManifestCache::default())
287 .expect("fixture graph should resolve")
288 }
289
290 fn node<'a>(graph: &'a resolver::FeatureGraph, name: &str) -> &'a resolver::FeatureNode {
291 graph
292 .sorted_nodes()
293 .find(|node| node.name == name)
294 .unwrap_or_else(|| panic!("missing node {name}"))
295 }
296
297 #[test]
298 fn normalizes_dep_and_namespaced_features() {
299 assert_eq!(normalize_feature_reference("dep:serde_json"), "serde_json");
300 assert_eq!(normalize_feature_reference("serde/derive"), "derive");
301 }
302
303 #[test]
304 fn uses_cargo_metadata_resolved_features_for_direct_dependency_features() {
305 let graph = resolver_v2_graph();
306 let leaf = node(&graph, "leaf");
307
308 assert!(leaf.active_features.contains("direct-leaf"));
309 assert!(leaf.feature_sources["direct-leaf"].iter().any(|source| {
310 source.requested_by == "resolver-app/leaf"
311 && source.path == ["resolver-app", "leaf", "direct-leaf"]
312 }));
313 }
314
315 #[test]
316 fn records_transitive_feature_propagation_from_manifest_expansion() {
317 let graph = resolver_v2_graph();
318 let leaf = node(&graph, "leaf");
319
320 assert!(leaf.active_features.contains("propagated"));
321 assert!(leaf.feature_sources["propagated"].iter().any(|source| {
322 source.requested_by == "mid/propagated"
323 && source.path == ["mid", "propagated", "leaf", "propagated"]
324 }));
325 assert!(leaf.feature_sources["mid-forward"].iter().any(|source| {
326 source.requested_by == "mid/direct-mid"
327 && source.path == ["mid", "direct-mid", "leaf", "mid-forward"]
328 }));
329 }
330
331 #[test]
332 fn includes_cargo_metadata_dev_and_build_dependency_resolution() {
333 let graph = resolver_v2_graph();
334 let helper = node(&graph, "helper");
335 let leaf = node(&graph, "leaf");
336
337 assert!(helper.active_features.contains("build-only"));
338 assert!(leaf.active_features.contains("dev-only"));
339 assert!(leaf.active_features.contains("build-transitive"));
340 assert!(leaf.feature_sources["dev-only"].iter().any(|source| {
341 source.requested_by == "resolver-app/leaf"
342 && source.path == ["resolver-app", "leaf", "dev-only"]
343 }));
344 assert!(leaf.feature_sources["build-transitive"]
345 .iter()
346 .any(|source| {
347 source.requested_by == "helper/leaf"
348 && source.path == ["helper", "leaf", "build-transitive"]
349 }));
350 }
351
352 #[test]
353 fn preserves_workspace_member_feature_unification_and_deterministic_edges() {
354 let graph = resolver_v2_graph();
355 let tool = node(&graph, "resolver-tool");
356 let leaf = node(&graph, "leaf");
357
358 assert_eq!(
359 tool.dependency_features["leaf"],
360 vec!["tool-only", "workspace-base"]
361 );
362 assert!(leaf.active_features.contains("workspace-base"));
363 assert!(leaf.active_features.contains("tool-only"));
364 assert_eq!(
365 node(&graph, "resolver-app").dependencies,
366 vec![
367 node(&graph, "helper").package_id.clone(),
368 leaf.package_id.clone(),
369 node(&graph, "mid").package_id.clone(),
370 ]
371 );
372 }
373}