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