1mod changes;
52mod graph;
53
54use anyhow::{anyhow, Context, Result};
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 {}. Fixing.", wanted));
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!("{f} is marked as __is_experimental, but is not reachable from experimental. Fixing."));
240 changes.push(Change::AddEdge("experimental".into(), f.clone()))
241 }
242
243 &in_experimental | &is_experimental
244 };
245
246 {
249 let complaint: &str = "# XX\x58X Mark as full, experimental, or non-additive!\n";
250
251 let all_features: HashSet<_> = graph.all_features().collect();
252 let meta: HashSet<_> = [
253 "__is_nonadditive",
254 "__is_experimental",
255 "full",
256 "default",
257 "experimental",
258 ]
259 .into_iter()
260 .map(String::from)
261 .collect();
262
263 let mut not_found = all_features;
264 for set in [
265 &reachable_from_full,
266 &meta,
267 &reachable_from_experimental,
268 &nonadditive,
269 ] {
270 not_found = ¬_found - set;
271 }
272
273 for f in not_found {
274 w(format!(
275 "{f} is not experimental, reachable from full, or nonadditive."
276 ));
277 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
278 }
279 }
280
281 {
283 let complaint = "# XX\x58X This is reachable from 'default', but from 'full'.\n";
284 let default: HashSet<_> = graph.edges_from("default").collect();
285 for f in default.difference(&reachable_from_full) {
286 if all_features.contains(f) {
287 w(format!("{f} is reachable from default, but not from full."));
288 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
289 }
290 }
291 }
292
293 {
296 let complaint = "# XX\x58X This is non-additive, but reachable from 'full'.\n";
297 for f in nonadditive.intersection(&reachable_from_full) {
298 w(format!("nonadditive feature {f} is reachable from full."));
299 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
300 }
301 let complaint = "# XX\x58X This is non-additive, but reachable from 'experimental'.\n";
302 for f in nonadditive.intersection(&reachable_from_experimental) {
303 w(format!(
304 "nonadditive feature {f} is reachable from experimental."
305 ));
306 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
307 }
308 }
309
310 {
312 let complaint = "# XX\x58X This is experimental, but reachable from 'full'.\n";
313 for f in reachable_from_full.intersection(&defined_experimental) {
314 w(format!("experimental feature {f} is reachable from full!"));
315 changes.push(Change::Annotate(f.clone(), complaint.to_string()));
316 }
317 }
318
319 for dep in &dependencies {
321 match dep.version.as_deref() {
322 Some("*") => w(format!(
323 "Dependency for {:?} is given as version='*'",
324 &dep.name
325 )),
326 None => w(format!(
327 "No version found for dependency on {:?}",
328 &dep.name
329 )),
330 _ => {}
331 }
332 }
333
334 if my_info.published {
337 println!("in {}", self.name);
338 for dep in &dependencies {
339 match other_crates.get(&dep.name) {
340 None => w(format!(
341 "Dependency on crate {:?}, which I could not find.",
342 &dep.name
343 )),
344 Some(info) if !info.published => w(format!(
345 "Dependency on crate {:?}, which has `publish = false`",
346 &dep.name
347 )),
348 _ => {}
349 }
350 }
351 }
352
353 if no_annotate {
354 changes.drop_annotations();
355 }
356 let features = self
358 .toml_doc
359 .get_mut("features")
360 .ok_or_else(|| anyhow!("I thought we added 'features' earlier!"))?
361 .as_table_mut()
362 .ok_or_else(|| anyhow!("Features was not table"))?;
363 changes.apply(features)?;
364
365 Ok(warnings)
366 }
367
368 fn save_if_changed(&self) -> Result<()> {
370 let old_text = self.toml_doc_orig.to_string();
371 let new_text = self.toml_doc.to_string();
372 if new_text != old_text {
373 println!("{} changed. Replacing.", self.name);
374 let tmpname = self.toml_file.with_extension("toml.tmp");
375 std::fs::write(&tmpname, new_text.as_str())?;
376 std::fs::rename(&tmpname, &self.toml_file)?;
377 }
378 Ok(())
379 }
380}
381
382fn list_crate_paths(
384 toplevel: impl AsRef<Path>,
385 exclusion_prefixes: &[String],
386) -> Result<Vec<String>> {
387 let s = std::fs::read_to_string(toplevel.as_ref())?;
388 let toml_doc = s.parse::<DocumentMut>()?;
389 Ok(toml_doc["workspace"]["members"]
390 .as_array()
391 .ok_or_else(|| anyhow!("workplace.members is not an array!?"))?
392 .iter()
393 .map(|v| {
394 v.as_str()
395 .expect("Some member of workplace.members is not a string!?")
396 .to_owned()
397 })
398 .filter(|s| {
399 !exclusion_prefixes
401 .iter()
402 .any(|prefix| s.starts_with(prefix))
403 })
404 .collect())
405}
406
407fn main() -> Result<()> {
408 let mut pargs = pico_args::Arguments::from_env();
409 const HELP: &str =
410 "fixup-features [--no-annotate] [--exclude <PREFIX1> --exclude <PREFIX2> ...] <toplevel Cargo.toml>";
411
412 if pargs.contains(["-h", "--help"]) {
413 println!("{}", HELP);
414 return Ok(());
415 }
416 let no_annotate = pargs.contains("--no-annotate");
417 let exclusion_prefixes: Vec<String> = pargs.values_from_str("--exclude").unwrap();
418 let toplevel_toml_file: PathBuf = pargs.free_from_str()?;
419 if !pargs.finish().is_empty() {
420 println!("{}", HELP);
421 return Ok(());
422 }
423
424 let toplevel_dir = toplevel_toml_file
425 .parent()
426 .expect("How is your Cargo.toml file `/`?")
427 .to_path_buf();
428 let mut crates = Vec::new();
429 let mut crate_info = HashMap::new();
430 for p in list_crate_paths(&toplevel_toml_file, &exclusion_prefixes)? {
431 let mut crate_toml_path = toplevel_dir.clone();
432 crate_toml_path.push(p);
433 crate_toml_path.push("Cargo.toml");
434 let cr =
435 Crate::load(&crate_toml_path).with_context(|| format!("In {crate_toml_path:?}"))?;
436 crate_info.insert(cr.name.clone(), cr.info());
437 crates.push(cr);
438 }
439
440 for cr in crates.iter_mut() {
441 for w in cr
442 .fix(no_annotate, &crate_info)
443 .with_context(|| format!("In {}", cr.name))?
444 {
445 println!("{}: {}", cr.name, w.0);
446 }
447 cr.save_if_changed()?;
448 }
449
450 Ok(())
451}