Skip to content

Commit 822f404

Browse files
max-sixtyclaude
andauthored
Fix inline snapshot corruption with multiple snapshots in with_settings! (#858)
## Summary Fixes #857 When `find_snapshot_macro(line)` was called for a line inside a macro like `with_settings!`, the `scan_nested_macros` function would scan all nested tokens and find all `@"..."` patterns. Since `try_extract_snapshot` always overwrote the result, the **last** snapshot found would win regardless of which line we were searching for. This caused: 1. All searches for lines inside `with_settings!` returned the same (last) snapshot position 2. The duplicate detection logic discarded all but the first pending snapshot 3. The first pending snapshot's content got written to the last snapshot's position **Example from the issue:** ```rust insta::with_settings!({filters => vec![...]}, { assert_snapshot!(":12345\n\nabc", @""); // line 10 assert_snapshot!(":12345\n\nabc", @""); // line 11 assert_snapshot!("", @""); // line 12 assert_snapshot!("", @""); // line 13 - incorrectly got content from line 10! }); ``` ## The Fix Instead of filtering during scanning, we now: 1. **Collect all snapshots** with their macro boundaries (start/end line) 2. **Filter at the end** to find the one whose macro span contains the target line This approach is more general because: - The scanning logic collects everything uniformly - The line-matching logic is centralized in one place - It works at any nesting depth ## Test plan - [x] All existing tests pass (111 tests) - [x] Added regression test `test_find_snapshot_macro_multiple_in_with_settings` - [x] Manually verified with the reproduction case from the issue 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7d27e3a commit 822f404

File tree

1 file changed

+72
-12
lines changed

1 file changed

+72
-12
lines changed

cargo-insta/src/inline.rs

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ impl FilePatcher {
125125
}
126126

127127
fn find_snapshot_macro(&self, line: usize) -> Option<InlineSnapshot> {
128-
struct Visitor<'a>(usize, Option<InlineSnapshot>, &'a [String]);
128+
// Stores (macro_start_line, macro_end_line, snapshot) for all found snapshots
129+
struct Visitor<'a>(usize, Vec<(usize, usize, InlineSnapshot)>, &'a [String]);
129130

