1use std::collections::{BTreeMap, BTreeSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result, anyhow, bail};
6use serde::Deserialize;
7use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, Value};
8
9use crate::model::{
10 DependencyInfo, Feature, FeatureGroup, FeatureManifest, FeatureMetadata, FeatureRef, LintLevel,
11 LintPreset, MetadataLayout,
12};
13
14pub const FEATURE_MANIFEST_METADATA_TABLE: &str = "feature-manifest";
16
17pub const FEATURE_DOCS_METADATA_TABLE: &str = "feature-docs";
19
20#[derive(Debug, Clone, PartialEq, Eq, Default)]
22pub struct SyncOptions {
23 pub check_only: bool,
25 pub remove_stale: bool,
27 pub style: Option<MetadataLayout>,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct SyncReport {
34 pub manifest_path: PathBuf,
36 pub package_name: Option<String>,
38 pub metadata_table: String,
40 pub style: MetadataLayout,
42 pub added_features: Vec<String>,
44 pub removed_features: Vec<String>,
46 pub would_change: bool,
48}
49
50impl SyncReport {
51 pub fn changed(&self) -> bool {
53 self.would_change
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct SyncPreview {
60 pub report: SyncReport,
62 pub rewritten: Option<String>,
64}
65
66#[derive(Debug, Clone, Deserialize)]
67#[serde(untagged)]
68enum RawFeatureMetadata {
69 Description(String),
70 Detailed(FeatureMetadata),
71}
72
73impl RawFeatureMetadata {
74 fn into_metadata(self) -> FeatureMetadata {
75 match self {
76 Self::Description(description) => FeatureMetadata {
77 description: Some(description),
78 ..FeatureMetadata::default()
79 },
80 Self::Detailed(metadata) => metadata,
81 }
82 }
83}
84
85#[derive(Debug, Deserialize)]
86struct RawManifest {
87 package: Option<RawPackage>,
88 #[serde(default)]
89 features: BTreeMap<String, Vec<String>>,
90 #[serde(default)]
91 dependencies: BTreeMap<String, RawDependency>,
92 #[serde(default)]
93 target: BTreeMap<String, RawTarget>,
94}
95
96#[derive(Debug, Deserialize)]
97struct RawPackage {
98 name: Option<String>,
99 metadata: Option<toml::Table>,
100}
101
102#[derive(Debug, Deserialize)]
103struct RawTarget {
104 #[serde(default)]
105 dependencies: BTreeMap<String, RawDependency>,
106}
107
108#[derive(Debug, Clone, Deserialize)]
109#[serde(untagged)]
110enum RawDependency {
111 Version(String),
112 Detailed(RawDependencyDetail),
113}
114
115#[derive(Debug, Clone, Deserialize)]
116struct RawDependencyDetail {
117 package: Option<String>,
118 #[serde(default)]
119 workspace: bool,
120 optional: Option<bool>,
121}
122
123impl RawDependency {
124 fn to_dependency_info(&self, key: &str) -> DependencyInfo {
125 match self {
126 Self::Version(version) => {
127 let _ = version;
128 DependencyInfo {
129 key: key.to_owned(),
130 package: key.to_owned(),
131 optional: false,
132 }
133 }
134 Self::Detailed(details) => DependencyInfo {
135 key: key.to_owned(),
136 package: details.package.clone().unwrap_or_else(|| key.to_owned()),
137 optional: details.optional.unwrap_or(details.workspace),
138 },
139 }
140 }
141}
142
143pub fn load_manifest(path: impl AsRef<Path>) -> Result<FeatureManifest> {
145 let path = path.as_ref();
146 let contents = fs::read_to_string(path)
147 .with_context(|| format!("failed to read manifest `{}`", path.display()))?;
148 parse_manifest_str(&contents, path)
149}
150
151pub fn parse_manifest_str(
153 manifest_source: &str,
154 manifest_path: impl Into<PathBuf>,
155) -> Result<FeatureManifest> {
156 let manifest_path = manifest_path.into();
157 let raw: RawManifest = toml::from_str(manifest_source).with_context(|| {
158 format!(
159 "failed to parse manifest TOML from `{}`",
160 manifest_path.display()
161 )
162 })?;
163
164 let default_members = raw
165 .features
166 .get("default")
167 .cloned()
168 .unwrap_or_default()
169 .into_iter()
170 .map(|value| FeatureRef::parse(&value))
171 .collect::<Vec<_>>();
172 let default_features = default_members
173 .iter()
174 .filter_map(FeatureRef::local_feature_name)
175 .map(str::to_owned)
176 .collect::<BTreeSet<_>>();
177
178 let (metadata_features, groups, metadata_table, metadata_layout, lint_overrides, lint_preset) =
179 extract_metadata(
180 raw.package
181 .as_ref()
182 .and_then(|package| package.metadata.as_ref()),
183 )
184 .with_context(|| {
185 format!(
186 "failed to parse feature metadata from `{}`",
187 manifest_path.display()
188 )
189 })?;
190
191 let dependencies = collect_manifest_dependency_info(&raw);
192 let package_name = raw.package.and_then(|package| package.name);
193 let mut metadata_only = metadata_features.clone();
194 let mut features = BTreeMap::new();
195
196 for (name, entries) in raw.features {
197 if name == "default" {
198 continue;
199 }
200
201 let metadata = metadata_only.remove(&name).unwrap_or_default();
202 let has_metadata = metadata_features.contains_key(&name);
203 let default_enabled = default_features.contains(&name);
204
205 features.insert(
206 name.clone(),
207 Feature {
208 name,
209 metadata,
210 has_metadata,
211 enables: entries
212 .into_iter()
213 .map(|entry| FeatureRef::parse(&entry))
214 .collect(),
215 default_enabled,
216 },
217 );
218 }
219
220 Ok(FeatureManifest {
221 manifest_path,
222 package_name,
223 metadata_table,
224 metadata_layout,
225 features,
226 metadata_only,
227 default_members,
228 default_features,
229 groups,
230 dependencies,
231 lint_overrides,
232 lint_preset,
233 })
234}
235
236fn collect_manifest_dependency_info(raw: &RawManifest) -> BTreeMap<String, DependencyInfo> {
237 let mut dependencies = BTreeMap::new();
238
239 for (key, dependency) in &raw.dependencies {
240 dependencies.insert(key.clone(), dependency.to_dependency_info(key));
241 }
242
243 for target in raw.target.values() {
244 for (key, dependency) in &target.dependencies {
245 dependencies.insert(key.clone(), dependency.to_dependency_info(key));
246 }
247 }
248
249 dependencies
250}
251
252pub fn sync_manifest(path: impl AsRef<Path>, options: &SyncOptions) -> Result<SyncReport> {
254 let path = path.as_ref();
255 let preview = preview_sync_manifest(path, options)?;
256
257 if !options.check_only {
258 if let Some(rewritten) = &preview.rewritten {
259 fs::write(path, rewritten)
260 .with_context(|| format!("failed to write manifest `{}`", path.display()))?;
261 }
262 }
263
264 Ok(preview.report)
265}
266
267pub fn preview_sync_manifest(path: impl AsRef<Path>, options: &SyncOptions) -> Result<SyncPreview> {
269 let path = path.as_ref();
270 let contents = fs::read_to_string(path)
271 .with_context(|| format!("failed to read manifest `{}`", path.display()))?;
272 let manifest = parse_manifest_str(&contents, path)?;
273
274 let mut added_features = manifest
275 .features
276 .values()
277 .filter(|feature| !feature.has_metadata)
278 .map(|feature| feature.name.clone())
279 .collect::<Vec<_>>();
280 added_features.sort();
281
282 let mut removed_features = if options.remove_stale {
283 manifest.metadata_only.keys().cloned().collect::<Vec<_>>()
284 } else {
285 Vec::new()
286 };
287 removed_features.sort();
288
289 let metadata_table = manifest
290 .metadata_table
291 .clone()
292 .unwrap_or_else(|| FEATURE_MANIFEST_METADATA_TABLE.to_owned());
293 let style = options.style.unwrap_or(manifest.metadata_layout);
294
295 let would_change = !added_features.is_empty()
296 || !removed_features.is_empty()
297 || options
298 .style
299 .is_some_and(|requested| requested != manifest.metadata_layout);
300
301 let report = SyncReport {
302 manifest_path: path.to_path_buf(),
303 package_name: manifest.package_name.clone(),
304 metadata_table: metadata_table.clone(),
305 style,
306 added_features,
307 removed_features,
308 would_change,
309 };
310
311 if !would_change {
312 return Ok(SyncPreview {
313 report,
314 rewritten: None,
315 });
316 }
317
318 let mut document = contents.parse::<DocumentMut>().with_context(|| {
319 format!(
320 "failed to parse TOML document for synchronization from `{}`",
321 path.display()
322 )
323 })?;
324
325 rewrite_feature_metadata(
326 &mut document,
327 &manifest,
328 &metadata_table,
329 style,
330 &report.added_features,
331 options.remove_stale,
332 )?;
333
334 Ok(SyncPreview {
335 report,
336 rewritten: Some(document.to_string()),
337 })
338}
339
340pub fn render_sync_diff(path: &Path, before: &str, after: &str) -> String {
342 let path = path.display();
343 let mut output = format!("--- a/{path}\n+++ b/{path}\n");
344 let before_lines = before.lines().collect::<Vec<_>>();
345 let after_lines = after.lines().collect::<Vec<_>>();
346
347 output.push_str(&format!(
348 "@@ -1,{} +1,{} @@\n",
349 before_lines.len(),
350 after_lines.len()
351 ));
352
353 for operation in diff_lines(&before_lines, &after_lines) {
354 let (prefix, line) = match operation {
355 DiffLine::Unchanged(line) => (' ', line),
356 DiffLine::Removed(line) => ('-', line),
357 DiffLine::Added(line) => ('+', line),
358 };
359 output.push(prefix);
360 output.push_str(line);
361 output.push('\n');
362 }
363
364 output
365}
366
367#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368enum DiffLine<'a> {
369 Unchanged(&'a str),
370 Removed(&'a str),
371 Added(&'a str),
372}
373
374fn diff_lines<'a>(before: &'a [&'a str], after: &'a [&'a str]) -> Vec<DiffLine<'a>> {
375 let before_len = before.len();
376 let after_len = after.len();
377 let mut lengths = vec![vec![0usize; after_len + 1]; before_len + 1];
378
379 for before_index in (0..before_len).rev() {
380 for after_index in (0..after_len).rev() {
381 lengths[before_index][after_index] = if before[before_index] == after[after_index] {
382 lengths[before_index + 1][after_index + 1] + 1
383 } else {
384 lengths[before_index + 1][after_index].max(lengths[before_index][after_index + 1])
385 };
386 }
387 }
388
389 let mut operations = Vec::new();
390 let mut before_index = 0usize;
391 let mut after_index = 0usize;
392
393 while before_index < before_len || after_index < after_len {
394 if before_index < before_len
395 && after_index < after_len
396 && before[before_index] == after[after_index]
397 {
398 operations.push(DiffLine::Unchanged(before[before_index]));
399 before_index += 1;
400 after_index += 1;
401 } else if after_index < after_len
402 && (before_index == before_len
403 || lengths[before_index][after_index + 1] >= lengths[before_index + 1][after_index])
404 {
405 operations.push(DiffLine::Added(after[after_index]));
406 after_index += 1;
407 } else if before_index < before_len {
408 operations.push(DiffLine::Removed(before[before_index]));
409 before_index += 1;
410 }
411 }
412
413 operations
414}
415
416type ExtractedMetadata = (
417 BTreeMap<String, FeatureMetadata>,
418 Vec<FeatureGroup>,
419 Option<String>,
420 MetadataLayout,
421 BTreeMap<String, LintLevel>,
422 Option<LintPreset>,
423);
424
425fn empty_metadata() -> ExtractedMetadata {
426 (
427 BTreeMap::new(),
428 Vec::new(),
429 None,
430 MetadataLayout::Structured,
431 BTreeMap::new(),
432 None,
433 )
434}
435
436fn extract_metadata(metadata: Option<&toml::Table>) -> Result<ExtractedMetadata> {
437 let Some(metadata) = metadata else {
438 return Ok(empty_metadata());
439 };
440
441 let (table_name, table_value) =
442 if let Some(value) = metadata.get(FEATURE_MANIFEST_METADATA_TABLE) {
443 (FEATURE_MANIFEST_METADATA_TABLE.to_owned(), value)
444 } else if let Some(value) = metadata.get(FEATURE_DOCS_METADATA_TABLE) {
445 (FEATURE_DOCS_METADATA_TABLE.to_owned(), value)
446 } else {
447 return Ok(empty_metadata());
448 };
449
450 let table = table_value.as_table().ok_or_else(|| {
451 anyhow!("`[package.metadata.{table_name}]` must be a TOML table, not a scalar value")
452 })?;
453
454 let metadata_layout = if table
455 .get("features")
456 .and_then(|item| item.as_table())
457 .is_some()
458 {
459 MetadataLayout::Structured
460 } else if table.iter().any(|(name, _)| {
461 name != "groups" && name != "features" && name != "lints" && name != "preset"
462 }) {
463 MetadataLayout::Flat
464 } else {
465 MetadataLayout::Structured
466 };
467
468 let mut features = BTreeMap::new();
469
470 if let Some(structured_features) = table.get("features") {
471 let structured_features = structured_features.as_table().ok_or_else(|| {
472 anyhow!("`[package.metadata.{table_name}.features]` must be a TOML table")
473 })?;
474
475 for (name, value) in structured_features {
476 insert_feature_metadata(&mut features, name, value, &table_name)?;
477 }
478 }
479
480 for (name, value) in table {
481 if name == "features" || name == "groups" || name == "lints" || name == "preset" {
482 continue;
483 }
484
485 insert_feature_metadata(&mut features, name, value, &table_name)?;
486 }
487
488 let groups = match table.get("groups") {
489 Some(groups) => groups
490 .clone()
491 .try_into()
492 .context("`groups` must be an array of tables")?,
493 None => Vec::new(),
494 };
495
496 let lint_overrides = match table.get("lints") {
497 Some(lints) => lints
498 .clone()
499 .try_into()
500 .context("`lints` must be a table of lint names to levels")?,
501 None => BTreeMap::new(),
502 };
503
504 let lint_preset = match table.get("preset") {
505 Some(preset) => Some(
506 preset
507 .as_str()
508 .ok_or_else(|| anyhow!("`preset` must be a string"))?
509 .parse()?,
510 ),
511 None => None,
512 };
513
514 Ok((
515 features,
516 groups,
517 Some(table_name),
518 metadata_layout,
519 lint_overrides,
520 lint_preset,
521 ))
522}
523
524fn insert_feature_metadata(
525 features: &mut BTreeMap<String, FeatureMetadata>,
526 name: &str,
527 value: &toml::Value,
528 table_name: &str,
529) -> Result<()> {
530 let raw_metadata: RawFeatureMetadata = value.clone().try_into().with_context(|| {
531 format!("feature `{name}` in `[package.metadata.{table_name}]` must be a string or table")
532 })?;
533 let metadata = raw_metadata.into_metadata();
534
535 if features.insert(name.to_owned(), metadata).is_some() {
536 bail!("feature `{name}` is defined more than once in `[package.metadata.{table_name}]`");
537 }
538
539 Ok(())
540}
541
542fn rewrite_feature_metadata(
543 document: &mut DocumentMut,
544 manifest: &FeatureManifest,
545 metadata_table_name: &str,
546 style: MetadataLayout,
547 added_features: &[String],
548 remove_stale: bool,
549) -> Result<()> {
550 let package_table = ensure_child_table(document.as_table_mut(), "package")?;
551 let metadata_parent = ensure_child_table(package_table, "metadata")?;
552 let feature_manifest_table = ensure_child_table(metadata_parent, metadata_table_name)?;
553
554 let mut feature_entries = manifest
555 .features
556 .values()
557 .filter(|feature| feature.has_metadata)
558 .map(|feature| (feature.name.clone(), feature.metadata.clone()))
559 .collect::<BTreeMap<_, _>>();
560
561 if !remove_stale {
562 feature_entries.extend(
563 manifest
564 .metadata_only
565 .iter()
566 .map(|(feature_name, metadata)| (feature_name.clone(), metadata.clone())),
567 );
568 }
569
570 for feature_name in added_features {
571 feature_entries.insert(
572 feature_name.clone(),
573 FeatureMetadata {
574 description: Some(format!("TODO: describe `{feature_name}`.")),
575 ..FeatureMetadata::default()
576 },
577 );
578 }
579
580 remove_existing_feature_metadata(feature_manifest_table)?;
581
582 match style {
583 MetadataLayout::Flat => {
584 feature_manifest_table.remove("features");
585 for (feature_name, metadata) in &feature_entries {
586 feature_manifest_table.insert(
587 feature_name,
588 Item::Value(metadata_to_inline_value(metadata, feature_name)),
589 );
590 }
591 }
592 MetadataLayout::Structured => {
593 let features_table = ensure_child_table(feature_manifest_table, "features")?;
594 for (feature_name, metadata) in &feature_entries {
595 features_table.insert(
596 feature_name,
597 Item::Value(metadata_to_inline_value(metadata, feature_name)),
598 );
599 }
600 }
601 }
602
603 Ok(())
604}
605
606fn remove_existing_feature_metadata(table: &mut Table) -> Result<()> {
607 let feature_keys = table
608 .iter()
609 .filter_map(|(name, _)| {
610 if name == "groups" || name == "features" || name == "lints" || name == "preset" {
611 None
612 } else {
613 Some(name.to_owned())
614 }
615 })
616 .collect::<Vec<_>>();
617
618 for key in feature_keys {
619 table.remove(&key);
620 }
621
622 if let Some(features_item) = table.get_mut("features") {
623 let features_table = features_item
624 .as_table_mut()
625 .ok_or_else(|| anyhow!("expected `features` to be a TOML table while editing"))?;
626 let nested_keys = features_table
627 .iter()
628 .map(|(name, _)| name.to_owned())
629 .collect::<Vec<_>>();
630 for key in nested_keys {
631 features_table.remove(&key);
632 }
633 }
634
635 Ok(())
636}
637
638fn metadata_to_inline_value(metadata: &FeatureMetadata, feature_name: &str) -> Value {
639 let mut inline = InlineTable::new();
640 inline.insert(
641 "description",
642 Value::from(
643 metadata
644 .description
645 .clone()
646 .unwrap_or_else(|| format!("TODO: describe `{feature_name}`.")),
647 ),
648 );
649
650 if !metadata.public {
651 inline.insert("public", Value::from(false));
652 }
653 if metadata.unstable {
654 inline.insert("unstable", Value::from(true));
655 }
656 if metadata.deprecated {
657 inline.insert("deprecated", Value::from(true));
658 }
659 if metadata.allow_default {
660 inline.insert("allow_default", Value::from(true));
661 }
662 if let Some(note) = &metadata.note {
663 inline.insert("note", Value::from(note.clone()));
664 }
665 if let Some(category) = &metadata.category {
666 inline.insert("category", Value::from(category.clone()));
667 }
668 if let Some(since) = &metadata.since {
669 inline.insert("since", Value::from(since.clone()));
670 }
671 if let Some(docs) = &metadata.docs {
672 inline.insert("docs", Value::from(docs.clone()));
673 }
674 if let Some(tracking_issue) = &metadata.tracking_issue {
675 inline.insert("tracking_issue", Value::from(tracking_issue.clone()));
676 }
677 if !metadata.requires.is_empty() {
678 let mut requires = Array::new();
679 for requirement in &metadata.requires {
680 requires.push(requirement.as_str());
681 }
682 inline.insert("requires", Value::Array(requires));
683 }
684
685 Value::InlineTable(inline)
686}
687
688fn ensure_child_table<'a>(parent: &'a mut Table, key: &str) -> Result<&'a mut Table> {
689 if !parent.contains_key(key) {
690 parent.insert(key, Item::Table(Table::new()));
691 }
692
693 parent[key]
694 .as_table_mut()
695 .ok_or_else(|| anyhow!("expected `{key}` to be a TOML table while editing the manifest"))
696}