1mod changes;
52mod graph;
53
54use anyhow::{Context, Result, anyhow};
55use std::collections::{HashMap, HashSet};
56use std::path::{Path, PathBuf};
57use toml_edit::{DocumentMut, Item, Table, Value};
58
59use changes::{Change, Changes};
60
61#[derive(Debug, Clone)]
65struct Warning(String);
66
67#[derive(Debug, Clone)]
72struct Dependency {
73 name: String,
74 optional: bool,
75 version: Option<String>,
76}
77
78#[derive(Debug, Clone)]
80struct Crate {
81 name: String,
83 toml_file: PathBuf,
85 toml_doc: DocumentMut,
87 toml_doc_orig: DocumentMut,
89}
90
91#[derive(Debug, Clone)]
93struct CrateInfo {
94 published: bool,
95}
96
97fn arti_dependencies(dependencies: &Table) -> Vec<Dependency> {
102 let mut deps = Vec::new();
103
104 for (depname, info) in dependencies {
105 let table = match info {
106 Item::Value(Value::InlineTable(info)) => info.clone().into_table(),
108 Item::Table(info) => info.clone(),
109 _ => continue, };
111 if !table.contains_key("path") {
112 continue; }
114 let optional = table
115 .get("optional")
116 .and_then(Item::as_value)
117 .and_then(Value::as_bool)
118 .unwrap_or(false);
119 let version = table
120 .get("version")
121 .and_then(Item::as_value)
122 .and_then(Value::as_str)
123 .map(str::to_string);
124
125 deps.push(Dependency {
126 name: depname.to_string(),
127 optional,
128 version,
129 });
130 }
131
132 deps
133}
134
135impl Crate {
136 fn load(p: impl AsRef<Path>) -> Result<Self> {
138 let toml_file = p.as_ref().to_owned();
139 let s = std::fs::read_to_string(&toml_file)?;
140 let toml_doc = s.parse::<DocumentMut>()?;
141 let toml_doc_orig = toml_doc.clone();
142 let name = toml_doc["package"]["name"]
143 .as_str()
144 .ok_or_else(|| anyhow!("package.name was not a string"))?
145 .to_string();
146 Ok(Crate {
147 name,
148 toml_file,
149 toml_doc,
150 toml_doc_orig,
151 })
152 }
153
154 fn info(&self) -> CrateInfo {
156 let package = self
157 .toml_doc
158 .get("package")
159 .expect("no package table!")
160 .as_table()
161 .expect("[package] was not a table");
162 let publish_option = package
163 .get("publish")
164 .and_then(Item::as_value)
165 .and_then(Value::as_bool);
166 let published = publish_option != Some(false);
167 CrateInfo { published }
168 }
169
170 fn fix(
172 &mut self,
173 no_annotate: bool,
174 other_crates: &HashMap<String, CrateInfo>,
175 ) -> Result<Vec<Warning>> {
176 let mut warnings = Vec::new();
177 let my_info = self.info();
178 let mut w = |s| warnings.push(Warning(s));
179 let dependencies = self
180 .toml_doc
181 .entry("dependencies")
182 .or_insert_with(|| Item::Table(Table::new()));
183 let dependencies = arti_dependencies(
184 dependencies
185 .as_table()
186 .ok_or_else(|| anyhow!("dependencies was not a table"))?,
187 );
188 let features = self
189 .toml_doc
190 .entry("features")
191 .or_insert_with(|| Item::Table(Table::new()))
192 .as_table_mut()
193 .ok_or_else(|| anyhow!("Features was not table"))?;
194 let graph = graph::FeatureGraph::from_features_table(features)?;
195 let mut changes = Changes::default();
196
197 let all_features: HashSet<_> = graph.all_features().collect();
199 let reachable_from_experimental: HashSet<_> =
200 graph.all_reachable_from("experimental").collect();
201 let nonadditive: HashSet<_> = graph.edges_to("__is_nonadditive").collect();
202 let reachable_from_full: HashSet<_> = graph.all_reachable_from("full").collect();
203
204 if !graph.contains_feature("full") {
206 w("full feature does not exist. Adding.".to_string());
207 changes.push(Change::AddFeature("full".to_string()));
208 }
209
210 for dep in dependencies.iter() {
212 let wanted = if dep.optional {
213 format!("{}?/full", dep.name)
214 } else {
215 format!("{}/full", dep.name)
216 };
217
218 if !graph.contains_edge("full", wanted.as_str()) {
219 w(format!("full should contain {wanted}. Fixing."));
220 changes.push(Change::AddExternalEdge("full".to_string(), wanted));
221 }
222 }
223
224 let defined_experimental: HashSet<_> = {
227 let in_experimental: HashSet<_> = graph.edges_from("experimental").collect();
228 let is_experimental: HashSet<_> = graph.edges_to("__is_experimental").collect();
229
230 for f in in_experimental.difference(&is_experimental) {
232 if all_features.contains(f) {
233 w(format!("{f} should depend on __is_experimental. Fixing."));
234 changes.push(Change::AddEdge(f.clone(), "__is_experimental".into()));
235 }
236 }
237 for f in is_experimental.difference(&reachable_from_experimental) {
239 w(format!(
240 "{f} is marked as __is_experimental, but is not reachable from experimental. Fixing."
241 ));
242 changes.push(Change::AddEdge("experimental".into(), f.clone()))
243 }
244
245 &in_experimental | &is_experimental
246 };
247
248 {
251 let complaint: &str = "# XX\x58X Mark as full, experimental, or non-additive!\n";
252
253 let all_features: HashSet<_> = graph.all_features().collect();
254 let meta: HashSet<_> = [
255 "__is_nonadditive",
256 "__is_experimental",
257 "full",
258 "default",
259 "experimental",
260 ]
261 .into_iter()
262 .map(String::from)
263 .collect();
264
265 let mut not_found = all_features;
266 for set in [
267 &reachable_from_full,
268 &meta,
269 &reachable_from_experimental,
270 &nonadditive,
271 ] {
272 not_found = ¬_found - set;
273 }
274
275 for f in not_found {
276 w(format!(
277 "{f} is not experimental, reachable from full, or nonadditive."
278 ));
279 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
280 }
281 }
282
283 {
285 let complaint = "# XX\x58X This is reachable from 'default', but from 'full'.\n";
286 let default: HashSet<_> = graph.edges_from("default").collect();
287 for f in default.difference(&reachable_from_full) {
288 if all_features.contains(f) {
289 w(format!("{f} is reachable from default, but not from full."));
290 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
291 }
292 }
293 }
294
295 {
298 let complaint = "# XX\x58X This is non-additive, but reachable from 'full'.\n";
299 for f in nonadditive.intersection(&reachable_from_full) {
300 w(format!("nonadditive feature {f} is reachable from full."));
301 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
302 }
303 let complaint = "# XX\x58X This is non-additive, but reachable from 'experimental'.\n";
304 for f in nonadditive.intersection(&reachable_from_experimental) {
305 w(format!(
306 "nonadditive feature {f} is reachable from experimental."
307 ));
308 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
309 }
310 }
311
312 {
314 let complaint = "# XX\x58X This is experimental, but reachable from 'full'.\n";
315 for f in reachable_from_full.intersection(&defined_experimental) {
316 w(format!("experimental feature {f} is reachable from full!"));
317 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
318 }
319 }
320
321 for dep in &dependencies {
323 match dep.version.as_deref() {
324 Some("*") => w(format!(
325 "Dependency for {:?} is given as version='*'",
326 &dep.name
327 )),
328 None => w(format!(
329 "No version found for dependency on {:?}",
330 &dep.name
331 )),
332 _ => {}
333 }
334 }
335
336 if my_info.published {
339 println!("in {}", self.name);
340 for dep in &dependencies {
341 match other_crates.get(&dep.name) {
342 None => w(format!(
343 "Dependency on crate {:?}, which I could not find.",
344 &dep.name
345 )),
346 Some(info) if !info.published => w(format!(
347 "Dependency on crate {:?}, which has `publish = false`",
348 &dep.name
349 )),
350 _ => {}
351 }
352 }
353 }
354
355 if no_annotate {
356 changes.drop_annotations();
357 }
358 let features = self
360 .toml_doc
361 .get_mut("features")
362 .ok_or_else(|| anyhow!("I thought we added 'features' earlier!"))?
363 .as_table_mut()
364 .ok_or_else(|| anyhow!("Features was not table"))?;
365 changes.apply(features)?;
366
367 Ok(warnings)
368 }
369
370 fn save_if_changed(&self) -> Result<()> {
372 let old_text = self.toml_doc_orig.to_string();
373 let new_text = self.toml_doc.to_string();
374 if new_text != old_text {
375 println!("{} changed. Replacing.", self.name);
376 let tmpname = self.toml_file.with_extension("toml.tmp");
377 std::fs::write(&tmpname, new_text.as_str())?;
378 std::fs::rename(&tmpname, &self.toml_file)?;
379 }
380 Ok(())
381 }
382}
383
384fn list_crate_paths(
386 toplevel: impl AsRef<Path>,
387 exclusion_prefixes: &[String],
388) -> Result<Vec<String>> {
389 let s = std::fs::read_to_string(toplevel.as_ref())?;
390 let toml_doc = s.parse::<DocumentMut>()?;
391 Ok(toml_doc["workspace"]["members"]
392 .as_array()
393 .ok_or_else(|| anyhow!("workplace.members is not an array!?"))?
394 .iter()
395 .map(|v| {
396 v.as_str()
397 .expect("Some member of workplace.members is not a string!?")
398 .to_owned()
399 })
400 .filter(|s| {
401 !exclusion_prefixes
403 .iter()
404 .any(|prefix| s.starts_with(prefix))
405 })
406 .collect())
407}
408
409fn main() -> Result<()> {
410 let mut pargs = pico_args::Arguments::from_env();
411 const HELP: &str = "fixup-features [--no-annotate] [--exclude <PREFIX1> --exclude <PREFIX2> ...] <toplevel Cargo.toml>";
412
413 if pargs.contains(["-h", "--help"]) {
414 println!("{HELP}");
415 return Ok(());
416 }
417 let no_annotate = pargs.contains("--no-annotate");
418 let exclusion_prefixes: Vec<String> = pargs.values_from_str("--exclude").unwrap();
419 let toplevel_toml_file: PathBuf = pargs.free_from_str()?;
420 if !pargs.finish().is_empty() {
421 println!("{HELP}");
422 return Ok(());
423 }
424
425 let toplevel_dir = toplevel_toml_file
426 .parent()
427 .expect("How is your Cargo.toml file `/`?")
428 .to_path_buf();
429 let mut crates = Vec::new();
430 let mut crate_info = HashMap::new();
431 for p in list_crate_paths(&toplevel_toml_file, &exclusion_prefixes)? {
432 let mut crate_toml_path = toplevel_dir.clone();
433 crate_toml_path.push(p);
434 crate_toml_path.push("Cargo.toml");
435 let cr =
436 Crate::load(&crate_toml_path).with_context(|| format!("In {crate_toml_path:?}"))?;
437 crate_info.insert(cr.name.clone(), cr.info());
438 crates.push(cr);
439 }
440
441 for cr in crates.iter_mut() {
442 for w in cr
443 .fix(no_annotate, &crate_info)
444 .with_context(|| format!("In {}", cr.name))?
445 {
446 println!("{}: {}", cr.name, w.0);
447 }
448 cr.save_if_changed()?;
449 }
450
451 Ok(())
452}