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