#
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!(
    685            "\n\nhelp: a dependency with the same name exists in `{found_table}`"
    686        ));
    687    } else if let Some(alt_name) = alt_name {
    688        msg.push_str(&format!(
    689            "\n\nhelp: a dependency with a similar name exists: `{alt_name}`"
    690        ));
    691    }
    692    anyhow::format_err!(msg)
    693}
    694
    695fn remove_array_index(array: &mut toml_edit::Array, index: usize) {
    696    let value = array.remove(index);
    697
    698    // Captures all lines before leading whitespace
    699    let prefix_lines = value
    700        .decor()
    701        .prefix()
    702        .and_then(|p| p.as_str().expect("spans removed").rsplit_once('\n'))
    703        .map(|(lines, _current)| lines);
    704    // Captures all lines after trailing whitespace, before the next comma
    705    let suffix_lines = value
    706        .decor()
    707        .suffix()
    708        .and_then(|p| p.as_str().expect("spans removed").split_once('\n'))
    709        .map(|(_current, lines)| lines);
    710    let mut merged_lines = String::new();
    711    if let Some(prefix_lines) = prefix_lines {
    712        merged_lines.push_str(prefix_lines);
    713        merged_lines.push('\n');
    714    }
    715    if let Some(suffix_lines) = suffix_lines {
    716        merged_lines.push_str(suffix_lines);
    717        merged_lines.push('\n');
    718    }
    719
    720    let next_index = index; // Since `index` was removed, that effectively auto-advances us
    721    if let Some(next) = array.get_mut(next_index) {
    722        let next_decor = next.decor_mut();
    723        let next_prefix = next_decor
    724            .prefix()
    725            .map(|s| s.as_str().expect("spans removed"))
    726            .unwrap_or_default();
    727        merged_lines.push_str(next_prefix);
    728        next_decor.set_prefix(merged_lines);
    729    } else {
    730        let trailing = array.trailing().as_str().expect("spans removed");
    731        merged_lines.push_str(trailing);
    732        array.set_trailing(merged_lines);
    733    }
    734}