use std::borrow::Cow;
use std::collections::BTreeSet;
use crate::e2e::fixture::{Fixture, FixtureDocsOperation};
const FALLBACK_ITEM_NAME: &str = "item";
pub(crate) fn without_shadowed_loop_bindings<'a>(fixture: &'a Fixture, bound_names: &[&str]) -> Cow<'a, Fixture> {
let taken: BTreeSet<&str> = bound_names.iter().copied().collect();
if !shadows_any(fixture, &taken) {
return Cow::Borrowed(fixture);
}
let mut renamed = fixture.clone();
if let Some(presentation) = renamed.docs.as_mut().and_then(|docs| docs.presentation.as_mut()) {
for operation in &mut presentation.operations {
if let FixtureDocsOperation::Iterate { item, .. } = operation
&& taken.contains(item.as_str())
{
*item = unshadowed_name(&taken);
}
}
}
Cow::Owned(renamed)
}
fn shadows_any(fixture: &Fixture, taken: &BTreeSet<&str>) -> bool {
fixture
.docs
.as_ref()
.and_then(|docs| docs.presentation.as_ref())
.is_some_and(|presentation| {
presentation.operations.iter().any(|operation| {
matches!(operation, FixtureDocsOperation::Iterate { item, .. } if taken.contains(item.as_str()))
})
})
}
fn unshadowed_name(taken: &BTreeSet<&str>) -> String {
if !taken.contains(FALLBACK_ITEM_NAME) {
return FALLBACK_ITEM_NAME.to_string();
}
(2..)
.map(|suffix| format!("{FALLBACK_ITEM_NAME}{suffix}"))
.find(|candidate| !taken.contains(candidate.as_str()))
.expect("an unbounded sequence of candidate names always yields a free one")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::e2e::fixture::{FixtureDocs, FixtureDocsPresentation, SideEffectClass};
fn iterate_fixture(items: &[&str]) -> Fixture {
Fixture {
id: "list_entries".into(),
description: "List entries".into(),
docs: Some(FixtureDocs {
topic: "guides".into(),
stem: None,
paths: Default::default(),
title: None,
description: None,
input: None,
shows: Vec::new(),
error: None,
presentation: Some(FixtureDocsPresentation {
call: None,
input: None,
args: None,
files: Vec::new(),
operations: items
.iter()
.map(|item| FixtureDocsOperation::Iterate {
path: "results".into(),
item: (*item).to_string(),
fields: vec!["text".into()],
display: false,
optional: true,
})
.collect(),
}),
client: None,
side_effects: SideEffectClass::Safe,
coverage_exceptions: Default::default(),
}),
..Fixture::default()
}
}
fn item_names(fixture: &Fixture) -> Vec<String> {
fixture
.docs
.as_ref()
.and_then(|docs| docs.presentation.as_ref())
.map(|presentation| {
presentation
.operations
.iter()
.map(|operation| match operation {
FixtureDocsOperation::Iterate { item, .. } => item.clone(),
FixtureDocsOperation::Show { path, .. } => path.clone(),
})
.collect()
})
.unwrap_or_default()
}
#[test]
fn renames_a_loop_binding_that_shadows_the_result_variable() {
let fixture = iterate_fixture(&["result"]);
let renamed = without_shadowed_loop_bindings(&fixture, &["result"]);
assert_eq!(item_names(&renamed), vec!["item".to_string()]);
}
#[test]
fn leaves_a_loop_binding_that_collides_with_nothing_alone() {
let fixture = iterate_fixture(&["entry"]);
let renamed = without_shadowed_loop_bindings(&fixture, &["result"]);
assert!(matches!(renamed, Cow::Borrowed(_)), "no collision must not clone");
assert_eq!(item_names(&renamed), vec!["entry".to_string()]);
}
#[test]
fn skips_a_fallback_name_that_is_itself_bound() {
let fixture = iterate_fixture(&["item"]);
let renamed = without_shadowed_loop_bindings(&fixture, &["item"]);
assert_eq!(item_names(&renamed), vec!["item2".to_string()]);
}
#[test]
fn renames_only_the_operations_that_collide() {
let fixture = iterate_fixture(&["entry", "result"]);
let renamed = without_shadowed_loop_bindings(&fixture, &["result"]);
assert_eq!(item_names(&renamed), vec!["entry".to_string(), "item".to_string()]);
}
}