130131
fn indentation(macro_start: LineColumn, code_lines: &[String]) -> String {
131132
// Only capture leading whitespace from the line, not arbitrary code
@@ -161,16 +162,24 @@ impl FilePatcher {
161162
fn scan_nested_macros(&mut self, tokens: &[TokenTree]) {
162163
for idx in 0..tokens.len() {
163164
// Look for the start of a macro (potential snapshot location)
164-
if let Some(TokenTree::Ident(_)) = tokens.get(idx) {
165+
if let Some(TokenTree::Ident(ident)) = tokens.get(idx) {
165166
if let Some(TokenTree::Punct(ref punct)) = tokens.get(idx + 1) {
166167
if punct.as_char() == '!' {
167168
if let Some(TokenTree::Group(ref group)) = tokens.get(idx + 2) {
168169
// Found a macro, determine its indentation
169170
let indentation = scan_for_path_start(tokens, idx, self.2);
171+
// Get macro span for later filtering
172+
let macro_start = ident.span().start().line;
173+
let macro_end = group.span().end().line;
170174
// Extract tokens from the macro arguments
171175
let tokens: Vec<_> = group.stream().into_iter().collect();
172176
// Try to extract a snapshot, passing the calculated indentation
173-
self.try_extract_snapshot(&tokens, indentation);
177+
self.try_extract_snapshot(
178+
&tokens,
179+
indentation,
180+
macro_start,
181+
macro_end,
182+
);
174183
}
175184
}
176185
}
@@ -185,7 +194,13 @@ impl FilePatcher {
185194
}
186195
}
187196

188-
fn try_extract_snapshot(&mut self, tokens: &[TokenTree], indentation: String) -> bool {
197+
fn try_extract_snapshot(
198+
&mut self,
199+
tokens: &[TokenTree],
200+
indentation: String,
201+
macro_start: usize,
202+
macro_end: usize,
203+
) -> bool {
189204
// ignore optional trailing comma
190205
let tokens = match tokens.last() {
191206
Some(TokenTree::Punct(ref punct)) if punct.as_char() == ',' => {
@@ -216,11 +231,15 @@ impl FilePatcher {
216231
_ => return false,
217232
};
218233

219-
self.1 = Some(InlineSnapshot {
220-
start,
221-
end,
222-
indentation,
223-
});
234+
self.1.push((
235+
macro_start,
236+
macro_end,
237+
InlineSnapshot {
238+
start,
239+
end,
240+
indentation,
241+
},
242+
));
224243
true
225244
}
226245
}
@@ -272,7 +291,7 @@ impl FilePatcher {
272291
}
273292

274293
let indentation = indentation(span_start, self.2);
275-
if !self.try_extract_snapshot(&tokens, indentation) {
294+
if !self.try_extract_snapshot(&tokens, indentation, start, end) {
276295
// if we can't extract a snapshot here we want to scan for nested
277296
// macros. These are just represented as unparsed tokens in a
278297
// token stream.
@@ -281,9 +300,15 @@ impl FilePatcher {
281300
}
282301
}
283302

284-
let mut visitor = Visitor(line, None, &self.lines);
303+
let mut visitor = Visitor(line, Vec::new(), &self.lines);
285304
syn::visit::visit_file(&mut visitor, &self.source);
286-
visitor.1
305+
306+
// Find the snapshot whose macro span contains the target line
307+
visitor
308+
.1
309+
.into_iter()
310+
.find(|(macro_start, macro_end, _)| line >= *macro_start && line <= *macro_end)
311+
.map(|(_, _, snapshot)| snapshot)
287312
}
288313
}
289314

@@ -664,4 +689,39 @@ fn test_function() {
664689
// even though scan_for_path_start finds "insta" as the path start
665690
assert_debug_snapshot!(snapshot.indentation, @r#"" ""#);
666691
}
692+
693+
#[test]
694+
fn test_find_snapshot_macro_multiple_in_with_settings() {
695+
// Regression test for issue #857: multiple snapshots inside with_settings!
696+
// Each snapshot should be found at its own line, not the last one.
697+
let content = r######"
698+
fn test_function() {
699+
insta::with_settings!({filters => vec![]}, {
700+
assert_snapshot!("a", @"a"); // line 4
701+
assert_snapshot!("b", @"b"); // line 5
702+
assert_snapshot!("c", @"c"); // line 6
703+
assert_snapshot!("d", @"d"); // line 7
704+
});
705+
}
706+
"######;
707+
708+
let file_patcher = FilePatcher {
709+
filename: PathBuf::new(),
710+
lines: content.lines().map(String::from).collect(),
711+
source: syn::parse_file(content).unwrap(),
712+
inline_snapshots: vec![],
713+
};
714+
715+
// Each line should find its own snapshot, not the last one
716+
let snapshot4 = file_patcher.find_snapshot_macro(4).unwrap();
717+
let snapshot5 = file_patcher.find_snapshot_macro(5).unwrap();
718+
let snapshot6 = file_patcher.find_snapshot_macro(6).unwrap();
719+
let snapshot7 = file_patcher.find_snapshot_macro(7).unwrap();
720+
721+
// Verify each snapshot is at the correct line (0-indexed)
722+
assert_eq!(snapshot4.start.0, 3); // line 4 -> index 3
723+
assert_eq!(snapshot5.start.0, 4); // line 5 -> index 4
724+
assert_eq!(snapshot6.start.0, 5); // line 6 -> index 5
725+
assert_eq!(snapshot7.start.0, 6); // line 7 -> index 6
726+
}
667727
}

0 commit comments

Comments
 (0)