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