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 analysis.properly_documented_public_functions; 6 7 import dparse.lexer; 8 import dparse.ast; 9 import 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 checkDdocParams(decl.name.line, decl.name.column, decl.parameters); 123 checkDdocParams(decl.name.line, decl.name.column, 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 { 192 import std.algorithm.searching : canFind; 193 194 if (lastSeenFun.active && params !is null) 195 foreach (p; params.parameters) 196 { 197 if (!lastSeenFun.ddocParams.canFind(p.name.text)) 198 addErrorMessage(line, column, MISSING_PARAMS_KEY, 199 format(MISSING_PARAMS_MESSAGE, p.name.text)); 200 else 201 lastSeenFun.params[p.name.text] = true; 202 } 203 } 204 205 void checkDdocParams(size_t line, size_t column, const TemplateParameters templateParams) 206 { 207 import std.algorithm.searching : canFind; 208 209 if (lastSeenFun.active && templateParams !is null && templateParams.templateParameterList !is null) 210 foreach (p; templateParams.templateParameterList.items) 211 { 212 auto name = templateParamName(p); 213 assert(name, "Invalid template parameter name."); // this shouldn't happen 214 if (!lastSeenFun.ddocParams.canFind(name)) 215 addErrorMessage(line, column, MISSING_PARAMS_KEY, 216 format(MISSING_TEMPLATE_PARAMS_MESSAGE, name)); 217 else 218 lastSeenFun.params[name] = true; 219 } 220 } 221 222 static string templateParamName(const TemplateParameter p) 223 { 224 if (p.templateTypeParameter) 225 return p.templateTypeParameter.identifier.text; 226 if (p.templateValueParameter) 227 return p.templateValueParameter.identifier.text; 228 if (p.templateAliasParameter) 229 return p.templateAliasParameter.identifier.text; 230 if (p.templateTupleParameter) 231 return p.templateTupleParameter.identifier.text; 232 if (p.templateThisParameter) 233 return p.templateThisParameter.templateTypeParameter.identifier.text; 234 235 return null; 236 } 237 238 bool hasDeclaration(const Declaration decl) 239 { 240 import std.meta : AliasSeq; 241 alias properties = AliasSeq!( 242 "aliasDeclaration", 243 "aliasThisDeclaration", 244 "anonymousEnumDeclaration", 245 "attributeDeclaration", 246 "classDeclaration", 247 "conditionalDeclaration", 248 "constructor", 249 "debugSpecification", 250 "destructor", 251 "enumDeclaration", 252 "eponymousTemplateDeclaration", 253 "functionDeclaration", 254 "importDeclaration", 255 "interfaceDeclaration", 256 "invariant_", 257 "mixinDeclaration", 258 "mixinTemplateDeclaration", 259 "postblit", 260 "pragmaDeclaration", 261 "sharedStaticConstructor", 262 "sharedStaticDestructor", 263 "staticAssertDeclaration", 264 "staticConstructor", 265 "staticDestructor", 266 "structDeclaration", 267 "templateDeclaration", 268 "unionDeclaration", 269 "unittest_", 270 "variableDeclaration", 271 "versionSpecification", 272 ); 273 if (decl.declarations !is null) 274 return false; 275 276 auto isNull = true; 277 foreach (property; properties) 278 if (mixin("decl." ~ property ~ " !is null")) 279 isNull = false; 280 281 return !isNull; 282 } 283 } 284 285 version(unittest) 286 { 287 import std.stdio : stderr; 288 import std.format : format; 289 import analysis.config : StaticAnalysisConfig, Check, disabledConfig; 290 import analysis.helpers : assertAnalyzerWarnings; 291 } 292 293 // missing params 294 unittest 295 { 296 StaticAnalysisConfig sac = disabledConfig; 297 sac.properly_documented_public_functions = Check.enabled; 298 299 assertAnalyzerWarnings(q{ 300 /** 301 Some text 302 */ 303 void foo(int k){} // [warn]: %s 304 }}.format( 305 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 306 ), sac); 307 308 assertAnalyzerWarnings(q{ 309 /** 310 Some text 311 */ 312 void foo(int K)(){} // [warn]: %s 313 }}.format( 314 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("K") 315 ), sac); 316 317 assertAnalyzerWarnings(q{ 318 /** 319 Some text 320 */ 321 struct Foo(Bar){} // [warn]: %s 322 }}.format( 323 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 324 ), sac); 325 326 assertAnalyzerWarnings(q{ 327 /** 328 Some text 329 */ 330 class Foo(Bar){} // [warn]: %s 331 }}.format( 332 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 333 ), sac); 334 335 assertAnalyzerWarnings(q{ 336 /** 337 Some text 338 */ 339 template Foo(Bar){} // [warn]: %s 340 }}.format( 341 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("Bar") 342 ), sac); 343 344 345 // test no parameters 346 assertAnalyzerWarnings(q{ 347 /** Some text */ 348 void foo(){} 349 }}, sac); 350 351 assertAnalyzerWarnings(q{ 352 /** Some text */ 353 struct Foo(){} 354 }}, sac); 355 356 assertAnalyzerWarnings(q{ 357 /** Some text */ 358 class Foo(){} 359 }}, sac); 360 361 assertAnalyzerWarnings(q{ 362 /** Some text */ 363 template Foo(){} 364 }}, sac); 365 366 } 367 368 // missing returns (only functions) 369 unittest 370 { 371 StaticAnalysisConfig sac = disabledConfig; 372 sac.properly_documented_public_functions = Check.enabled; 373 374 assertAnalyzerWarnings(q{ 375 /** 376 Some text 377 */ 378 int foo(){} // [warn]: %s 379 }}.format( 380 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 381 ), sac); 382 383 assertAnalyzerWarnings(q{ 384 /** 385 Some text 386 */ 387 auto foo(){} // [warn]: %s 388 }}.format( 389 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 390 ), sac); 391 } 392 393 // ignore private 394 unittest 395 { 396 StaticAnalysisConfig sac = disabledConfig; 397 sac.properly_documented_public_functions = Check.enabled; 398 399 assertAnalyzerWarnings(q{ 400 /** 401 Some text 402 */ 403 private void foo(int k){} 404 }}, sac); 405 406 // with block 407 assertAnalyzerWarnings(q{ 408 private: 409 /** 410 Some text 411 */ 412 private void foo(int k){} 413 public int bar(){} // [warn]: %s 414 public: 415 int foobar(){} // [warn]: %s 416 }}.format( 417 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 418 ProperlyDocumentedPublicFunctions.MISSING_RETURNS_MESSAGE, 419 ), sac); 420 421 // with block (template) 422 assertAnalyzerWarnings(q{ 423 private: 424 /** 425 Some text 426 */ 427 private template foo(int k){} 428 public template bar(T){} // [warn]: %s 429 public: 430 template foobar(T){} // [warn]: %s 431 }}.format( 432 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 433 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 434 ), sac); 435 436 // with block (struct) 437 assertAnalyzerWarnings(q{ 438 private: 439 /** 440 Some text 441 */ 442 private struct foo(int k){} 443 public struct bar(T){} // [warn]: %s 444 public: 445 struct foobar(T){} // [warn]: %s 446 }}.format( 447 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 448 ProperlyDocumentedPublicFunctions.MISSING_TEMPLATE_PARAMS_MESSAGE.format("T"), 449 ), sac); 450 } 451 452 // test parameter names 453 unittest 454 { 455 StaticAnalysisConfig sac = disabledConfig; 456 sac.properly_documented_public_functions = Check.enabled; 457 458 assertAnalyzerWarnings(q{ 459 /** 460 * Description. 461 * 462 * Params: 463 * 464 * Returns: 465 * A long description. 466 */ 467 int foo(int k){} // [warn]: %s 468 }}.format( 469 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 470 ), sac); 471 472 assertAnalyzerWarnings(q{ 473 /** 474 Description. 475 476 Params: 477 val = A stupid parameter 478 k = A stupid parameter 479 480 Returns: 481 A long description. 482 */ 483 int foo(int k){} // [warn]: %s 484 }}.format( 485 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("val") 486 ), sac); 487 488 assertAnalyzerWarnings(q{ 489 /** 490 Description. 491 492 Params: 493 494 Returns: 495 A long description. 496 */ 497 int foo(int k){} // [warn]: %s 498 }}.format( 499 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("k") 500 ), sac); 501 502 assertAnalyzerWarnings(q{ 503 /** 504 Description. 505 506 Params: 507 foo = A stupid parameter 508 bad = A stupid parameter (does not exist) 509 foobar = A stupid parameter 510 511 Returns: 512 A long description. 513 */ 514 int foo(int foo, int foobar){} // [warn]: %s 515 }}.format( 516 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") 517 ), sac); 518 519 assertAnalyzerWarnings(q{ 520 /** 521 Description. 522 523 Params: 524 foo = A stupid parameter 525 bad = A stupid parameter (does not exist) 526 foobar = A stupid parameter 527 528 Returns: 529 A long description. 530 */ 531 struct foo(int foo, int foobar){} // [warn]: %s 532 }}.format( 533 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("bad") 534 ), sac); 535 536 // properly documented 537 assertAnalyzerWarnings(q{ 538 /** 539 Description. 540 541 Params: 542 foo = A stupid parameter 543 bar = A stupid parameter 544 545 Returns: 546 A long description. 547 */ 548 int foo(int foo, int bar){} 549 }}, sac); 550 551 assertAnalyzerWarnings(q{ 552 /** 553 Description. 554 555 Params: 556 foo = A stupid parameter 557 bar = A stupid parameter 558 559 Returns: 560 A long description. 561 */ 562 struct foo(int foo, int bar){} 563 }}, sac); 564 } 565 566 // support ditto 567 unittest 568 { 569 StaticAnalysisConfig sac = disabledConfig; 570 sac.properly_documented_public_functions = Check.enabled; 571 572 assertAnalyzerWarnings(q{ 573 /** 574 * Description. 575 * 576 * Params: 577 * k = A stupid parameter 578 * 579 * Returns: 580 * A long description. 581 */ 582 int foo(int k){} 583 584 /// ditto 585 int bar(int k){} 586 }}, sac); 587 588 assertAnalyzerWarnings(q{ 589 /** 590 * Description. 591 * 592 * Params: 593 * k = A stupid parameter 594 * K = A stupid parameter 595 * 596 * Returns: 597 * A long description. 598 */ 599 int foo(int k){} 600 601 /// ditto 602 struct Bar(K){} 603 }}, sac); 604 605 assertAnalyzerWarnings(q{ 606 /** 607 * Description. 608 * 609 * Params: 610 * k = A stupid parameter 611 * f = A stupid parameter 612 * 613 * Returns: 614 * A long description. 615 */ 616 int foo(int k){} 617 618 /// ditto 619 int bar(int f){} 620 }}, sac); 621 622 assertAnalyzerWarnings(q{ 623 /** 624 * Description. 625 * 626 * Params: 627 * k = A stupid parameter 628 * 629 * Returns: 630 * A long description. 631 */ 632 int foo(int k){} 633 634 /// ditto 635 int bar(int bar){} // [warn]: %s 636 }}.format( 637 ProperlyDocumentedPublicFunctions.MISSING_PARAMS_MESSAGE.format("bar") 638 ), sac); 639 640 assertAnalyzerWarnings(q{ 641 /** 642 * Description. 643 * 644 * Params: 645 * k = A stupid parameter 646 * bar = A stupid parameter 647 * f = A stupid parameter 648 * 649 * Returns: 650 * A long description. 651 * See_Also: 652 * $(REF takeExactly, std,range) 653 */ 654 int foo(int k){} // [warn]: %s 655 656 /// ditto 657 int bar(int bar){} 658 }}.format( 659 ProperlyDocumentedPublicFunctions.NON_EXISTENT_PARAMS_MESSAGE.format("f") 660 ), sac); 661 } 662 663 // check correct ddoc headers 664 unittest 665 { 666 StaticAnalysisConfig sac = disabledConfig; 667 sac.properly_documented_public_functions = Check.enabled; 668 669 assertAnalyzerWarnings(q{ 670 /++ 671 Counts elements in the given 672 $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) 673 until the given predicate is true for one of the given $(D needles). 674 675 Params: 676 val = A stupid parameter 677 678 Returns: Awesome values. 679 +/ 680 string bar(string val){} 681 }}, sac); 682 683 assertAnalyzerWarnings(q{ 684 /++ 685 Counts elements in the given 686 $(REF_ALTTEXT forward range, isForwardRange, std,range,primitives) 687 until the given predicate is true for one of the given $(D needles). 688 689 Params: 690 val = A stupid parameter 691 692 Returns: Awesome values. 693 +/ 694 template bar(string val){} 695 }}, sac); 696 697 stderr.writeln("Unittest for ProperlyDocumentedPublicFunctions passed."); 698 } 699 700 unittest 701 { 702 StaticAnalysisConfig sac = disabledConfig; 703 sac.properly_documented_public_functions = Check.enabled; 704 705 assertAnalyzerWarnings(q{ 706 /** 707 * Ddoc for the inner function appears here. 708 * This function is declared this way to allow for multiple variable-length 709 * template argument lists. 710 * --- 711 * abcde!("a", "b", "c")(100, x, y, z); 712 * --- 713 * Params: 714 * Args = foo 715 * U = bar 716 * T = barr 717 * varargs = foobar 718 * t = foo 719 * Returns: bar 720 */ 721 template abcde(Args ...) { 722 auto abcde(T, U...)(T t, U varargs) { 723 /// .... 724 } 725 } 726 }}, sac); 727 }