cargo/util/toml_mut/
manifest.rs

1//! Parsing and editing of manifest files.
2
3use std::ops::{Deref, DerefMut};
4use std::path::{Path, PathBuf};
5use std::str;
6
7use anyhow::Context as _;
8
9use super::dependency::Dependency;
10use crate::core::dependency::DepKind;
11use crate::core::{FeatureValue, Features, Workspace};
12use crate::util::closest;
13use crate::util::frontmatter::ScriptSource;
14use crate::util::toml::is_embedded;
15use crate::{CargoResult, GlobalContext};
16
17/// Dependency table to add deps to.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct DepTable {
20    kind: DepKind,
21    target: Option<String>,
22}
23
24impl DepTable {
25    const KINDS: &'static [Self] = &[
26        Self::new().set_kind(DepKind::Normal),
27        Self::new().set_kind(DepKind::Development),
28        Self::new().set_kind(DepKind::Build),
29    ];
30
31    /// Reference to a Dependency Table.
32    pub const fn new() -> Self {
33        Self {
34            kind: DepKind::Normal,
35            target: None,
36        }
37    }
38
39    /// Choose the type of dependency.
40    pub const fn set_kind(mut self, kind: DepKind) -> Self {
41        self.kind = kind;
42        self
43    }
44
45    /// Choose the platform for the dependency.
46    pub fn set_target(mut self, target: impl Into<String>) -> Self {
47        self.target = Some(target.into());
48        self
49    }
50
51    /// Type of dependency.
52    pub fn kind(&self) -> DepKind {
53        self.kind
54    }
55
56    /// Platform for the dependency.
57    pub fn target(&self) -> Option<&str> {
58        self.target.as_deref()
59    }
60
61    /// Keys to the table.
62    pub fn to_table(&self) -> Vec<&str> {
63        if let Some(target) = &self.target {
64            vec!["target", target, self.kind.kind_table()]
65        } else {
66            vec![self.kind.kind_table()]
67        }
68    }
69}
70
71impl Default for DepTable {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl From<DepKind> for DepTable {
78    fn from(other: DepKind) -> Self {
79        Self::new().set_kind(other)
80    }
81}
82
83/// An editable Cargo manifest.
84#[derive(Debug, Clone)]
85pub struct Manifest {
86    /// Manifest contents as TOML data.
87    pub data: toml_edit::DocumentMut,
88}
89
90impl Manifest {
91    /// Get the manifest's package name.
92    pub fn package_name(&self) -> CargoResult<&str> {
93        self.data
94            .as_table()
95            .get("package")
96            .and_then(|m| m.get("name"))
97            .and_then(|m| m.as_str())
98            .ok_or_else(parse_manifest_err)
99    }
100
101    /// Get the specified table from the manifest.
102    pub fn get_table<'a>(&'a self, table_path: &[String]) -> CargoResult<&'a toml_edit::Item> {
103        /// Descend into a manifest until the required table is found.
104        fn descend<'a>(
105            input: &'a toml_edit::Item,
106            path: &[String],
107        ) -> CargoResult<&'a toml_edit::Item> {
108            if let Some(segment) = path.get(0) {
109                let value = input
110                    .get(&segment)
111                    .ok_or_else(|| non_existent_table_err(segment))?;
112
113                if value.is_table_like() {
114                    descend(value, &path[1..])
115                } else {
116                    Err(non_existent_table_err(segment))
117                }
118            } else {
119                Ok(input)
120            }
121        }
122
123        descend(self.data.as_item(), table_path)
124    }
125
126    /// Get the specified table from the manifest.
127    pub fn get_table_mut<'a>(
128        &'a mut self,
129        table_path: &[String],
130    ) -> CargoResult<&'a mut toml_edit::Item> {
131        /// Descend into a manifest until the required table is found.
132        fn descend<'a>(
133            input: &'a mut toml_edit::Item,
134            path: &[String],
135        ) -> CargoResult<&'a mut toml_edit::Item> {
136            if let Some(segment) = path.get(0) {
137                let mut default_table = toml_edit::Table::new();
138                default_table.set_implicit(true);
139                let value = input[&segment].or_insert(toml_edit::Item::Table(default_table));
140
141                if value.is_table_like() {
142                    descend(value, &path[1..])
143                } else {
144                    Err(non_existent_table_err(segment))
145                }
146            } else {
147                Ok(input)
148            }
149        }
150
151        descend(self.data.as_item_mut(), table_path)
152    }
153
154    /// Get all sections in the manifest that exist and might contain
155    /// dependencies. The returned items are always `Table` or
156    /// `InlineTable`.
157    pub fn get_sections(&self) -> Vec<(DepTable, toml_edit::Item)> {
158        let mut sections = Vec::new();
159
160        for table in DepTable::KINDS {
161            let dependency_type = table.kind.kind_table();
162            // Dependencies can be in the three standard sections...
163            if self
164                .data
165                .get(dependency_type)
166                .map(|t| t.is_table_like())
167                .unwrap_or(false)
168            {
169                sections.push((table.clone(), self.data[dependency_type].clone()))
170            }
171
172            // ... and in `target.<target>.(build-/dev-)dependencies`.
173            let target_sections = self
174                .data
175                .as_table()
176                .get("target")
177                .and_then(toml_edit::Item::as_table_like)
178                .into_iter()
179                .flat_map(toml_edit::TableLike::iter)
180                .filter_map(|(target_name, target_table)| {
181                    let dependency_table = target_table.get(dependency_type)?;
182                    dependency_table.as_table_like().map(|_| {
183                        (
184                            table.clone().set_target(target_name),
185                            dependency_table.clone(),
186                        )
187                    })
188                });
189
190            sections.extend(target_sections);
191        }
192
193        sections
194    }
195
196    pub fn get_legacy_sections(&self) -> Vec<String> {
197        let mut result = Vec::new();
198
199        for dependency_type in ["dev_dependencies", "build_dependencies"] {
200            if self.data.contains_key(dependency_type) {
201                result.push(dependency_type.to_owned());
202            }
203
204            // ... and in `target.<target>.(build-/dev-)dependencies`.
205            result.extend(
206                self.data
207                    .as_table()
208                    .get("target")
209                    .and_then(toml_edit::Item::as_table_like)
210                    .into_iter()
211                    .flat_map(toml_edit::TableLike::iter)
212                    .filter_map(|(target_name, target_table)| {
213                        if target_table.as_table_like()?.contains_key(dependency_type) {
214                            Some(format!("target.{target_name}.{dependency_type}"))
215                        } else {
216                            None
217                        }
218                    }),
219            );
220        }
221        result
222    }
223}
224
225impl str::FromStr for Manifest {
226    type Err = anyhow::Error;
227
228    /// Read manifest data from string
229    fn from_str(input: &str) -> ::std::result::Result<Self, Self::Err> {
230        let d: toml_edit::DocumentMut = input.parse().context("Manifest not valid TOML")?;
231
232        Ok(Manifest { data: d })
233    }
234}
235
236impl std::fmt::Display for Manifest {
237    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
238        self.data.fmt(f)
239    }
240}
241
242/// An editable Cargo manifest that is available locally.
243#[derive(Debug, Clone)]
244pub struct LocalManifest {
245    /// Path to the manifest.
246    pub path: PathBuf,
247    /// Manifest contents.
248    pub manifest: Manifest,
249    /// The raw, unparsed package file
250    pub raw: String,
251    /// Edit location for an embedded manifest, if relevant
252    pub embedded: Option<Embedded>,
253}
254
255impl Deref for LocalManifest {
256    type Target = Manifest;
257
258    fn deref(&self) -> &Manifest {
259        &self.manifest
260    }
261}
262
263impl DerefMut for LocalManifest {
264    fn deref_mut(&mut self) -> &mut Manifest {
265        &mut self.manifest
266    }
267}
268
269impl LocalManifest {
270    /// Construct the `LocalManifest` corresponding to the `Path` provided..
271    pub fn try_new(path: &Path) -> CargoResult<Self> {
272        if !path.is_absolute() {
273            anyhow::bail!("can only edit absolute paths, got {}", path.display());
274        }
275        let raw = cargo_util::paths::read(&path)?;
276        let mut data = raw.clone();
277        let mut embedded = None;
278        if is_embedded(path) {
279            let source = ScriptSource::parse(&data)?;
280            if let Some(frontmatter) = source.frontmatter_span() {
281                embedded = Some(Embedded::exists(frontmatter));
282                data = source.frontmatter().unwrap().to_owned();
283            } else if let Some(shebang) = source.shebang_span() {
284                embedded = Some(Embedded::after(shebang));
285                data = String::new();
286            } else {
287                embedded = Some(Embedded::start());
288                data = String::new();
289            }
290        }
291        let manifest = data.parse().context("Unable to parse Cargo.toml")?;
292        Ok(LocalManifest {
293            manifest,
294            path: path.to_owned(),
295            raw,
296            embedded,
297        })
298    }
299
300    /// Write changes back to the file.
301    pub fn write(&self) -> CargoResult<()> {
302        let mut manifest = self.manifest.data.to_string();
303        let raw = match self.embedded.as_ref() {
304            Some(Embedded::Implicit(start)) => {
305                if !manifest.ends_with("\n") {
306                    manifest.push_str("\n");
307                }
308                let fence = "---\n";
309                let prefix = &self.raw[0..*start];
310                let suffix = &self.raw[*start..];
311                let empty_line = if prefix.is_empty() { "\n" } else { "" };
312                format!("{prefix}{fence}{manifest}{fence}{empty_line}{suffix}")
313            }
314            Some(Embedded::Explicit(span)) => {
315                if !manifest.ends_with("\n") {
316                    manifest.push_str("\n");
317                }
318                let prefix = &self.raw[0..span.start];
319                let suffix = &self.raw[span.end..];
320                format!("{prefix}{manifest}{suffix}")
321            }
322            None => manifest,
323        };
324        let new_contents_bytes = raw.as_bytes();
325
326        cargo_util::paths::write_atomic(&self.path, new_contents_bytes)
327    }
328
329    /// Lookup a dependency.
330    pub fn get_dependencies<'s>(
331        &'s self,
332        ws: &'s Workspace<'_>,
333        unstable_features: &'s Features,
334    ) -> impl Iterator<Item = (String, DepTable, CargoResult<Dependency>)> + 's {
335        let crate_root = self.path.parent().expect("manifest path is absolute");
336        self.get_sections()
337            .into_iter()
338            .filter_map(move |(table_path, table)| {
339                let table = table.into_table().ok()?;
340                Some(
341                    table
342                        .into_iter()
343                        .map(|(key, item)| (table_path.clone(), key, item))
344                        .collect::<Vec<_>>(),
345                )
346            })
347            .flatten()
348            .map(move |(table_path, dep_key, dep_item)| {
349                let dep = Dependency::from_toml(
350                    ws.gctx(),
351                    ws.root(),
352                    crate_root,
353                    unstable_features,
354                    &dep_key,
355                    &dep_item,
356                );
357                (dep_key, table_path, dep)
358            })
359    }
360
361    /// Add entry to a Cargo.toml.
362    pub fn insert_into_table(
363        &mut self,
364        table_path: &[String],
365        dep: &Dependency,
366        gctx: &GlobalContext,
367        workspace_root: &Path,
368        unstable_features: &Features,
369    ) -> CargoResult<()> {
370        let crate_root = self
371            .path
372            .parent()
373            .expect("manifest path is absolute")
374            .to_owned();
375        let dep_key = dep.toml_key();
376
377        let table = self.get_table_mut(table_path)?;
378        if let Some((mut dep_key, dep_item)) = table
379            .as_table_like_mut()
380            .unwrap()
381            .get_key_value_mut(dep_key)
382        {
383            dep.update_toml(
384                gctx,
385                workspace_root,
386                &crate_root,
387                unstable_features,
388                &mut dep_key,
389                dep_item,
390            )?;
391            if let Some(table) = dep_item.as_inline_table_mut() {
392                // So long as we don't have `Cargo.toml` auto-formatting and inline-tables can only
393                // be on one line, there isn't really much in the way of interesting formatting to
394                // include (no comments), so let's just wipe it clean
395                table.fmt();
396            }
397        } else {
398            let new_dependency =
399                dep.to_toml(gctx, workspace_root, &crate_root, unstable_features)?;
400            table[dep_key] = new_dependency;
401        }
402
403        Ok(())
404    }
405
406    /// Remove entry from a Cargo.toml.
407    pub fn remove_from_table(&mut self, table_path: &[String], name: &str) -> CargoResult<()> {
408        let parent_table = self.get_table_mut(table_path)?;
409
410        match parent_table.get_mut(name).filter(|t| !t.is_none()) {
411            Some(dep) => {
412                // remove the dependency
413                *dep = toml_edit::Item::None;
414
415                // remove table if empty
416                if parent_table.as_table_like().unwrap().is_empty() {
417                    *parent_table = toml_edit::Item::None;
418                }
419            }
420            None => {
421                let names = parent_table
422                    .as_table_like()
423                    .map(|t| t.iter())
424                    .into_iter()
425                    .flatten();
426                let alt_name = closest(name, names.map(|(k, _)| k), |k| k).map(|n| n.to_owned());
427
428                // Search in other tables.
429                let sections = self.get_sections();
430                let found_table_path = sections.iter().find_map(|(t, i)| {
431                    let table_path: Vec<String> =
432                        t.to_table().iter().map(|s| s.to_string()).collect();
433                    i.get(name).is_some().then(|| table_path.join("."))
434                });
435
436                return Err(non_existent_dependency_err(
437                    name,
438                    table_path.join("."),
439                    found_table_path,
440                    alt_name.as_deref(),
441                ));
442            }
443        }
444
445        Ok(())
446    }
447
448    /// Allow mutating dependencies, wherever they live.
449    /// Copied from cargo-edit.
450    pub fn get_dependency_tables_mut(
451        &mut self,
452    ) -> impl Iterator<Item = &mut dyn toml_edit::TableLike> + '_ {
453        let root = self.data.as_table_mut();
454        root.iter_mut().flat_map(|(k, v)| {
455            if DepTable::KINDS
456                .iter()
457                .any(|dt| dt.kind.kind_table() == k.get())
458            {
459                v.as_table_like_mut().into_iter().collect::<Vec<_>>()
460            } else if k == "workspace" {
461                v.as_table_like_mut()
462                    .unwrap()
463                    .iter_mut()
464                    .filter_map(|(k, v)| {
465                        if k.get() == "dependencies" {
466                            v.as_table_like_mut()
467                        } else {
468                            None
469                        }
470                    })
471                    .collect::<Vec<_>>()
472            } else if k == "target" {
473                v.as_table_like_mut()
474                    .unwrap()
475                    .iter_mut()
476                    .flat_map(|(_, v)| {
477                        v.as_table_like_mut().into_iter().flat_map(|v| {
478                            v.iter_mut().filter_map(|(k, v)| {
479                                if DepTable::KINDS
480                                    .iter()
481                                    .any(|dt| dt.kind.kind_table() == k.get())
482                                {
483                                    v.as_table_like_mut()
484                                } else {
485                                    None
486                                }
487                            })
488                        })
489                    })
490                    .collect::<Vec<_>>()
491            } else {
492                Vec::new()
493            }
494        })
495    }
496
497    /// Remove references to `dep_key` if its no longer present.
498    pub fn gc_dep(&mut self, dep_key: &str) {
499        let explicit_dep_activation = self.is_explicit_dep_activation(dep_key);
500        let status = self.dep_status(dep_key);
501
502        if let Some(toml_edit::Item::Table(feature_table)) =
503            self.data.as_table_mut().get_mut("features")
504        {
505            for (_feature, mut feature_values) in feature_table.iter_mut() {
506                if let toml_edit::Item::Value(toml_edit::Value::Array(feature_values)) =
507                    &mut feature_values
508                {
509                    fix_feature_activations(
510                        feature_values,
511                        dep_key,
512                        status,
513                        explicit_dep_activation,
514                    );
515                }
516            }
517        }
518    }
519
520    pub fn is_explicit_dep_activation(&self, dep_key: &str) -> bool {
521        if let Some(toml_edit::Item::Table(feature_table)) = self.data.as_table().get("features") {
522            for values in feature_table
523                .iter()
524                .map(|(_, a)| a)
525                .filter_map(|i| i.as_value())
526                .filter_map(|v| v.as_array())
527            {
528                for value in values.iter().filter_map(|v| v.as_str()) {
529                    let value = FeatureValue::new(value.into());
530                    if let FeatureValue::Dep { dep_name } = &value {
531                        if dep_name.as_str() == dep_key {
532                            return true;
533                        }
534                    }
535                }
536            }
537        }
538
539        false
540    }
541
542    fn dep_status(&self, dep_key: &str) -> DependencyStatus {
543        let mut status = DependencyStatus::None;
544        for (_, tbl) in self.get_sections() {
545            if let toml_edit::Item::Table(tbl) = tbl {
546                if let Some(dep_item) = tbl.get(dep_key) {
547                    let optional = dep_item
548                        .get("optional")
549                        .and_then(|i| i.as_value())
550                        .and_then(|i| i.as_bool())
551                        .unwrap_or(false);
552                    if optional {
553                        return DependencyStatus::Optional;
554                    } else {
555                        status = DependencyStatus::Required;
556                    }
557                }
558            }
559        }
560        status
561    }
562}
563
564impl std::fmt::Display for LocalManifest {
565    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
566        self.manifest.fmt(f)
567    }
568}
569
570/// Edit location for an embedded manifest
571#[derive(Clone, Debug)]
572pub enum Embedded {
573    /// Manifest is implicit
574    ///
575    /// This is the insert location for a frontmatter
576    Implicit(usize),
577    /// Manifest is explicit in a frontmatter
578    ///
579    /// This is the span of the frontmatter body
580    Explicit(std::ops::Range<usize>),
581}
582
583impl Embedded {
584    fn start() -> Self {
585        Self::Implicit(0)
586    }
587
588    fn after(after: std::ops::Range<usize>) -> Self {
589        Self::Implicit(after.end)
590    }
591
592    fn exists(exists: std::ops::Range<usize>) -> Self {
593        Self::Explicit(exists)
594    }
595}
596
597#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
598enum DependencyStatus {
599    None,
600    Optional,
601    Required,
602}
603
604fn fix_feature_activations(
605    feature_values: &mut toml_edit::Array,
606    dep_key: &str,
607    status: DependencyStatus,
608    explicit_dep_activation: bool,
609) {
610    let remove_list: Vec<usize> = feature_values
611        .iter()
612        .enumerate()
613        .filter_map(|(idx, value)| value.as_str().map(|s| (idx, s)))
614        .filter_map(|(idx, value)| {
615            let parsed_value = FeatureValue::new(value.into());
616            match status {
617                DependencyStatus::None => match (parsed_value, explicit_dep_activation) {
618                    (FeatureValue::Feature(dep_name), false)
619                    | (FeatureValue::Dep { dep_name }, _)
620                    | (FeatureValue::DepFeature { dep_name, .. }, _) => dep_name == dep_key,
621                    _ => false,
622                },
623                DependencyStatus::Optional => false,
624                DependencyStatus::Required => match (parsed_value, explicit_dep_activation) {
625                    (FeatureValue::Feature(dep_name), false)
626                    | (FeatureValue::Dep { dep_name }, _) => dep_name == dep_key,
627                    (FeatureValue::Feature(_), true) | (FeatureValue::DepFeature { .. }, _) => {
628                        false
629                    }
630                },
631            }
632            .then(|| idx)
633        })
634        .collect();
635
636    // Remove found idx in revers order so we don't invalidate the idx.
637    for idx in remove_list.iter().rev() {
638        remove_array_index(feature_values, *idx);
639    }
640
641    if status == DependencyStatus::Required {
642        for value in feature_values.iter_mut() {
643            let parsed_value = if let Some(value) = value.as_str() {
644                FeatureValue::new(value.into())
645            } else {
646                continue;
647            };
648            if let FeatureValue::DepFeature {
649                dep_name,
650                dep_feature,
651                weak,
652            } = parsed_value
653            {
654                if dep_name == dep_key && weak {
655                    let mut new_value = toml_edit::Value::from(format!("{dep_name}/{dep_feature}"));
656                    *new_value.decor_mut() = value.decor().clone();
657                    *value = new_value;
658                }
659            }
660        }
661    }
662}
663
664pub fn str_or_1_len_table(item: &toml_edit::Item) -> bool {
665    item.is_str() || item.as_table_like().map(|t| t.len() == 1).unwrap_or(false)
666}
667
668fn parse_manifest_err() -> anyhow::Error {
669    anyhow::format_err!("unable to parse external Cargo.toml")
670}
671
672fn non_existent_table_err(table: impl std::fmt::Display) -> anyhow::Error {
673    anyhow::format_err!("the table `{table}` could not be found.")
674}
675
676fn non_existent_dependency_err(
677    name: impl std::fmt::Display,
678    search_table: impl std::fmt::Display,
679    found_table: Option<impl std::fmt::Display>,
680    alt_name: Option<&str>,
681) -> anyhow::Error {
682    let mut msg = format!("the dependency `{name}` could not be found in `{search_table}`");
683    if let Some(found_table) = found_table {
684        msg.push_str(&format!("; it is present in `{found_table}`",));
685    } else if let Some(alt_name) = alt_name {
686        msg.push_str(&format!("; dependency `{alt_name}` exists",));
687    }
688    anyhow::format_err!(msg)
689}
690
691fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
692    let value = array.remove(index);
693
694    // Captures all lines before leading whitespace
695    let prefix_lines = value
696        .decor()
697        .prefix()
698        .and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
699        .map(|(lines, _current)| lines);
700    // Captures all lines after trailing whitespace, before the next comma
701    let suffix_lines = value
702        .decor()
703        .suffix()
704        .and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
705        .map(|(_current, lines)| lines);
706    let mut merged_lines = String::new();
707    if let Some(prefix_lines) = prefix_lines {
708        merged_lines.push_str(prefix_lines);
709        merged_lines.push('\n');
710    }
711    if let Some(suffix_lines) = suffix_lines {
712        merged_lines.push_str(suffix_lines);
713        merged_lines.push('\n');
714    }
715
716    let next_index = index; // Since `index` was removed, that effectively auto-advances us
717    if let Some(next) = array.get_mut(next_index) {
718        let next_decor = next.decor_mut();
719        let next_prefix = next_decor
720            .prefix()
721            .map(|s| s.as_str().expect("spans removed"))
722            .unwrap_or_default();
723        merged_lines.push_str(next_prefix);
724        next_decor.set_prefix(merged_lines);
725    } else {
726        let trailing = array.trailing().as_str().expect("spans removed");
727        merged_lines.push_str(trailing);
728        array.set_trailing(merged_lines);
729    }
730}