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 }