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 }