1pub use super::{TrimmedLiteral, TrimmedLiteralDisplay};
2
3use crate::{CheckableChunk, CommentVariant, Range};
4
5use std::fmt;
6
7#[derive(Clone, Default, Debug, Hash, PartialEq, Eq)]
11pub struct LiteralSet {
12 literals: Vec<TrimmedLiteral>,
14 pub coverage: (usize, usize),
16 variant: CommentVariant,
18}
19
20impl LiteralSet {
21 pub fn from(literal: TrimmedLiteral) -> Self {
23 Self {
24 coverage: (literal.span().start.line, literal.span().end.line),
25 variant: literal.variant(),
26 literals: vec![literal],
27 }
28 }
29
30 pub fn add_adjacent(&mut self, literal: TrimmedLiteral) -> Result<(), TrimmedLiteral> {
35 if literal.variant().category() != self.variant.category() {
36 log::debug!(
37 "Adjacent literal is not the same comment variant: {:?} vs {:?}",
38 literal.variant().category(),
39 self.variant.category()
40 );
41 return Err(literal);
42 }
43 let previous_line = literal.span().end.line;
44 if previous_line == self.coverage.1 + 1 {
45 self.coverage.1 += 1;
46 self.literals.push(literal);
47 return Ok(());
48 }
49
50 let next_line = literal.span().start.line;
51 if next_line + 1 == self.coverage.0 {
52 self.literals.push(literal);
53 self.coverage.1 -= 1;
54 return Ok(());
55 }
56
57 Err(literal)
58 }
59
60 pub fn literals(&self) -> Vec<&TrimmedLiteral> {
62 self.literals.iter().by_ref().collect()
63 }
64
65 pub fn len(&self) -> usize {
67 self.literals.len()
68 }
69
70 pub fn into_chunk(self) -> crate::CheckableChunk {
74 let n = self.len();
75 let mut source_mapping = indexmap::IndexMap::with_capacity(n);
76 let mut content = String::with_capacity(n * 120);
77 if n > 0 {
78 let mut cursor = 0usize;
80 let mut start; let mut end; let mut it = self.literals.iter();
84 let mut next = it.next();
85 while let Some(literal) = next {
86 start = cursor;
87 cursor += literal.len_in_chars();
88 end = cursor;
89
90 let span = literal.span();
91 let range = Range { start, end };
92
93 if literal.variant() != CommentVariant::MacroDocEqMacro {
97 if let Some(span_len) = span.one_line_len() {
98 assert_eq!(range.len(), span_len);
99 }
100 }
101 source_mapping.insert(range, span);
103 content.push_str(literal.as_str());
104 next = it.next();
106 if next.is_some() {
107 content.push('\n');
109 cursor += 1;
110 }
111 }
112 }
113 let variant = if let Some(literal) = self.literals.first() {
115 literal.variant()
116 } else {
117 crate::CommentVariant::Unknown
118 };
119 CheckableChunk::from_string(content, source_mapping, variant)
120 }
121}
122
123impl<'s> fmt::Display for LiteralSet {
124 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
125 let n = self.len();
126 if n > 0 {
127 for literal in self.literals.iter().take(n - 1) {
128 writeln!(formatter, "{}", literal.as_str())?;
129 }
130 if let Some(literal) = self.literals.last() {
131 write!(formatter, "{}", literal.as_str())?;
132 }
133 }
134 Ok(())
135 }
136}
137#[macro_export]
139macro_rules! chyrp_dbg {
140 ($first:literal $(, $( $line:literal ),+ )? $(,)? $(@ $prefix:literal)? ) => {
141 dbg!(concat!($first $( $(, "\n", $line )+ )?).len());
142 dbg!(concat!($first $( $(, "\n", $line )+ )?));
143 }
144}
145
146#[macro_export]
161macro_rules! chyrp_up {
162 ([ $( $line:literal ),+ $(,)? ] $(@ $prefix:literal)? ) => {
163 chyrp_up!( $( $line ),+ $(@ $prefix)? )
164 };
165 ($first:literal $(, $( $line:literal ),+ )? $(,)? $(@ $prefix:literal)? ) => {
166 concat!($( $prefix ,)? r##"#[doc=r#""##, $first $( $(, "\n", $line )+ )?, r##""#]"##, "\n", "struct ChyrpChyrp;")
167 };
168}
169
170#[macro_export]
185macro_rules! fluff_up {
186 ([ $( $line:literal ),+ $(,)?] $( @ $prefix:literal)?) => {
187 fluff_up!($( $line ),+ $(@ $prefix)?)
188 };
189 ($($line:literal ),+ $(,)? ) => {
190 fluff_up!($( $line ),+ @ "")
191 };
192 ($($line:literal ),+ $(,)? @ $prefix:literal ) => {
193 concat!("" $(, $prefix, "/// ", $line, "\n")+ , "struct Fluff;")
194 };
195}
196
197pub mod testhelper {
198 use super::*;
199 use crate::testcase::annotated_literals;
200
201 pub fn gen_literal_set(source: &str) -> LiteralSet {
202 let literals = dbg!(annotated_literals(dbg!(source)));
203
204 let mut iter = dbg!(literals).into_iter();
205 let literal = iter
206 .next()
207 .expect("Must have at least one item in laterals");
208 let mut cls = LiteralSet::from(literal);
209
210 for literal in iter {
211 assert!(cls.add_adjacent(literal).is_ok());
212 }
213 dbg!(cls)
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 use super::testhelper::gen_literal_set;
222 use crate::util::load_span_from;
223 use crate::util::sub_chars;
224
225 #[test]
226 fn fluff_one() {
227 const RAW: &str = fluff_up!(["a"]);
228 const EXPECT: &str = r#"/// a
229struct Fluff;"#;
230 assert_eq!(RAW, EXPECT);
231 }
232
233 #[test]
234 fn fluff_multi() {
235 const RAW: &str = fluff_up!(["a", "b", "c"]);
236 const EXPECT: &str = r#"/// a
237/// b
238/// c
239struct Fluff;"#;
240 assert_eq!(RAW, EXPECT);
241 }
242
243 const EXMALIBU_RANGE_START: usize = 9;
245 const EXMALIBU_RANGE_END: usize = EXMALIBU_RANGE_START + 8;
246 const EXMALIBU_RANGE: Range = EXMALIBU_RANGE_START..EXMALIBU_RANGE_END;
247 const RAW: &str = r#"/// Another exmalibu verification pass.
248/// 🚤w🌴x🌋y🍈z🍉0
249/// ♫ Boats float, ♫♫ don't they? ♫
250struct Vikings;
251"#;
252
253 const EXMALIBU_CHUNK_STR: &str = r#" Another exmalibu verification pass.
254 🚤w🌴x🌋y🍈z🍉0
255 ♫ Boats float, ♫♫ don't they? ♫"#;
256
257 #[test]
258 fn combine_literals() {
259 let _ = env_logger::builder()
260 .is_test(true)
261 .filter(None, log::LevelFilter::Trace)
262 .try_init();
263
264 let cls = gen_literal_set(RAW);
265
266 assert_eq!(cls.len(), 3);
267 assert_eq!(cls.to_string(), EXMALIBU_CHUNK_STR.to_owned());
268 }
269
270 #[test]
271 fn coverage() {
272 let _ = env_logger::builder()
273 .is_test(true)
274 .filter(None, log::LevelFilter::Trace)
275 .try_init();
276
277 let literal_set = gen_literal_set(RAW);
278 let chunk: CheckableChunk = literal_set.into_chunk();
279 let map_range_to_span = chunk.find_spans(EXMALIBU_RANGE);
280 let (_range, _span) = map_range_to_span
281 .first()
282 .expect("Must be at least one literal");
283
284 let range_for_raw_str = Range {
285 start: EXMALIBU_RANGE_START,
286 end: EXMALIBU_RANGE_END,
287 };
288
289 assert_eq!("exmalibu", &EXMALIBU_CHUNK_STR[EXMALIBU_RANGE]);
291
292 assert_eq!(
294 &EXMALIBU_CHUNK_STR[EXMALIBU_RANGE],
295 &chunk.as_str()[range_for_raw_str.clone()]
296 );
297 }
298
299 macro_rules! test_raw {
300 ($test: ident, [ $($txt: literal),+ $(,)? ]; $range: expr, $expected: literal) => {
301 #[test]
302 fn $test() {
303 test_raw!([$($txt),+] ; $range, $expected);
304 }
305 };
306
307 ([$($txt:literal),+ $(,)?]; $range: expr, $expected: literal) => {
308 let _ = env_logger::builder()
309 .filter(None, log::LevelFilter::Trace)
310 .is_test(true)
311 .try_init();
312
313 let range: Range = $range;
314
315 const RAW: &str = fluff_up!($( $txt),+);
316 const START: usize = 3; let _end: usize = START $( + $txt.len())+;
318 let literal_set = gen_literal_set(dbg!(RAW));
319
320
321 let chunk: CheckableChunk = dbg!(literal_set.into_chunk());
322 let map_range_to_span = chunk.find_spans(range.clone());
323
324 let mut iter = dbg!(map_range_to_span).into_iter();
325 let (range, _span) = iter.next().expect("Must be at least one literal");
326
327 let range_for_raw_str = Range {
329 start: range.start + START,
330 end: range.end + START,
331 };
332
333 assert_eq!(&RAW[range_for_raw_str.clone()], &chunk.as_str()[range], "Testing range extract vs stringified chunk for integrity");
334 assert_eq!(&RAW[range_for_raw_str], $expected, "Testing range extract vs expected");
335 };
336 }
337
338 #[test]
339 fn first_line_extract_0() {
340 test_raw!(["livelyness", "yyy"] ; 2..6, "ivel");
341 }
342
343 #[test]
344 fn first_line_extract_1() {
345 test_raw!(["+ 12 + x0"] ; 9..10, "0");
346 }
347
348 #[test]
349 fn literal_set_into_chunk() {
350 let _ = env_logger::builder()
351 .filter(None, log::LevelFilter::Trace)
352 .is_test(true)
353 .try_init();
354
355 let literal_set = dbg!(gen_literal_set(RAW));
356
357 let chunk = dbg!(literal_set.clone().into_chunk());
358 let it = literal_set.literals();
359
360 for (range, span, s) in itertools::cons_tuples(chunk.iter().zip(it)) {
361 if range.len() == 0 {
362 continue;
363 }
364 assert_eq!(
365 load_span_from(RAW.as_bytes(), span.clone()).expect("Span extraction must work"),
366 sub_chars(chunk.as_str(), range.clone())
367 );
368
369 let r: Range = span.to_content_range(&chunk).expect("Should work");
370 assert_eq!(
372 sub_chars(chunk.as_str(), range.clone()),
373 s.as_str().to_owned()
374 );
375 assert_eq!(&r, range);
376 }
377 }
378}