1 /**
2  * Module to work with trivia tokens (`comment`, `whitespace`,
3  * `specialTokenSequence`) which are attached to tokens near them when source
4  * code gets tokenized.
5  */
6 module dparse.trivia;
7 
8 import std.algorithm;
9 import std.array;
10 import std.range;
11 import std..string;
12 import std.traits;
13 
14 import dparse.lexer;
15 
16 enum CommentType : ubyte
17 {
18     none,
19     docLine,
20     docBlock,
21     normalLine,
22     normalBlock,
23 }
24 
25 CommentType determineCommentType(string comment) pure nothrow @safe
26 {
27     auto bytes = comment.representation;
28     auto index = bytes.startsWith(
29         "//".representation,
30         "/+".representation,
31         "/*".representation
32     );
33     bool isDoc = bytes.length >= 3 && bytes[1] == bytes[2];
34     switch (index)
35     {
36     case 1:
37         // Don't treat "////////...." comments as doc comments
38         isDoc = isDoc && (bytes.length == 3 || bytes[3..$].any!(c => c != '/'));
39         return isDoc ? CommentType.docLine :  CommentType.normalLine;
40     case 2:
41     case 3:
42         return isDoc ? CommentType.docBlock :  CommentType.normalBlock;
43     default:
44         return CommentType.none;
45     }
46 }
47 
48 ///
49 unittest
50 {
51     assert (determineCommentType("/// hello") == CommentType.docLine);
52     assert (determineCommentType("/++ hello") == CommentType.docBlock);
53     assert (determineCommentType("/** hello") == CommentType.docBlock);
54     assert (determineCommentType("// hello") == CommentType.normalLine);
55     assert (determineCommentType("/+ hello") == CommentType.normalBlock);
56     assert (determineCommentType("/* hello") == CommentType.normalBlock);
57     assert (determineCommentType("/ hello") == CommentType.none);
58     assert (determineCommentType("/") == CommentType.none);
59 
60     assert (determineCommentType("////////////////////") == CommentType.normalLine);
61     assert (determineCommentType("///") == CommentType.docLine);
62     assert (determineCommentType("///   ") == CommentType.docLine);
63 }
64 
65 bool isDocComment(CommentType type) @safe nothrow pure
66 {
67     return type == CommentType.docLine || type == CommentType.docBlock;
68 }
69 
70 /**
71  * Removes "decoration" such as leading whitespace, leading + and * characters,
72  * and places the result into the given output range
73  */
74 public void unDecorateComment(T)(string comment, auto ref T outputRange)
75 if (isOutputRange!(T, string))
76 in
77 {
78     assert (comment.length >= 3);
79 }
80 do
81 {
82     import std..string : chompPrefix, KeepTerminator, lineSplitter, stripRight;
83 
84     string leadingChars;
85 
86     enum LineType { none, normal, strange }
87     LineType prevLineType;
88 
89     switch (comment[0 .. 3])
90     {
91     case "///":
92         foreach (line; lineSplitter!(KeepTerminator.yes)(comment))
93         {
94             if (leadingChars.empty)
95             {
96                 size_t k = 3;
97                 while (k < line.length && (line[k] == ' ' || line[k] == '\t'))
98                     k++;
99                 leadingChars = line[0 .. k];
100             }
101             outputRange.put(line.chompPrefix(leadingChars));
102         }
103         break;
104     case "/++":
105     case "/**":
106         alias CL = MultiLineCommentHelper!(ElementEncodingType!(typeof(comment)));
107         CL cl = CL(comment);
108         cl.process(outputRange);
109         break;
110     default:
111         outputRange.put(comment);
112     }
113 }
114 
115 ///
116 unittest
117 {
118     import std.array:array, appender;
119     import std.stdio:stderr;
120     stderr.writeln("Running unittest for unDecorateComment...");
121 
122     string[] inputs = [
123         "/***************\n*******************/",
124         "/***************\n *\n ******************/",
125         "/**\n*/",
126         "/** */",
127         "/***/",
128         "/******/",
129         "/** abcde1 */",
130         "/// abcde2\n/// abcde2",
131         "/**\n * stuff1\n */",
132         "/**\n *\n * stuff2\n */",
133         "/**\n *\n * stuff3\n *\n */",
134         "/**\n *\n * stuff4\n *\n*/",
135         "/**\n *  abcde3\n *    abcde3 \n */",
136         "/**\n * abcde4\n *\n * abcde4\n */",
137         "/**abcde5\n*abcde5\n*/",
138         "/** abcde6\n * abcde6\n*/",
139         "/**\n1\n\n\n\n*/",
140         "/**\r\n1\r\n\r\n\r\n\r\n*/",
141         "/**\na1\n\na2\n\n*/",
142         "/**b1\n*b2\n*b3*/",
143         "/**c1\n    *c2\n    *c3*/",
144         "/**d1\n    *d2\n    *d3\n*/",
145         "///a\fbc\n///def"
146     ];
147     string[] outputs = [
148         "",
149         "",
150         "",
151         "",
152         "",
153         "",
154         "abcde1",
155         "abcde2\nabcde2",
156         "stuff1",
157         "stuff2",
158         "stuff3",
159         "stuff4",
160         "abcde3\n  abcde3",
161         "abcde4\n\nabcde4",
162         "abcde5\nabcde5",
163         "abcde6\nabcde6",
164         "1",
165         "1",
166         "a1\n\na2",
167         "b1\nb2\nb3",
168         "c1\nc2\nc3",
169         "d1\nd2\nd3",
170         "a\fbc\ndef"
171     ];
172 
173     // tests where * and + are not interchangeable
174     string[2][] np =
175     [
176         ["/**\n * d1\n d2\n */", "* d1\nd2"],
177         ["/**\n + d1\n d2\n */", "+ d1\nd2"],
178         ["/**d1\n\n\n*d2\n*/",  "d1\n\n*d2"],
179     ];
180 
181     assert(inputs.length == outputs.length);
182     foreach (pair; zip(inputs, outputs))
183     {
184         foreach (b; [true, false])
185         {
186             auto app = appender!string();
187             unDecorateComment(b ? pair[0] : pair[0].replace("*", "+"), app);
188             assert(pair[1] == app.data, "[[" ~ pair[0] ~ "]] => [[" ~ app.data ~ "]]");
189         }
190     }
191     foreach (pair; np)
192     {
193         auto app = appender!string();
194         unDecorateComment(pair[0], app);
195         assert(pair[1] == app.data, "[[" ~ pair[0] ~ "]] => [[" ~ app.data ~ "]]");
196     }
197     stderr.writeln("Unittest for unDecorateComment passed.");
198 }
199 
200 /** Gives a line per line view on DDOC comments of type `/++` and `/**` which
201  * makes easier to remove the decoration and in an almost 100% nogc way. */
202 private struct MultiLineCommentHelper(CharType : const(char))
203 {
204     // this struct is more used as a 'function with nested functions' would.
205     this() @disable;
206     this(this) @disable;
207     auto opAssign(T)(T t) @disable;
208 
209 private:
210 
211     char[][] lines;
212     // either lines.length or lines.length-1, depending on if last line only closes
213     size_t lastLineInBlockPlusOne;
214     // either '*' or '+'
215     const(char) commentChar;
216     // either 0 or 1, depending on if first line only opens
217     ubyte firstLineInBlock;
218 
219     import std.ascii : isWhite;
220 
221     void stripIndent() @safe @nogc pure nothrow
222     {
223         if (lines.length < 2)
224             return;
225         size_t count;
226         foreach (const j; 0 .. lines[1].length)
227             if (!(lines[1][j]).isWhite)
228         {
229             count = j;
230             break;
231         }
232         if (count < 2)
233             return;
234         foreach (ref line; lines[1 .. $])
235         {
236             foreach (const j; 0 .. line.length)
237             {
238                 if (!(line[j]).isWhite)
239                     break;
240                 if (j == count - 1)
241                 {
242                     line = line[j .. $];
243                     break;
244                 }
245             }
246         }
247     }
248 
249     void processFirstLine() @safe @nogc pure nothrow
250     {
251         assert(lines.length);
252         if (lines[0].length > 3)
253         {
254             foreach (const i; 1..lines[0].length)
255             {
256                 if (lines[0][i] == commentChar)
257                 {
258                     if (i < lines[0].length - 2)
259                         continue;
260                     if (i == lines[0].length - 2 && lines[0][i+1] == '/')
261                     {
262                         lines[0][] = ' ';
263                         break;
264                     }
265                     if (i == lines[0].length - 1)
266                     {
267                         lines[0][] = ' ';
268                         break;
269                     }
270                 }
271                 else
272                 {
273                     lines[0][0..i] = ' ';
274                     break;
275                 }
276             }
277         }
278         lines[0][0..3] = "   ";
279         if (lines.length == 1 &&
280             lines[0][$-2] == commentChar && lines[0][$-1] == '/')
281         {
282             lines[0][$-2..$] = "  ";
283         }
284         foreach (const i; 0..lines[0].length)
285             if (!(lines[0][i].isWhite))
286                 return;
287         firstLineInBlock = 1;
288     }
289 
290     void processLastLine() @safe @nogc pure nothrow
291     {
292         lastLineInBlockPlusOne = lines.length;
293         if (lines.length == 1)
294             return;
295         size_t closeStartIndex = size_t.max;
296         foreach (const i; 0..lines[$-1].length)
297         {
298             if (lines[$-1][i] == commentChar)
299             {
300                 if (closeStartIndex == size_t.max)
301                     closeStartIndex = i;
302                 if (i == lines[$-1].length - 2)
303                 {
304                     // see the FIXME note in unDecorate()
305                     lastLineInBlockPlusOne = closeStartIndex == 0 ? lines.length-1 : lines.length;
306 
307                     lines[$-1][closeStartIndex..$] = ' ';
308                     break;
309                 }
310             }
311             else
312             {
313                 closeStartIndex = size_t.max;
314                 lastLineInBlockPlusOne = lines.length;
315             }
316         }
317     }
318 
319     void unDecorate() @safe @nogc pure nothrow
320     {
321         if (lines.length == 1 || lines.length == 2 && lines[$-1].length == 0)
322             return;
323         bool allDecorated;
324         static immutable char[2][2] pattern = [[' ', '*'],[' ', '+']];
325         const ubyte patternIndex = commentChar == '+';
326         // first line is never decorated
327         const size_t lo = 1;
328         // although very uncommon, the last line can be decorated e.g in `* lastline */`:
329         // the first '*' is a deco if all prev lines are also decorated.
330         // FIXME: `hi` should be set to `lastLineInBlockPlusOne`...
331         const size_t hi = (lines[$-1].length > 1 &&
332             (lines[$-1][0] == commentChar || lines[$-1][0..2] == pattern[patternIndex]))
333             ?  lines.length : lines.length-1;
334         // deco with a leading white
335         foreach (const i; lo .. hi)
336         {
337             if (lines[i].length < 2)
338                 break;
339             else if (lines[i][0..2] != pattern[patternIndex])
340                 break;
341             else if (i == hi-1)
342                 allDecorated = true;
343         }
344         // deco w/o leading white
345         if (!allDecorated)
346             foreach (const i; lo .. hi)
347         {
348             if (lines[i].length == 0)
349                 break;
350             if (lines[i][0] != commentChar)
351                 break;
352             else if (i == hi-1)
353                 allDecorated = true;
354         }
355         if (!allDecorated)
356             return;
357 
358         const size_t indexToChange = (lines[lo][0] == commentChar) ? 0 : 1;
359         foreach (ref line; lines[lo .. hi])
360             line[indexToChange] = ' ';
361     }
362 
363     void stripLeft() @safe @nogc pure nothrow
364     {
365         foreach (const i; 0 .. lines[0].length)
366             if (!(lines[0][i]).isWhite)
367         {
368             lines[0] = lines[0][i..$];
369             break;
370         }
371         if (lines.length == 1)
372             return;
373         while (true)
374         {
375             bool processColumn;
376             foreach (ref line; lines[1 .. lastLineInBlockPlusOne])
377             {
378                 if (line.length == 0)
379                     continue;
380                 if (!(line[0]).isWhite)
381                     return;
382                 processColumn = true;
383             }
384             if (!processColumn)
385                 return;
386             foreach (ref line; lines[1 .. lastLineInBlockPlusOne])
387             {
388                 if (line.length == 0)
389                     continue;
390                 line = line[1..$];
391             }
392         }
393     }
394 
395     void stripRight() @safe @nogc pure nothrow
396     {
397         foreach (ref line; lines[0 .. lines.length])
398         {
399             if (line.length == 0)
400                 continue;
401             if ((line[$-1]).isWhite)
402             {
403                 size_t firstWhite = line.length;
404                 while (firstWhite > 0 && (line[firstWhite-1]).isWhite)
405                     firstWhite--;
406                 line = line[0..firstWhite];
407             }
408         }
409     }
410 
411     void run() @safe @nogc pure nothrow
412     {
413         stripIndent();
414         processFirstLine();
415         processLastLine();
416         unDecorate();
417         stripLeft();
418         stripRight();
419     }
420 
421 public:
422 
423     this(CharType[] text) @safe pure nothrow
424     {
425         assert(text.length >= 3 && text[0] == '/',
426             "MultiLineCommentHelper text must start with a comment in form /++ or /**");
427 
428         commentChar = text[1];
429         size_t startIndex, i;
430         Appender!(char[][]) linesApp;
431         linesApp.reserve(512);
432 
433         void storeLine(size_t endIndexPlusOne)
434         {
435             static if (isMutable!CharType)
436                 linesApp ~= text[startIndex..endIndexPlusOne];
437             else
438                 linesApp ~= text[startIndex..endIndexPlusOne].dup;
439         }
440 
441         // if we go over text length (in \r\n) we already stored the line, so just exit there
442         while (i < text.length)
443         {
444             // check if next char is going to be end of text, store until then & break
445             if (i + 1 == text.length)
446             {
447                 storeLine(text.length);
448                 break;
449             }
450             if (text[i] == '\n')
451             {
452                 storeLine(i);
453                 startIndex = i + 1;
454             }
455             else if (i + 1 < text.length && text[i .. i+2] == "\r\n")
456             {
457                 storeLine(i);
458                 i++;
459                 startIndex = i + 1;
460             }
461             i++;
462         }
463         lines = linesApp.data;
464     }
465 
466     void process(T)(ref T outbuffer)
467     {
468         run();
469         outbuffer.reserve(lines.length * 90);
470         bool prevWritten, empties;
471         foreach (ref line; lines[firstLineInBlock .. lines.length])
472         {
473             if (line.length != 0)
474             {
475                 // close preceeding line
476                 if (prevWritten)
477                     outbuffer ~= "\n";
478                 // insert new empty line
479                 if (prevWritten && empties)
480                     outbuffer ~= "\n";
481 
482                 outbuffer ~= line;
483                 prevWritten = true;
484                 empties = false;
485             }
486             else empties = true;
487         }
488     }
489 }
490 
491 unittest
492 {
493     import std.conv : to;
494 
495     alias SC = MultiLineCommentHelper!(immutable(char));
496 
497     // checks full comment processing on the given string and compares the generated lines
498     void check(string comment, string[] lines, size_t lineNo = __LINE__)
499     {
500         auto sc = SC(comment);
501         sc.run();
502         assert(sc.lines == lines, sc.lines.to!string ~ " != " ~ lines.to!string
503             ~ " (for check on line " ~ lineNo.to!string ~ ")");
504     }
505 
506     // check common cases while typing
507     check("/++", [""]);
508     check("/++\r", [""]);
509     check("/++\n", [""]);
510     check("/++\r\n", [""]);
511     check("/++\r\n+", ["", "+"]);
512     check("/++\r\n+ ok", ["", "ok"]);
513     check("/++\r\n+ ok\r\n+/", ["", "ok", ""]);
514     check("/++/", [""]);
515 }
516 
517 /// Extracts and combines ddoc comments from trivia comments.
518 string extractDdocFromTrivia(Tokens)(Tokens tokens) pure nothrow @safe
519     if (isInputRange!Tokens && is(ElementType!Tokens : Token))
520 {
521     bool hasDoc;
522     auto ret = appender!string;
523     foreach (trivia; tokens)
524     {
525         if (trivia.type == tok!"comment"
526             && trivia.text.determineCommentType.isDocComment)
527         {
528             hasDoc = true;
529             if (!ret.data.empty)
530                 ret.put('\n');
531             unDecorateComment(trivia.text, ret);
532         }
533     }
534 
535     if (ret.data.length)
536         return ret.data;
537     else
538         return hasDoc ? "" : null;
539 }
540 
541 unittest
542 {
543     Token[] tokens = [
544         Token(cast(ubyte) tok!"whitespace", "\n\n", 0, 0, 0),
545         Token(cast(ubyte) tok!"comment", "///", 0, 0, 0),
546         Token(cast(ubyte) tok!"whitespace", "\n", 0, 0, 0)
547     ];
548 
549     // Empty comment is non-null
550     auto comment = extractDdocFromTrivia(tokens);
551     assert(comment !is null);
552     assert(comment == "");
553 
554     // Missing comment is null
555     comment = extractDdocFromTrivia(tokens[0 .. 1]);
556     assert(comment is null);
557     assert(comment == "");
558 }
559 
560 string extractLeadingDdoc(const Token token) pure nothrow @safe
561 {
562     return extractDdocFromTrivia(token.leadingTrivia);
563 }
564 
565 string extractTrailingDdoc(const Token token) pure nothrow @safe
566 {
567     return extractDdocFromTrivia(token.trailingTrivia.filter!(a => a.line == token.line));
568 }
569 
570 // test token trivia members
571 unittest
572 {
573     import std.conv : to;
574     import std.exception : enforce;
575 
576     static immutable src = `/// this is a module.
577 // mixed
578 /// it can do stuff
579 module foo.bar;
580 
581 // hello
582 
583 /**
584  * some doc
585  * hello
586  */
587 int x; /// very nice
588 
589 // TODO: do stuff
590 void main() {
591     #line 40
592     /// could be better
593     writeln(":)");
594 }
595 
596 ///
597 unittest {}
598 
599 /// end of file`;
600 
601     LexerConfig cf;
602     StringCache ca = StringCache(16);
603 
604     const tokens = getTokensForParser(src, cf, &ca);
605 
606     assert(tokens.length == 22);
607 
608     assert(tokens[0].type == tok!"module");
609     assert(tokens[0].leadingTrivia.length == 6);
610     assert(tokens[0].leadingTrivia[0].type == tok!"comment");
611     assert(tokens[0].leadingTrivia[0].text == "/// this is a module.");
612     assert(tokens[0].leadingTrivia[1].type == tok!"whitespace");
613     assert(tokens[0].leadingTrivia[1].text == "\n");
614     assert(tokens[0].leadingTrivia[2].type == tok!"comment");
615     assert(tokens[0].leadingTrivia[2].text == "// mixed");
616     assert(tokens[0].leadingTrivia[3].type == tok!"whitespace");
617     assert(tokens[0].leadingTrivia[3].text == "\n");
618     assert(tokens[0].leadingTrivia[4].type == tok!"comment");
619     assert(tokens[0].leadingTrivia[4].text == "/// it can do stuff");
620     assert(tokens[0].leadingTrivia[5].type == tok!"whitespace");
621     assert(tokens[0].leadingTrivia[5].text == "\n");
622     assert(tokens[0].trailingTrivia.length == 1);
623     assert(tokens[0].trailingTrivia[0].type == tok!"whitespace");
624     assert(tokens[0].trailingTrivia[0].text == " ");
625 
626     assert(tokens[1].type == tok!"identifier");
627     assert(tokens[1].text == "foo");
628     assert(!tokens[1].leadingTrivia.length);
629     assert(!tokens[1].trailingTrivia.length);
630 
631     assert(tokens[2].type == tok!".");
632     assert(!tokens[2].leadingTrivia.length);
633     assert(!tokens[2].trailingTrivia.length);
634 
635     assert(tokens[3].type == tok!"identifier");
636     assert(tokens[3].text == "bar");
637     assert(!tokens[3].leadingTrivia.length);
638     assert(!tokens[3].trailingTrivia.length);
639 
640     assert(tokens[4].type == tok!";");
641     assert(!tokens[4].leadingTrivia.length);
642     assert(tokens[4].trailingTrivia.length == 1);
643     assert(tokens[4].trailingTrivia[0].type == tok!"whitespace");
644     assert(tokens[4].trailingTrivia[0].text == "\n\n");
645 
646     assert(tokens[5].type == tok!"int");
647     assert(tokens[5].leadingTrivia.length == 4);
648     assert(tokens[5].leadingTrivia[0].text == "// hello");
649     assert(tokens[5].leadingTrivia[1].text == "\n\n");
650     assert(tokens[5].leadingTrivia[2].text == "/**\n * some doc\n * hello\n */");
651     assert(tokens[5].leadingTrivia[3].text == "\n");
652     assert(tokens[5].trailingTrivia.length == 1);
653     assert(tokens[5].trailingTrivia[0].text == " ");
654 
655     assert(tokens[6].type == tok!"identifier");
656     assert(tokens[6].text == "x");
657     assert(!tokens[6].leadingTrivia.length);
658     assert(!tokens[6].trailingTrivia.length);
659 
660     assert(tokens[7].type == tok!";");
661     assert(!tokens[7].leadingTrivia.length);
662     assert(tokens[7].trailingTrivia.length == 3);
663     assert(tokens[7].trailingTrivia[0].text == " ");
664     assert(tokens[7].trailingTrivia[1].text == "/// very nice");
665     assert(tokens[7].trailingTrivia[2].text == "\n\n");
666 
667     assert(tokens[8].type == tok!"void");
668     assert(tokens[8].leadingTrivia.length == 2);
669     assert(tokens[8].leadingTrivia[0].text == "// TODO: do stuff");
670     assert(tokens[8].leadingTrivia[1].text == "\n");
671     assert(tokens[8].trailingTrivia.length == 1);
672     assert(tokens[8].trailingTrivia[0].text == " ");
673 
674     assert(tokens[9].type == tok!"identifier");
675     assert(tokens[9].text == "main");
676     assert(!tokens[9].leadingTrivia.length);
677     assert(!tokens[9].trailingTrivia.length);
678 
679     assert(tokens[10].type == tok!"(");
680     assert(!tokens[10].leadingTrivia.length);
681     assert(!tokens[10].trailingTrivia.length);
682 
683     assert(tokens[11].type == tok!")");
684     assert(!tokens[11].leadingTrivia.length);
685     assert(tokens[11].trailingTrivia.length == 1);
686     assert(tokens[11].trailingTrivia[0].text == " ");
687 
688     assert(tokens[12].type == tok!"{");
689     assert(!tokens[12].leadingTrivia.length);
690     assert(tokens[12].trailingTrivia.length == 1);
691     assert(tokens[12].trailingTrivia[0].text == "\n    ");
692 
693     assert(tokens[13].type == tok!"identifier");
694     assert(tokens[13].text == "writeln");
695     assert(tokens[13].leadingTrivia.length == 4);
696     assert(tokens[13].leadingTrivia[0].type == tok!"specialTokenSequence");
697     assert(tokens[13].leadingTrivia[0].text == "#line 40");
698     assert(tokens[13].leadingTrivia[1].type == tok!"whitespace");
699     assert(tokens[13].leadingTrivia[1].text == "\n    ");
700     assert(tokens[13].leadingTrivia[2].type == tok!"comment");
701     assert(tokens[13].leadingTrivia[2].text == "/// could be better");
702     assert(tokens[13].leadingTrivia[3].type == tok!"whitespace");
703     assert(tokens[13].leadingTrivia[3].text == "\n    ");
704     assert(!tokens[13].trailingTrivia.length);
705 
706     assert(tokens[14].type == tok!"(");
707     assert(!tokens[14].leadingTrivia.length);
708     assert(!tokens[14].trailingTrivia.length);
709 
710     assert(tokens[15].type == tok!"stringLiteral");
711     assert(!tokens[15].leadingTrivia.length);
712     assert(!tokens[15].trailingTrivia.length);
713 
714     assert(tokens[16].type == tok!")");
715     assert(!tokens[16].leadingTrivia.length);
716     assert(!tokens[16].trailingTrivia.length);
717 
718     assert(tokens[17].type == tok!";");
719     assert(!tokens[17].leadingTrivia.length);
720     assert(tokens[17].trailingTrivia.length == 1);
721     assert(tokens[17].trailingTrivia[0].text == "\n");
722 
723     assert(tokens[18].type == tok!"}");
724     assert(!tokens[18].leadingTrivia.length);
725     assert(tokens[18].trailingTrivia.length == 1);
726     assert(tokens[18].trailingTrivia[0].type == tok!"whitespace");
727     assert(tokens[18].trailingTrivia[0].text == "\n\n");
728 
729     assert(tokens[19].type == tok!"unittest");
730     assert(tokens[19].leadingTrivia.length == 2);
731     assert(tokens[19].leadingTrivia[0].type == tok!"comment");
732     assert(tokens[19].leadingTrivia[0].text == "///");
733     assert(tokens[19].leadingTrivia[1].type == tok!"whitespace");
734     assert(tokens[19].leadingTrivia[1].text == "\n");
735 
736     assert(tokens[19].trailingTrivia.length == 1);
737     assert(tokens[19].trailingTrivia[0].type == tok!"whitespace");
738     assert(tokens[19].trailingTrivia[0].text == " ");
739 
740     assert(tokens[20].type == tok!"{");
741     assert(!tokens[20].leadingTrivia.length);
742     assert(!tokens[20].trailingTrivia.length);
743 
744     assert(tokens[21].type == tok!"}");
745     assert(!tokens[21].leadingTrivia.length);
746     assert(tokens[21].trailingTrivia.length == 2);
747     assert(tokens[21].trailingTrivia[0].type == tok!"whitespace");
748     assert(tokens[21].trailingTrivia[0].text == "\n\n");
749     assert(tokens[21].trailingTrivia[1].type == tok!"comment");
750     assert(tokens[21].trailingTrivia[1].text == "/// end of file");
751 }