1crate::ix!();
3
4#[derive(Builder,Setters,Getters,Debug,Clone)]
5#[getset(get="pub", set="pub")]
6#[builder(setter(into))]
7pub struct InterstitialSegment {
8 text_range: TextRange,
11
12 text: String,
14
15 file_path: PathBuf,
17
18 crate_path: PathBuf,
20}
21
22pub fn gather_interstitial_segments(
23 source_file: &SourceFile,
24 recognized_items: &[ConsolidatedItem],
25 file_path: &PathBuf,
26 crate_path: &PathBuf,
27) -> Vec<InterstitialSegment> {
28 trace!("Entering gather_interstitial_segments.");
29 let file_syntax = source_file.syntax();
30 let file_text = file_syntax.text();
31 let file_range = file_syntax.text_range();
32 debug!("Entire file range: {:?}", file_range);
33
34 let mut item_ranges = Vec::new();
36 for item in recognized_items {
37 let rng = match item {
38 ConsolidatedItem::Fn(ci) => *ci.effective_range(),
39 ConsolidatedItem::Struct(ci) => *ci.effective_range(),
40 ConsolidatedItem::Enum(ci) => *ci.effective_range(),
41 ConsolidatedItem::Trait(ci) => *ci.effective_range(),
42 ConsolidatedItem::TypeAlias(ci) => *ci.effective_range(),
43 ConsolidatedItem::Macro(ci) => *ci.effective_range(),
44 ConsolidatedItem::MacroCall(ci) => *ci.effective_range(),
45 ConsolidatedItem::ImplBlock(ib) => *ib.text_range(),
46 ConsolidatedItem::Module(mo) => *mo.text_range(),
47 ConsolidatedItem::MockTest(_) => continue,
48 };
49 item_ranges.push(rng);
50 }
51 item_ranges.sort_by_key(|r| r.start());
52
53 let mut segments = Vec::new();
55 let mut current_pos = file_range.start();
56
57 for &r in &item_ranges {
58 if r.start() > current_pos {
59 let gap_range = TextRange::new(current_pos, r.start());
60 let gap_txt = file_text.slice(gap_range).to_string();
61 if !gap_txt.is_empty() {
62 segments.push(
63 InterstitialSegmentBuilder::default()
64 .text_range(gap_range)
65 .text(gap_txt)
66 .file_path(file_path.clone())
67 .crate_path(crate_path.clone())
68 .build()
69 .unwrap()
70 );
71 }
72 }
73 current_pos = r.end();
74 }
75 if current_pos < file_range.end() {
77 let gap_range = TextRange::new(current_pos, file_range.end());
78 let gap_txt = file_text.slice(gap_range).to_string();
79 if !gap_txt.is_empty() {
80 segments.push(
81 InterstitialSegmentBuilder::default()
82 .text_range(gap_range)
83 .text(gap_txt)
84 .file_path(file_path.clone())
85 .crate_path(crate_path.clone())
86 .build()
87 .unwrap()
88 );
89 }
90 }
91
92 debug!("Gathered {} interstitial segments.", segments.len());
93
94 if recognized_items.len() == 1 && !segments.is_empty() {
100 let first_segment = &segments[0];
101 if first_segment.text_range().start() < TextSize::from(2) && first_segment.text().trim().is_empty()
105 {
106 segments.remove(0);
107 }
108 }
109
110 segments
111}
112
113#[cfg(test)]
114mod test_interstitial_segments_exhaustive {
115 use super::*;
116
117 fn parse_source_file(snippet: &str) -> SourceFile {
119 SourceFile::parse(snippet, Edition::Edition2021).tree()
120 }
121
122 fn gather_items_and_segments(
126 snippet: &str,
127 ) -> (Vec<ConsolidatedItem>, Vec<InterstitialSegment>) {
128 let sf = parse_source_file(snippet);
129
130 let file_path = PathBuf::from("EXHAUSTIVE_TEST_FILE.rs");
131 let crate_path = PathBuf::from("FAKE_CRATE_ROOT");
132
133 let opts = ConsolidationOptions::new().with_private_items().with_docs().with_test_items();
135
136 let items = gather_items_in_node(sf.syntax(), &opts, &file_path, &crate_path);
137 let segments = gather_interstitial_segments(&sf, &items, &file_path, &crate_path);
138
139 (items, segments)
140 }
141
142 #[traced_test]
143 fn test_all_interstitial_empty_file() {
144 info!("Scenario: empty file => the entire file is one interstitial segment or zero if no content.");
145
146 let snippet = "";
147 let (items, segments) = gather_items_and_segments(snippet);
148
149 assert!(items.is_empty(), "No recognized items in an empty file");
150 assert_eq!(
151 segments.len(),
152 0,
153 "No text at all => zero interstitial segments"
154 );
155 }
156
157 #[traced_test]
158 fn test_all_interstitial_file_with_only_whitespace_and_comments() {
159 info!("Scenario: file has no items, only whitespace and line/block comments => single large interstitial segment.");
160
161 let snippet = r#"
162 // Leading comment
163 // Another line
164 /*
165 Block comment, multi-line
166 */
167
168 "#;
169 let (items, segments) = gather_items_and_segments(snippet);
170
171 assert!(items.is_empty(), "Still no recognized items");
172 assert_eq!(
173 segments.len(),
174 1,
175 "All text should be in a single interstitial segment"
176 );
177 let seg_text = segments[0].text();
178 assert!(
179 seg_text.contains("Leading comment") && seg_text.contains("Block comment"),
180 "Segment should contain all the comment text"
181 );
182 }
183
184 #[traced_test]
185 fn test_item_at_file_start_no_leading_interstitial() {
186 info!("Scenario: snippet begins immediately with an item, so there's no leading gap. Then trailing whitespace.");
187
188 let snippet = r#"
189fn immediate() {
190 // body
191}
192
193 // trailing lines
194 "#;
195
196 let (items, segments) = gather_items_and_segments(snippet);
197 assert_eq!(items.len(), 1, "Should find exactly one function item");
199 assert_eq!(segments.len(), 1, "Just one trailing gap after the item");
200 let trailing_text = segments[0].text();
201 debug!("Trailing text: {:?}", trailing_text);
202 assert!(trailing_text.contains("trailing lines"), "Should contain the trailing comment/whitespace");
203 }
204
205 #[traced_test]
206 fn test_items_back_to_back_no_mid_gap() {
207 info!("Scenario: snippet with two items declared consecutively without any extra whitespace/comment in between.");
208
209 let snippet = r#"
210fn first() {}
211fn second() {}
212"#;
213
214 let (items, segments) = gather_items_and_segments(snippet);
215
216 assert_eq!(items.len(), 2, "Two recognized functions");
220 debug!("Found {} interstitial segments total.", segments.len());
222
223 for seg in &segments {
227 assert!(!seg.text().contains("fn first"));
228 assert!(!seg.text().contains("fn second"));
229 }
230 }
231
232 #[traced_test]
233 fn test_intermittent_whitespace_comments_between_items() {
234 info!("Scenario: snippet with multiple items, each separated by lines or comments => each becomes an interstitial segment.");
235
236 let snippet = r#"
237fn alpha() {}
238
239// middle block comment
240/*
241 big block
242*/
243fn beta() {}
244
245// line comment at end
246"#;
247 let (items, segments) = gather_items_and_segments(snippet);
248
249 assert_eq!(items.len(), 2, "Should see alpha() and beta() as 2 items");
250 assert!(
251 segments.len() >= 2,
252 "We expect at least 2 segments: between items, and trailing"
253 );
254
255 let mut combined_seg_text = String::new();
257 for seg in &segments {
258 combined_seg_text.push_str(seg.text());
259 }
260 assert!(combined_seg_text.contains("middle block comment"));
261 assert!(combined_seg_text.contains("big block"));
262 assert!(combined_seg_text.contains("line comment at end"));
263 }
264
265 #[traced_test]
266 fn test_doc_comments_are_not_interstitial() {
267 info!("Scenario: doc comments (/// lines) attached to an item do NOT appear in interstitial segments, because they're recognized as doc comments on the item.");
268
269 let snippet = r#"
270/// This doc comment belongs to alpha
271fn alpha() {}
272
273fn beta() {}
274"#;
275
276 let (items, segments) = gather_items_and_segments(snippet);
277
278 assert_eq!(items.len(), 2, "We have 2 recognized fns");
280 let combined_seg_text: String = segments.iter().map(|s| s.text().to_string()).collect();
283 assert!(
284 !combined_seg_text.contains("This doc comment belongs to alpha"),
285 "Doc comment should attach to the item, not appear in interstitial"
286 );
287 }
288
289 #[traced_test]
290 fn test_line_comment_between_items_is_interstitial() {
291 info!("Scenario: a normal `//` line comment between two items => interstitial segment should contain it.");
292
293 let snippet = r#"
294fn one() {}
295// normal comment
296fn two() {}
297"#;
298
299 let (items, segments) = gather_items_and_segments(snippet);
300
301 assert_eq!(items.len(), 2, "Two recognized fns");
302 let found_comment = segments.iter().any(|seg| seg.text().contains("normal comment"));
304 assert!(
305 found_comment,
306 "Should find 'normal comment' in an interstitial gap"
307 );
308 }
309
310 #[traced_test]
311 fn test_impl_block_and_surrounding_comments() {
312 info!("Scenario: have an impl block, plus some leading/trailing comments and whitespace.");
313
314 let snippet = r#"
315// Leading block
316impl Thing {
317 fn method(&self) {}
318}
319// Trailing block
320"#;
321
322 let (items, segments) = gather_items_and_segments(snippet);
323
324 let impl_count = items.iter().filter(|i| matches!(i, ConsolidatedItem::ImplBlock(_))).count();
326 assert_eq!(impl_count, 1, "We should find exactly one ImplBlock item.");
327
328 let text_joined: String = segments.iter().map(|seg| seg.text().to_string()).collect();
331 assert!(text_joined.contains("Leading block"), "Leading comment should be in interstitial");
332 assert!(text_joined.contains("Trailing block"), "Trailing comment should be in interstitial");
333 }
334
335 #[traced_test]
336 fn test_nested_modules_only_top_level_whitespace_is_interstitial() {
337 info!("Scenario: we have a nested mod; the outer mod is recognized as an item, the inner mod is recognized as an item, etc. We check that top-level whitespace is interstitial, but inside mod is not (since it's part of that mod's content).");
338
339 let snippet = r#"
340// top-level leading
341mod outer {
342 mod inner {
343 fn inside() {}
344 }
345}
346// trailing
347"#;
348
349 let (items, segments) = gather_items_and_segments(snippet);
350
351 let mod_count = items.iter().filter(|i| matches!(i, ConsolidatedItem::Module(_))).count();
354 assert_eq!(mod_count, 1, "One top-level module recognized");
355
356 let text_joined: String = segments.iter().map(|s| s.text().to_string()).collect();
358 assert!(text_joined.contains("top-level leading"));
359 assert!(text_joined.contains("trailing"));
360 }
362
363 #[traced_test]
364 fn test_cfg_test_comment_handling() {
365 info!("Scenario: a test function with #[cfg(test)], plus normal comments around it. The normal comment lines around it are interstitial, but the cfg-attribute is attached to the item (if recognized).");
366
367 let snippet = r#"
368// leading normal comment
369#[cfg(test)]
370fn test_something() {
371 // inside body
372}
373// trailing normal comment
374"#;
375
376 let (items, segments) = gather_items_and_segments(snippet);
377
378 assert_eq!(items.len(), 1, "One recognized fn with cfg(test).");
380
381 let text_joined: String = segments.iter().map(|s| s.text().to_string()).collect();
383 assert!(text_joined.contains("leading normal comment"));
384 assert!(text_joined.contains("trailing normal comment"));
385
386 assert!(!text_joined.contains("#[cfg(test)]"), "cfg(test) belongs to the item, not an interstitial gap");
388 }
389
390 #[traced_test]
391 fn test_multiple_enums_structs_fns_and_interstitials() {
392 info!("Scenario: a more complicated snippet with multiple items, each separated by a variety of comments, plus trailing lines. We'll verify everything is recognized or placed in the correct segments.");
393
394 let snippet = r#"
395enum E1 { A, B }
396
397// mid comment
398struct S1;
399
400fn function_one() {}
401
402// more mid lines
403enum E2 {}
404/* block comment mid */
405
406struct S2 {}
407fn function_two() {}
408// final
409"#;
410
411 let (items, segments) = gather_items_and_segments(snippet);
412
413 assert_eq!(items.len(), 6, "Should parse 6 recognized items in total");
415 let text_joined: String = segments.iter().map(|s| s.text().to_string()).collect();
416
417 assert!(text_joined.contains("mid comment"));
418 assert!(text_joined.contains("more mid lines"));
419 assert!(text_joined.contains("block comment mid"));
420 assert!(text_joined.contains("// final"));
421 }
422}