1 /* This file is part of the 'hgen' project.
2  *
3  * Copyright (c) 2014-2022
4  *     Economic Modeling Specialists, Intl.,
5  *     the D Community.
6  *
7  * Main contributors:
8  *     Brian Schott
9  *     Ferdinand Majerech
10  *     Ilya Yaroshenko
11  *     Anton Akzhigitov
12  *     Nemanja Boric
13  *     Basile Burg
14  *     Eugene Stulin
15  *
16  * Distributed under the Boost Software License, Version 1.0.
17  *
18  * You should have received a copy of the Boost Software License
19  * along with this program. If not, see <http://www.boost.org/LICENSE_1_0.txt>.
20  * This file is offered as-is, without any warranty.
21  */
22 
23 module visitor;
24 
25 import std.ascii;
26 import std.algorithm;
27 import std.array: appender, empty, array, popBack, back, popFront, front;
28 import std.exception: assumeWontThrow;
29 import std.file;
30 import std.path;
31 import std.stdio;
32 import std.string: format;
33 import std.typecons;
34 
35 import dparse.ast;
36 import dparse.lexer;
37 
38 import config;
39 import ddoc.comments;
40 import item;
41 import symboldatabase;
42 import unittest_preprocessor;
43 import writer;
44 
45 /**
46  * Generates documentation for a (single) module.
47  */
48 class DocVisitor(Writer) : ASTVisitor
49 {
50     /**
51      * Params:
52      *
53      * config          = Configuration data, including macros and the output directory.
54      * database        = Stores information about modules and symbols for e.g. cross-referencing.
55      * unitTestMapping = The mapping of declaration addresses to their documentation unittests
56      * fileBytes       = The source code of the module as a byte array.
57      * writer          = Handles writing into generated files.
58      */
59     this(ref const Config config, SymbolDatabase database,
60          TestRange[][size_t] unitTestMapping, const(ubyte[]) fileBytes, Writer writer)
61     {
62         this.config          = &config;
63         this.database        = database;
64         this.unitTestMapping = unitTestMapping;
65         this.fileBytes       = fileBytes;
66         this.writer          = writer;
67 
68         this.writer.processCode = &crossReference;
69     }
70 
71     override void visit(const Unittest){}
72 
73     override void visit(const Module mod)
74     {
75         import std.conv : to;
76         import std.range: join;
77         assert(mod.moduleDeclaration !is null, "DataGatherVisitor should have caught this");
78         pushAttributes();
79         stack = cast(string[])
80             mod.moduleDeclaration.moduleName.identifiers.map!(a => a.text).array;
81         writer.prepareModule(stack);
82 
83         moduleName = stack.join(".").to!string;
84 
85         scope(exit) { writer.finishModule(); }
86 
87         // The module is the first and only top-level "symbol".
88         bool dummyFirst;
89         string link;
90         auto fileWriter = writer.pushSymbol(stack, database, dummyFirst, link);
91         scope(exit) { writer.popSymbol(); }
92 
93         writer.writeHeader(fileWriter, moduleName, stack.length - 1);
94         writer.writeBreadcrumbs(fileWriter, stack, database);
95         writer.writeTOC(fileWriter, moduleName);
96         writer.writeSymbolStart(fileWriter, link);
97 
98         prevComments.length = 1;
99 
100         const comment = mod.moduleDeclaration.comment;
101         memberStack.length = 1;
102 
103         mod.accept(this);
104 
105         writer.writeSymbolDescription(fileWriter,
106         {
107             memberStack.back.writePublicImports(fileWriter, writer);
108 
109             if (comment !is null)
110             {
111                 writer.readAndWriteComment(fileWriter, comment, prevComments,
112                     null, getUnittestDocTuple(mod.moduleDeclaration));
113             }
114         });
115 
116         memberStack.back.write(fileWriter, writer);
117         writer.writeSymbolEnd(fileWriter);
118     }
119 
120     override void visit(const EnumDeclaration ed)
121     {
122         enum formattingCode = q{
123         fileWriter.put("enum " ~ ad.name.text);
124         if (ad.type !is null)
125         {
126             fileWriter.put(" : ");
127             formatter.format(ad.type);
128         }
129         };
130         visitAggregateDeclaration!(formattingCode, "enums")(ed);
131     }
132 
133     override void visit(const EnumMember member)
134     {
135         // Document all enum members even if they have no doc comments.
136         if (member.comment is null)
137         {
138             memberStack.back.values ~= Item("#", member.name.text, "");
139             return;
140         }
141         auto dummy = appender!string();
142         // No interest in detailed docs for an enum member.
143         string summary = writer.readAndWriteComment(dummy, member.comment,
144             prevComments, null, getUnittestDocTuple(member));
145         memberStack.back.values ~= Item("#", member.name.text, summary);
146     }
147 
148     override void visit(const ClassDeclaration cd)
149     {
150         enum formattingCode = q{
151         fileWriter.put("class " ~ ad.name.text);
152         if (ad.templateParameters !is null)
153             formatter.format(ad.templateParameters);
154         if (ad.baseClassList !is null)
155             formatter.format(ad.baseClassList);
156         if (ad.constraint !is null)
157             formatter.format(ad.constraint);
158         };
159         visitAggregateDeclaration!(formattingCode, "classes")(cd);
160     }
161 
162     override void visit(const TemplateDeclaration td)
163     {
164         enum formattingCode = q{
165         fileWriter.put("template " ~ ad.name.text);
166         if (ad.templateParameters !is null)
167             formatter.format(ad.templateParameters);
168         if (ad.constraint)
169             formatter.format(ad.constraint);
170         };
171         visitAggregateDeclaration!(formattingCode, "templates")(td);
172     }
173 
174     override void visit(const StructDeclaration sd)
175     {
176         enum formattingCode = q{
177         fileWriter.put("struct " ~ ad.name.text);
178         if (ad.templateParameters)
179             formatter.format(ad.templateParameters);
180         if (ad.constraint)
181             formatter.format(ad.constraint);
182         };
183         visitAggregateDeclaration!(formattingCode, "structs")(sd);
184     }
185 
186     override void visit(const InterfaceDeclaration id)
187     {
188         enum formattingCode = q{
189         fileWriter.put("interface " ~ ad.name.text);
190         if (ad.templateParameters !is null)
191             formatter.format(ad.templateParameters);
192         if (ad.baseClassList !is null)
193             formatter.format(ad.baseClassList);
194         if (ad.constraint !is null)
195             formatter.format(ad.constraint);
196         };
197         visitAggregateDeclaration!(formattingCode, "interfaces")(id);
198     }
199 
200     override void visit(const AliasDeclaration ad)
201     {
202         if (ad.comment is null)
203             return;
204         bool first;
205         if (ad.declaratorIdentifierList !is null)
206             foreach (name; ad.declaratorIdentifierList.identifiers)
207         {
208             string itemURL;
209             auto fileWriter = pushSymbol(name.text, first, itemURL);
210             scope(exit) popSymbol(fileWriter);
211 
212             string type, summary;
213             writer.writeSymbolDescription(fileWriter,
214             {
215                 type = writeAliasType(fileWriter, name.text, ad.type);
216                 summary = writer.readAndWriteComment(fileWriter, ad.comment, prevComments);
217             });
218 
219             memberStack[$ - 2].aliases ~= Item(itemURL, name.text, summary, type);
220         }
221         else foreach (initializer; ad.initializers)
222         {
223             string itemURL;
224             auto fileWriter = pushSymbol(initializer.name.text, first, itemURL);
225             scope(exit) popSymbol(fileWriter);
226 
227             string type, summary;
228             writer.writeSymbolDescription(fileWriter,
229             {
230                 type = writeAliasType(fileWriter, initializer.name.text, initializer.type);
231                 summary = writer.readAndWriteComment(fileWriter, ad.comment, prevComments);
232             });
233 
234             memberStack[$ - 2].aliases ~= Item(itemURL, initializer.name.text, summary, type);
235         }
236     }
237 
238     override void visit(const VariableDeclaration vd)
239     {
240         // Write the variable attributes, type, name.
241         void writeVariableHeader(R)(ref R dst, string typeStr, string nameStr, string defStr = "")
242         {
243             writer.writeCodeBlock(dst,
244             {
245                 assert(attributeStack.length > 0,
246                     "Attributes stack must not be empty when writing variable attributes");
247                 auto formatter = writer.newFormatter(dst);
248                 scope(exit) { destroy(formatter.sink); }
249                 // Attributes like public, etc.
250                 writeAttributes(dst, formatter, attributeStack.back);
251                 dst.put(typeStr);
252                 dst.put(` `);
253                 dst.put(nameStr);
254                 dst.put(defStr);
255             });
256         }
257         bool first;
258         foreach (const Declarator dec; vd.declarators)
259         {
260             if (vd.comment is null && dec.comment is null)
261                 continue;
262             string itemURL;
263             auto fileWriter = pushSymbol(dec.name.text, first, itemURL);
264             scope(exit) popSymbol(fileWriter);
265 
266             string typeStr = writer.formatNode(vd.type);
267             string summary;
268             writer.writeSymbolDescription(fileWriter,
269             {
270                 string defStr;
271                 if (dec.initializer)
272                 {
273                     import dparse.formatter : fmt = format;
274                     import std.array : Appender;
275                     Appender!string app;
276                     app.put(" = ");
277                     fmt(&app, dec.initializer);
278                     defStr = app.data;
279                 }
280                 writeVariableHeader(fileWriter, typeStr, dec.name.text, defStr);
281                 summary = writer.readAndWriteComment(fileWriter,
282                     dec.comment is null ? vd.comment : dec.comment,
283                     prevComments);
284             });
285 
286             memberStack[$ - 2].variables ~= Item(itemURL, dec.name.text, summary, typeStr, dec);
287         }
288         if (vd.comment !is null && vd.autoDeclaration !is null)
289         {
290             foreach (part; vd.autoDeclaration.parts) with (part)
291             {
292                 string itemURL;
293                 auto fileWriter = pushSymbol(identifier.text, first, itemURL);
294                 scope(exit) popSymbol(fileWriter);
295 
296                 // TODO this was hastily updated to get hgen to compile
297                 // after a libdparse update. Revisit and validate/fix any errors.
298                 string[] storageClasses;
299                 foreach(stor; vd.storageClasses)
300                 {
301                     storageClasses ~= str(stor.token.type);
302                 }
303 
304                 string typeStr = storageClasses.canFind("enum") ? null : "auto";
305                 string summary;
306                 writer.writeSymbolDescription(fileWriter,
307                 {
308                     writeVariableHeader(fileWriter, typeStr, identifier.text);
309                     summary = writer.readAndWriteComment(fileWriter, vd.comment, prevComments);
310                 });
311                 auto i = Item(itemURL, identifier.text, summary, typeStr);
312                 if (storageClasses.canFind("enum"))
313                     memberStack[$ - 2].enums ~= i;
314                 else
315                     memberStack[$ - 2].variables ~= i;
316 
317                 // string storageClass;
318                 // foreach (attr; vd.attributes)
319                 // {
320                 //     if (attr.storageClass !is null)
321                 //         storageClass = str(attr.storageClass.token.type);
322                 // }
323                 // auto i = Item(name, ident.text,
324                 //     summary, storageClass == "enum" ? null : "auto");
325                 // if (storageClass == "enum")
326                 //     memberStack[$ - 2].enums ~= i;
327                 // else
328                 //     memberStack[$ - 2].variables ~= i;
329             }
330         }
331     }
332 
333     override void visit(const StructBody sb)
334     {
335         pushAttributes();
336         sb.accept(this);
337         popAttributes();
338     }
339 
340     override void visit(const BlockStatement bs)
341     {
342         pushAttributes();
343         bs.accept(this);
344         popAttributes();
345     }
346 
347     override void visit(const Declaration dec)
348     {
349         attributeStack.back ~= dec.attributes;
350         dec.accept(this);
351         if (dec.attributeDeclaration is null)
352             attributeStack.back = attributeStack.back[0 .. $ - dec.attributes.length];
353     }
354 
355     override void visit(const AttributeDeclaration dec)
356     {
357         attributeStack.back ~= dec.attribute;
358     }
359 
360     override void visit(const Constructor cons)
361     {
362         if (cons.comment is null)
363             return;
364         writeFnDocumentation("this", cons, attributeStack.back);
365     }
366 
367     override void visit(const FunctionDeclaration fd)
368     {
369         if (fd.comment is null)
370             return;
371         writeFnDocumentation(fd.name.text, fd, attributeStack.back);
372     }
373 
374     override void visit(const ImportDeclaration imp)
375     {
376         // public attribute must be specified explicitly for public imports.
377         foreach(attr; attributeStack.back) if(attr.attribute.type == tok!"public")
378         {
379             foreach(i; imp.singleImports)
380             {
381                 import std.conv;
382                 // Using 'dup' here because of std.algorithm's apparent
383                 // inability to work with const arrays. Probably not an
384                 // issue (imports are not hugely common), but keep the
385                 // possible GC overhead in mind.
386                 auto nameParts = i.identifierChain.identifiers
387                                  .dup.map!(t => t.text).array;
388                 const name = nameParts.joiner(".").to!string;
389 
390                 const knownModule = database.moduleNames.canFind(name);
391                 const link = knownModule ? writer.moduleLink(nameParts)
392                                          : null;
393                 memberStack.back.publicImports ~=
394                     Item(link, name, null, null, imp);
395             }
396             return;
397         }
398         //TODO handle imp.importBindings as well? Need to figure out how it works.
399     }
400 
401     // Optimization: don't allow visit() for these AST nodes to result in visit()
402     // calls for their subnodes. This avoids most of the dynamic cast overhead.
403     override void visit(const AssignExpression assignExpression) {}
404     override void visit(const CmpExpression cmpExpression) {}
405     override void visit(const TernaryExpression ternaryExpression) {}
406     override void visit(const IdentityExpression identityExpression) {}
407     override void visit(const InExpression inExpression) {}
408 
409     alias visit = ASTVisitor.visit;
410 
411 private:
412     /// Get the current protection attribute.
413     IdType currentProtection()
414     out(result)
415     {
416         assert([tok!"private", tok!"package", tok!"protected", tok!"public"].canFind(result),
417                "Unknown protection attribute");
418     }
419     do
420     {
421         foreach(a; attributeStack.back.filter!(a => a.attribute.type.isProtection))
422         {
423             return a.attribute.type;
424         }
425         return tok!"public";
426     }
427 
428     /** Writes attributes to the range dst using formatter to format code.
429      *
430      * Params:
431      *
432      * dst       = Range to write to.
433      * formatter = Formatter to format the attributes with.
434      * attrs     = Attributes to write.
435      */
436     final void writeAttributes(R, F)(ref R dst, F formatter, const(Attribute)[] attrs)
437     {
438         import dparse.lexer: IdType, isProtection, tok;
439         IdType protection = currentProtection();
440         switch (protection)
441         {
442             case tok!"private":   dst.put("private ");   break;
443             case tok!"package":   dst.put("package ");   break;
444             case tok!"protected": dst.put("protected "); break;
445             default:              dst.put("");    break;
446         }
447         foreach (a; attrs.filter!(a => !a.attribute.type.isProtection))
448         {
449             formatter.format(a);
450             dst.put(" ");
451         }
452     }
453 
454 
455     void visitAggregateDeclaration(string formattingCode, string name, A)(const A ad)
456     {
457         bool first;
458         if (ad.comment is null)
459             return;
460 
461         string itemURL;
462         auto fileWriter = pushSymbol(ad.name.text, first, itemURL);
463         scope(exit) popSymbol(fileWriter);
464 
465         string summary;
466         writer.writeSymbolDescription(fileWriter,
467         {
468             writer.writeCodeBlock(fileWriter,
469             {
470                 auto formatter = writer.newFormatter(fileWriter);
471                 scope(exit) destroy(formatter.sink);
472                 assert(attributeStack.length > 0,
473                     "Attributes stack must not be empty when writing aggregate attributes");
474                 writeAttributes(fileWriter, formatter, attributeStack.back);
475                 mixin(formattingCode);
476             });
477 
478             summary = writer.readAndWriteComment(fileWriter, ad.comment, prevComments,
479                 null, getUnittestDocTuple(ad));
480         });
481 
482         mixin(`memberStack[$ - 2].` ~ name ~ ` ~= Item(itemURL, ad.name.text, summary);`);
483 
484         prevComments.length = prevComments.length + 1;
485         ad.accept(this);
486         prevComments.popBack();
487 
488         memberStack.back.write(fileWriter, writer);
489     }
490 
491     /**
492      * Params:
493      *     t = The declaration.
494      * Returns: An array of tuples where the first item is the contents of the
495      *     unittest block and the second item is the doc comment for the
496      *     unittest block. This array may be empty.
497      */
498     Tuple!(string, string)[] getUnittestDocTuple(T)(const T t)
499     {
500         immutable size_t index = cast(size_t) (cast(void*) t);
501 //        writeln("Searching for unittest associated with ", index);
502         auto tupArray = index in unitTestMapping;
503         if (tupArray is null)
504             return [];
505 //        writeln("Found a doc unit test for ", cast(size_t) &t);
506         Tuple!(string, string)[] rVal;
507         foreach (tup; *tupArray)
508             rVal ~= tuple(cast(string) fileBytes[tup[0] + 2 .. tup[1]], tup[2]);
509         return rVal;
510     }
511 
512     /**
513      *
514      */
515     void writeFnDocumentation(Fn)(string name, Fn fn, const(Attribute)[] attrs)
516     {
517         bool first;
518         string itemURL;
519         auto fileWriter = pushSymbol(name, first, itemURL);
520         scope(exit) popSymbol(fileWriter);
521 
522         string summary;
523         writer.writeSymbolDescription(fileWriter,
524         {
525             auto formatter = writer.newFormatter(fileWriter);
526             scope(exit) destroy(formatter.sink);
527 
528             // Write the function signature.
529             writer.writeCodeBlock(fileWriter,
530             {
531                 assert(attributeStack.length > 0,
532                        "Attributes stack must not be empty when writing " ~
533                        "function attributes");
534                 // Attributes like public, etc.
535                 writeAttributes(fileWriter, formatter, attrs);
536                 // Return type and function name, with special case fo constructor
537                 static if (__traits(hasMember, typeof(fn), "returnType"))
538                 {
539                     if (fn.returnType)
540                     {
541                         formatter.format(fn.returnType);
542                         fileWriter.put(" ");
543                     } else {
544                         fileWriter.put("auto ");
545                     }
546                     formatter.format(fn.name);
547                 }
548                 else
549                 {
550                     fileWriter.put("this");
551                 }
552                 // Template params
553                 if (fn.templateParameters !is null)
554                     formatter.format(fn.templateParameters);
555                 // Function params
556                 if (fn.parameters !is null)
557                     formatter.format(fn.parameters);
558                 // Attributes like const, nothrow, etc.
559                 foreach (a; fn.memberFunctionAttributes)
560                 {
561                     fileWriter.put(" ");
562                     formatter.format(a);
563                 }
564                 // Template constraint
565                 if (fn.constraint)
566                 {
567                     fileWriter.put(" ");
568                     formatter.format(fn.constraint);
569                 }
570             });
571 
572             summary = writer.readAndWriteComment(fileWriter, fn.comment,
573                 prevComments, fn.functionBody, getUnittestDocTuple(fn));
574         });
575         string fdName;
576         static if (__traits(hasMember, typeof(fn), "name"))
577             fdName = fn.name.text;
578         else
579             fdName = "this";
580         auto fnItem = Item(itemURL, fdName, summary, null, fn);
581         memberStack[$ - 2].functions ~= fnItem;
582         prevComments.length = prevComments.length + 1;
583         fn.accept(this);
584 
585         // The function may have nested functions/classes/etc, so at the very
586         // least we need to close their files, and once public/private works even
587         // document them.
588         memberStack.back.write(fileWriter, writer);
589         prevComments.popBack();
590     }
591 
592     /**
593      * Writes an alias' type to the given range and returns it.
594      * Params:
595      *     dst  = The range to write to
596      *     name = the name of the alias
597      *     t    = the aliased type
598      * Returns: A string reperesentation of the given type.
599      */
600     string writeAliasType(R)(ref R dst, string name, const Type t)
601     {
602         if (t is null)
603             return null;
604         string formatted = writer.formatNode(t);
605         writer.writeCodeBlock(dst,
606         {
607             dst.put("alias %s = ".format(name));
608             dst.put(formatted);
609         });
610         return formatted;
611     }
612 
613 
614     /**
615      * Generate links from symbols in input to files documenting those symbols.
616      *
617      * Note: The current implementation is far from perfect. It doesn't try
618      * to parse input; it just searches for alphanumeric words and patterns like
619      * "alnumword.otheralnumword" and asks SymbolDatabase to find a reference
620      * to them.
621      *
622      * TODO: Improve this by trying to parse input as D code first,
623      * only falling back to current implementation if the parsing fails.
624      * Parsing would only be used to correctly detect names, but must not
625      * reformat any code from input.
626      *
627      * Params:
628      *
629      * input = String to find symbols in.
630      *
631      * Returns:
632      *
633      * string with symbols replaced by links (links' format depends on Writer).
634      */
635     string crossReference(string input) @trusted nothrow {
636         bool isNameCharacter(dchar c) {
637             char c8 = cast(char)c;
638             return c8 == c && (c8.isAlphaNum || "_.".canFind(c8));
639         }
640 
641         auto app = appender!string();
642         dchar prevC = '\0';
643         dchar c;
644 
645         // Scan a symbol name. When done, both c and input.front will be set to
646         // the first character after the name.
647         string scanName() {
648             auto scanApp = appender!string();
649             while(!input.empty) {
650                 c = input.front;
651                 if (!isNameCharacter(c) && isNameCharacter(prevC)) {
652                     break;
653                 }
654                 scanApp.put(c);
655                 prevC = c;
656                 input.popFront();
657             }
658             return scanApp.data;
659         }
660 
661         // There should be no UTF decoding errors as we validate text
662         // when loading with std.file.readText().
663         try while(!input.empty) {
664             c = input.front;
665             if (isNameCharacter(c) && !isNameCharacter(prevC)) {
666                 string name = scanName();
667 
668                 auto link = database.crossReference(writer, stack, name);
669                 size_t partIdx = 0;
670 
671                 if (link !is null) {
672                     writer.writeLink(app, link, { app.put(name); });
673                 } else {
674                     // Attempt to cross-reference individual parts of the name
675                     // (e.g. "variable.method") will not match anything if
676                     // "variable" is a local variable "method" by itself may
677                     // still match something)
678                     foreach (part; name.splitter(".")) {
679                         if (partIdx++ > 0) {
680                             app.put(".");
681                         }
682                         link = database.crossReference(writer, stack, part);
683                         if (link !is null) {
684                             writer.writeLink(app, link, { app.put(part); });
685                         } else {
686                             app.put(part);
687                         }
688                     }
689                 }
690             }
691 
692             if (input.empty) {
693                 break;
694             }
695 
696             // Even if scanName was called above, c is the first character
697             // *after* scanName.
698             app.put(c);
699             prevC = c;
700             // Must check again because scanName might have exhausted the input.
701             input.popFront();
702         } catch(Exception e) {
703             writeln("Unexpected exception when cross-referencing: ", e.msg)
704                 .assumeWontThrow;
705         }
706 
707         return app.data;
708     }
709 
710     /**
711      * Params:
712      *
713      * name    = The symbol's name
714      * first   = Set to true if this is the first time that pushSymbol has been
715      *           called for this name.
716      * itemURL = URL to use in the Item for this symbol will be written here.
717      *
718      * Returns: A range to write the symbol's documentation to.
719      */
720     auto pushSymbol(string name, ref bool first, ref string itemURL)
721     {
722         import std.array : array, join;
723         import std.string : format;
724         stack ~= name;
725         memberStack.length = memberStack.length + 1;
726 
727         // Sets first
728         auto result = writer.pushSymbol(stack, database, first, itemURL);
729 
730         if(first)
731         {
732             writer.writeHeader(result, name, writer.moduleNameLength);
733             writer.writeBreadcrumbs(result, stack, database);
734             writer.writeTOC(result, moduleName);
735         }
736         else
737         {
738             writer.writeSeparator(result);
739         }
740         writer.writeSymbolStart(result, itemURL);
741         return result;
742     }
743 
744     void popSymbol(R)(ref R dst)
745     {
746         writer.writeSymbolEnd(dst);
747         stack.popBack();
748         memberStack.popBack();
749         writer.popSymbol();
750     }
751 
752     void pushAttributes() { attributeStack.length = attributeStack.length + 1; }
753 
754     void popAttributes() { attributeStack.popBack(); }
755 
756 
757     /// The module name in "package.package.module" format.
758     string moduleName;
759 
760     const(Attribute)[][] attributeStack;
761     Comment[] prevComments;
762     /** Namespace stack of the current symbol,
763      *
764      * E.g. ["package", "subpackage", "module", "Class", "member"]
765      */
766     string[] stack;
767     /** Every item of this stack corresponds to a parent module/class/etc of the
768      * current symbol, but not package.
769      *
770      * Each Members struct is used to accumulate all members of that module/class/etc
771      * so the list of all members can be generated.
772      */
773     Members[] memberStack;
774     TestRange[][size_t] unitTestMapping;
775     const(ubyte[]) fileBytes;
776     const(Config)* config;
777     /// Information about modules and symbols for e.g. cross-referencing.
778     SymbolDatabase database;
779     Writer writer;
780 }