1 // Distributed under the Boost Software License, Version 1.0. 2 // (See accompanying file LICENSE_1_0.txt or copy at 3 // http://www.boost.org/LICENSE_1_0.txt) 4 5 module dscanner.analysis.properly_documented_public_functions; 6 7 import dparse.lexer; 8 import dparse.ast; 9 import dscanner.analysis.base : BaseAnalyzer; 10 11 import std.format : format; 12 import std.range.primitives; 13 import std.stdio; 14 15 /** 16 * Requires each public function to contain the following ddoc sections 17 - PARAMS: 18 - if the function has at least one parameter 19 - every parameter must have a ddoc params entry (applies for template paramters too) 20 - Ddoc params entries without a parameter trigger warnings as well 21 - RETURNS: (except if it's void, only functions) 22 */ 23 class ProperlyDocumentedPublicFunctions : BaseAnalyzer 24 { 25 enum string MISSING_PARAMS_KEY = "dscanner.style.doc_missing_params"; 26 enum string MISSING_PARAMS_MESSAGE = "Parameter %s isn't documented in the `Params` section."; 27 enum string MISSING_TEMPLATE_PARAMS_MESSAGE 28 = "Template parameters %s isn't documented in the `Params` section."; 29 30 enum string NON_EXISTENT_PARAMS_KEY = "dscanner.style.doc_non_existing_params"; 31 enum string NON_EXISTENT_PARAMS_MESSAGE = "Documented parameter %s isn't a function parameter."; 32 33 enum string MISSING_RETURNS_KEY = "dscanner.style.doc_missing_returns"; 34 enum string MISSING_RETURNS_MESSAGE = "A public function needs to contain a `Returns` section."; 35 36 /// 37 this(string fileName, bool skipTests = false) 38 { 39 super(fileName, null, skipTests); 40 } 41 42 override void visit(const Module mod) 43 { 44 islastSeenVisibilityLabelPublic = true; 45 mod.accept(this); 46 postCheckSeenDdocParams(); 47 } 48 49 override void visit(const Declaration decl) 50 { 51 import std.algorithm.searching : any; 52 import std.algorithm.iteration : map; 53 54 // skip private symbols 55 enum tokPrivate = tok!"private", 56 tokProtected = tok!"protected", 57 tokPackage = tok!"package", 58 tokPublic = tok!"public"; 59 60 if (decl.attributes.length > 0) 61 { 62 const bool isPublic = !decl.attributes.map!`a.attribute`.any!(x => x == tokPrivate || 63 x == tokProtected || 64 x == tokPackage); 65 // recognize label blocks 66 if (!hasDeclaration(decl)) 67 islastSeenVisibilityLabelPublic = isPublic; 68 69 if (!isPublic) 70 return; 71 } 72 73 if (islastSeenVisibilityLabelPublic || decl.attributes.map!`a.attribute`.any!(x => x == tokPublic)) 74 { 75 if (decl.functionDeclaration !is null || 76 decl.templateDeclaration !is null || 77 decl.mixinTemplateDeclaration !is null || 78 decl.classDeclaration !is null || 79 decl.structDeclaration !is null) 80 decl.accept(this); 81 } 82 } 83 84 override void visit(const TemplateDeclaration decl) 85 { 86 setLastDdocParams(decl.name.line, decl.name.column, decl.comment); 87 checkDdocParams(decl.name.line, decl.name.column, decl.templateParameters); 88 89 withinTemplate = true; 90 scope(exit) withinTemplate = false; 91 decl.accept(this); 92 } 93 94 override void visit(const MixinTemplateDeclaration decl) 95 { 96 decl.accept(this); 97 } 98 99 override void visit(const StructDeclaration decl) 100 { 101 setLastDdocParams(decl.name.line, decl.name.column, decl.comment); 102 checkDdocParams(decl.name.line, decl.name.column, decl.templateParameters); 103 decl.accept(this); 104 } 105 106 override void visit(const ClassDeclaration decl) 107 { 108 setLastDdocParams(decl.name.line, decl.name.column, decl.comment); 109 checkDdocParams(decl.name.line, decl.name.column, decl.templateParameters); 110 decl.accept(this); 111 } 112 113 override void visit(const FunctionDeclaration decl) 114 { 115 import std.algorithm.searching : any; 116 117 // ignore header declaration for now 118 if (decl.functionBody is null) 119 return; 120 121 auto comment = setLastDdocParams(decl.name.line, decl.name.column, decl.comment); 122 123 checkDdocParams(decl.name.line, decl.name.column, decl.parameters, decl.templateParameters); 124 125 enum voidType = tok!"void"; 126 127 if (decl.returnType is null || decl.returnType.type2.builtinType != voidType) 128 if (!(comment.isDitto || withinTemplate || comment.sections.any!(s => s.name == "Returns"))) 129 addErrorMessage(decl.name.line, decl.name.column, MISSING_RETURNS_KEY, MISSING_RETURNS_MESSAGE); 130 } 131 132 alias visit = BaseAnalyzer.visit; 133 134 private: 135 bool islastSeenVisibilityLabelPublic; 136 bool withinTemplate; 137 138 static struct Function 139 { 140 bool active; 141 size_t line, column; 142 const(string)[] ddocParams; 143 bool[string] params; 144 } 145 Function lastSeenFun; 146 147 // find invalid ddoc parameters (i.e. they don't occur in a function declaration) 148 void postCheckSeenDdocParams() 149 { 150 import std.format : format; 151 152 if (lastSeenFun.active) 153 foreach (p; lastSeenFun.ddocParams) 154 if (p !in lastSeenFun.params) 155 addErrorMessage(lastSeenFun.line, lastSeenFun.column, NON_EXISTENT_PARAMS_KEY, 156 NON_EXISTENT_PARAMS_MESSAGE.format(p)); 157 158 lastSeenFun.active = false; 159 } 160 161 auto setLastDdocParams(size_t line, size_t column, string commentText) 162 { 163 import ddoc.comments : parseComment; 164 import std.algorithm.searching : find; 165 import std.algorithm.iteration : map; 166 import std.array : array; 167 168 const comment = parseComment(commentText, null); 169 if (!comment.isDitto && !withinTemplate) 170 { 171 // check old function for invalid ddoc params 172 if (lastSeenFun.active) 173 postCheckSeenDdocParams(); 174 175 const paramSection = comment.sections.find!(s => s.name == "Params"); 176 if (paramSection.empty) 177 { 178 lastSeenFun = Function(true, line, column, null); 179 } 180 else 181 { 182 auto ddocParams = paramSection[0].mapping.map!(a => a[0]).array; 183 lastSeenFun = Function(true, line, column, ddocParams); 184 } 185 } 186 187 return comment; 188 } 189 190 void checkDdocParams(size_t line, size_t column, const Parameters params, 191 const TemplateParameters templateParameters = null) 192 { 193 import std.array : array; 194 import std.algorithm.searching : canFind, countUntil; 195 import std.algorithm.iteration : map; 196 import std.algorithm.mutation : remove; 197 import std.range : indexed, iota; 198 199 // convert templateParameters into a string[] for faster access 200 const(TemplateParameter)[] templateList; 201 if (const tp = templateParameters) 202 if (const tpl = tp.templateParameterList) 203 templateList = tpl.items; 204 string[] tlList = templateList.map!(a => templateParamName(a)).array; 205 206 // make a copy of all parameters and remove the seen ones later during the loop 207 size_t[] unseenTemplates = templateList.length.iota.array; 208 209 if (lastSeenFun.active && params !is null) 210 foreach (p; params.parameters) 211 { 212 string templateName; 213 if (const t = p.type) 214 if (const t2 = t.type2) 215 if (const tip = t2.typeIdentifierPart) 216 if (const iot = tip.identifierOrTemplateInstance) 217 templateName = iot.identifier.text; 218 219 const idx = tlList.countUntil(templateName); 220 if (idx >= 0) 221 { 222 unseenTemplates = unseenTemplates.remove(idx); 223 tlList = tlList.remove(idx); 224 // documenting template parameter should be allowed 225 lastSeenFun.params[templateName] = true; 226 } 227 228 if (!lastSeenFun.ddocParams.canFind(p.name.text)) 229 addErrorMessage(line, column, MISSING_PARAMS_KEY, 230 format(MISSING_PARAMS_MESSAGE, p.name.text)); 231 else 232 lastSeenFun.params[p.name.text] = true; 233 } 234 235 // now check the remaining, not used template parameters 236 auto unseenTemplatesArr = templateList.indexed(unseenTemplates).array; 237 checkDdocParams(line, column, unseenTemplatesArr); 238 } 239 240 void checkDdocParams(size_t line, size_t column, const TemplateParameters templateParams) 241 { 242 243 if (lastSeenFun.active && templateParams !is null && templateParams.templateParameterList !is null) 244 checkDdocParams(line, column, templateParams.templateParameterList.items); 245 } 246 247 void checkDdocParams(size_t line, size_t column, const TemplateParameter[] templateParams) 248 { 249 import std.algorithm.searching : canFind; 250 foreach (p; templateParams) 251 { 252 const name = templateParamName(p); 253 assert(name, "Invalid template parameter name."); // this shouldn't happen 254 if (!lastSeenFun.ddocParams.canFind(name)) 255 addErrorMessage(line, column, MISSING_PARAMS_KEY, 256 format(MISSING_TEMPLATE_PARAMS_MESSAGE, name)); 257 else 258 lastSeenFun.params[name] = true; 259 } 260 } 261 262 static string templateParamName(const TemplateParameter p) 263 { 264 if (p.templateTypeParameter) 265 return p.templateTypeParameter.identifier.text; 266 if (p.templateValueParameter) 267 return p.templateValueParameter.identifier.text; 268 if (p.templateAliasParameter) 269 return p.templateAliasParameter.identifier.text; 270 if (p.templateTupleParameter) 271 return p.templateTupleParameter.identifier.text; 272 if (p.templateThisParameter) 273 return p.templateThisParameter.templateTypeParameter.identifier.text; 274 275 return null; 276 } 277 278 bool hasDeclaration(const Declaration decl) 279 { 280 import std.meta : AliasSeq; 281 alias properties = AliasSeq!( 282 "aliasDeclaration", 283 "aliasThisDeclaration", 284 "anonymousEnumDeclaration", 285 "attributeDeclaration", 286 "classDeclaration", 287 "conditionalDeclaration", 288 "constructor", 289 "debugSpecification", 290 "destructor", 291 "enumDeclaration", 292 "eponymousTemplateDeclaration", 293 "functionDeclaration", 294 "importDeclaration", 295 "interfaceDeclaration", 296 "invariant_", 297 "mixinDeclaration", 298 "mixinTemplateDeclaration", 299 "postblit", 300 "pragmaDeclaration", 301 "sharedStaticConstructor", 302 "sharedStaticDestructor", 303 "staticAssertDeclaration", 304 "staticConstructor", 305 "staticDestructor", 306 "structDeclaration", 307 "templateDeclaration", 308 "unionDeclaration", 309 "unittest_", 310 "variableDeclaration", 311 "versionSpecification", 312 ); 313 if (decl.declarations !is null) 314 return false; 315 316 auto isNull = true; 317 foreach (property; properties) 318 if (mixin("decl." ~ property ~ " !is null")) 319 isNull = false; 320 321 return !isNull; 322 } 323 } 324 325 version(unittest) 326 { 327 import std.stdio : stderr; 328 import std.format : format; 329 import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig; 330 import dscanner.analysis.helpers : assertAnalyzerWarnings; 331 } 332 333 // missing params 334 unittest 335 { 336 StaticAnalysisConfig sac = disabledConfig; 337 sac.properly_documented_public_functions = Check.enabled; 338 339 assertAnalyzerWarnings(q{ 340 /** 341 Some text 342 */ 343 void foo(int k){} // [warn]: %s 344 }}.format( 345 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 346 ), sac); 347 348 assertAnalyzerWarnings(q{ 349 /** 350 Some text 351 */ 352 void foo(int K)(){} // [warn]: %s 353 }}.format( 354 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("K") 355 ), sac); 356 357 assertAnalyzerWarnings(q{ 358 /** 359 Some text 360 */ 361 struct Foo(Bar){} // [warn]: %s 362 }}.format( 363 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 364 ), sac); 365 366 assertAnalyzerWarnings(q{ 367 /** 368 Some text 369 */ 370 class Foo(Bar){} // [warn]: %s 371 }}.format( 372 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 373 ), sac); 374 375 assertAnalyzerWarnings(q{ 376 /** 377 Some text 378 */ 379 template Foo(Bar){} // [warn]: %s 380 }}.format( 381 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 382 ), sac); 383 384 385 // test no parameters 386 assertAnalyzerWarnings(q{ 387 /** Some text */ 388 void foo(){} 389 }}, sac); 390 391 assertAnalyzerWarnings(q{ 392 /** Some text */ 393 struct Foo(){} 394 }}, sac); 395 396 assertAnalyzerWarnings(q{ 397 /** Some text */ 398 class Foo(){} 399 }}, sac); 400 401 assertAnalyzerWarnings(q{ 402 /** Some text */ 403 template Foo(){} 404 }}, sac); 405 406 } 407 408 // missing returns (only functions) 409 unittest 410 { 411 StaticAnalysisConfig sac = disabledConfig; 412 sac.properly_documented_public_functions = Check.enabled; 413 414 assertAnalyzerWarnings(q{ 415 /** 416 Some text 417 */ 418 int foo(){} // [warn]: %s 419 }}.format( 420 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 421 ), sac); 422 423 assertAnalyzerWarnings(q{ 424 /** 425 Some text 426 */ 427 auto foo(){} // [warn]: %s 428 }}.format( 429 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 430 ), sac); 431 } 432 433 // ignore private 434 unittest 435 { 436 StaticAnalysisConfig sac = disabledConfig; 437 sac.properly_documented_public_functions = Check.enabled; 438 439 assertAnalyzerWarnings(q{ 440 /** 441 Some text 442 */ 443 private void foo(int k){} 444 }}, sac); 445 446 // with block 447 assertAnalyzerWarnings(q{ 448 private: 449 /** 450 Some text 451 */ 452 private void foo(int k){} 453 public int bar(){} // [warn]: %s 454 public: 455 int foobar(){} // [warn]: %s 456 }}.format( 457 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 458 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 459 ), sac); 460 461 // with block (template) 462 assertAnalyzerWarnings(q{ 463 private: 464 /** 465 Some text 466 */ 467 private template foo(int k){} 468 public template bar(T){} // [warn]: %s 469 public: 470 template foobar(T){} // [warn]: %s 471 }}.format( 472 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 473 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 474 ), sac); 475 476 // with block (struct) 477 assertAnalyzerWarnings(q{ 478 private: 479 /** 480 Some text 481 */ 482 private struct foo(int k){} 483 public struct bar(T){} // [warn]: %s 484 public: 485 struct foobar(T){} // [warn]: %s 486 }}.format( 487 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 488 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 489 ), sac); 490 } 491 492 // test parameter names 493 unittest 494 { 495 StaticAnalysisConfig sac = disabledConfig; 496 sac.properly_documented_public_functions = Check.enabled; 497 498 assertAnalyzerWarnings(q{ 499 /** 500 * Description. 501 * 502 * Params: 503 * 504 * Returns: 505 * A long description. 506 */ 507 int foo(int k){} // [warn]: %s 508 }}.format( 509 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 510 ), sac); 511 512 assertAnalyzerWarnings(q{ 513 /** 514 Description. 515 516 Params: 517 val = A stupid parameter 518 k = A stupid parameter 519 520 Returns: 521 A long description. 522 */ 523 int foo(int k){} // [warn]: %s 524 }}.format( 525 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("val") 526 ), sac); 527 528 assertAnalyzerWarnings(q{ 529 /** 530 Description. 531 532 Params: 533 534 Returns: 535 A long description. 536 */ 537 int foo(int k){} // [warn]: %s 538 }}.format( 539 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 540 ), sac); 541 542 assertAnalyzerWarnings(q{ 543 /** 544 Description. 545 546 Params: 547 foo = A stupid parameter 548 bad = A stupid parameter (does not exist) 549 foobar = A stupid parameter 550 551 Returns: 552 A long description. 553 */ 554 int foo(int foo, int foobar){} // [warn]: %s 555 }}.format( 556 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") 557 ), sac); 558 559 assertAnalyzerWarnings(q{ 560 /** 561 Description. 562 563 Params: 564 foo = A stupid parameter 565 bad = A stupid parameter (does not exist) 566 foobar = A stupid parameter 567 568 Returns: 569 A long description. 570 */ 571 struct foo(int foo, int foobar){} // [warn]: %s 572 }}.format( 573 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") 574 ), sac); 575 576 // properly documented 577 assertAnalyzerWarnings(q{ 578 /** 579 Description. 580 581 Params: 582 foo = A stupid parameter 583 bar = A stupid parameter 584 585 Returns: 586 A long description. 587 */ 588 int foo(int foo, int bar){} 589 }}, sac); 590 591 assertAnalyzerWarnings(q{ 592 /** 593 Description. 594 595 Params: 596 foo = A stupid parameter 597 bar = A stupid parameter 598 599 Returns: 600 A long description. 601 */ 602 struct foo(int foo, int bar){} 603 }}, sac); 604 } 605 606 // support ditto 607 unittest 608 { 609 StaticAnalysisConfig sac = disabledConfig; 610 sac.properly_documented_public_functions = Check.enabled; 611 612 assertAnalyzerWarnings(q{ 613 /** 614 * Description. 615 * 616 * Params: 617 * k = A stupid parameter 618 * 619 * Returns: 620 * A long description. 621 */ 622 int foo(int k){} 623 624 /// ditto 625 int bar(int k){} 626 }}, sac); 627 628 assertAnalyzerWarnings(q{ 629 /** 630 * Description. 631 * 632 * Params: 633 * k = A stupid parameter 634 * K = A stupid parameter 635 * 636 * Returns: 637 * A long description. 638 */ 639 int foo(int k){} 640 641 /// ditto 642 struct Bar(K){} 643 }}, sac); 644 645 assertAnalyzerWarnings(q{ 646 /** 647 * Description. 648 * 649 * Params: 650 * k = A stupid parameter 651 * f = A stupid parameter 652 * 653 * Returns: 654 * A long description. 655 */ 656 int foo(int k){} 657 658 /// ditto 659 int bar(int f){} 660 }}, sac); 661 662 assertAnalyzerWarnings(q{ 663 /** 664 * Description. 665 * 666 * Params: 667 * k = A stupid parameter 668 * 669 * Returns: 670 * A long description. 671 */ 672 int foo(int k){} 673 674 /// ditto 675 int bar(int bar){} // [warn]: %s 676 }}.format( 677 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("bar") 678 ), sac); 679 680 assertAnalyzerWarnings(q{ 681 /** 682 * Description. 683 * 684 * Params: 685 * k = A stupid parameter 686 * bar = A stupid parameter 687 * f = A stupid parameter 688 * 689 * Returns: 690 * A long description. 691 * See_Also: 692 * $(REF takeExactly, std,range) 693 */ 694 int foo(int k){} // [warn]: %s 695 696 /// ditto 697 int bar(int bar){} 698 }}.format( 699 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("f") 700 ), sac); 701 } 702 703 // check correct ddoc headers 704 unittest 705 { 706 StaticAnalysisConfig sac = disabledConfig; 707 sac.properly_documented_public_functions = Check.enabled; 708 709 assertAnalyzerWarnings(q{ 710 /++ 711 Counts elements in the given 712 $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) 713 until the given predicate is true for one of the given $(D needles). 714 715 Params: 716 val = A stupid parameter 717 718 Returns: Awesome values. 719 +/ 720 string bar(string val){} 721 }}, sac); 722 723 assertAnalyzerWarnings(q{ 724 /++ 725 Counts elements in the given 726 $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) 727 until the given predicate is true for one of the given $(D needles). 728 729 Params: 730 val = A stupid parameter 731 732 Returns: Awesome values. 733 +/ 734 template bar(string val){} 735 }}, sac); 736 737 } 738 739 unittest 740 { 741 StaticAnalysisConfig sac = disabledConfig; 742 sac.properly_documented_public_functions = Check.enabled; 743 744 assertAnalyzerWarnings(q{ 745 /** 746 * Ddoc for the inner function appears here. 747 * This function is declared this way to allow for multiple variable-length 748 * template argument lists. 749 * --- 750 * abcde!("a", "b", "c")(100, x, y, z); 751 * --- 752 * Params: 753 * Args = foo 754 * U = bar 755 * T = barr 756 * varargs = foobar 757 * t = foo 758 * Returns: bar 759 */ 760 template abcde(Args ...) { 761 auto abcde(T, U...)(T t, U varargs) { 762 /// .... 763 } 764 } 765 }}, sac); 766 } 767 768 // Don't force the documentation of the template parameter if it's a used type in the parameter list 769 unittest 770 { 771 StaticAnalysisConfig sac = disabledConfig; 772 sac.properly_documented_public_functions = Check.enabled; 773 774 assertAnalyzerWarnings(q{ 775 /++ 776 An awesome description. 777 778 Params: 779 r = an input range. 780 781 Returns: Awesome values. 782 +/ 783 string bar(R)(R r){} 784 }}, sac); 785 786 assertAnalyzerWarnings(q{ 787 /++ 788 An awesome description. 789 790 Params: 791 r = an input range. 792 793 Returns: Awesome values. 794 +/ 795 string bar(P, R)(R r){}// [warn]: %s 796 }}.format( 797 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("P") 798 ), sac); 799 800 stderr.writeln("Unittest for ProperlyDocumentedPublicFunctions passed."); 801 }