1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
//! Names a docs snippet's loop bindings so they cannot collide with what the snippet has already
//! bound where the loop is emitted.
//!
//! A fixture's `iterate` operation carries the loop variable's name verbatim, and nothing stopped
//! an author from picking the name the call's result is already bound to. Every generator then
//! rendered the collection accessor off the result and the loop binding off the same word:
//!
//! ```text
//! for (const result of result.results ?? []) // TS2448 / TS7022
//! for (var result : result.results()) // error: variable result is already defined
//! ```
//!
//! Rust, Python and Go accept that (their loop binding shadows from the body onward, and the
//! iterated expression is evaluated in the enclosing scope), which is why the name reached a
//! published fixture at all — the snippet reads fine in the languages that permit shadowing and
//! does not compile in the ones that do not. The name is a generated-code decision either way, so
//! it is decided here, once, and applied by rewriting the *operation* before any generator
//! resolves it: the per-item field accessors are rendered from `item` as their root, so renaming
//! afterwards would mean rewriting rendered expressions instead of naming them right the first
//! time. ~keep
use std::borrow::Cow;
use std::collections::BTreeSet;
use crate::e2e::fixture::{Fixture, FixtureDocsOperation};
/// The loop-binding name used when the fixture's own choice is unavailable.
///
/// A plain noun rather than a decorated form of the author's word (`resultItem`, `result2`): it
/// needs no case conversion to read idiomatically in either a camelCase or a snake_case target,
/// and this module deliberately owns no naming policy. It is also the name the fixture schema's
/// own `iterate` example uses. ~keep
const FALLBACK_ITEM_NAME: &str = "item";
/// `fixture` with every `iterate` operation whose loop binding would collide with one of
/// `bound_names` renamed, or the fixture untouched when none would.
///
/// `bound_names` is what the caller's emitted snippet has in scope where the loop lands — at
/// minimum the call's result variable, which is the collision that actually shipped. A caller that
/// binds more names (a destructured element, a client handle) passes those too.
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()))
})
})
}
/// [`FALLBACK_ITEM_NAME`], or the first numbered variant of it that is free.
///
/// Sibling loops may share a name — each `for` opens its own scope in both targets — so only the
/// caller's bound names are avoided, and the common case never reaches a suffix.
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 {
sample_url: None,
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(),
sample_url_vars: Default::default(),
body_file: None,
}),
..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()]);
}
}