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 }