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.has_public_example;
6 
7 import analysis.base;
8 import dsymbol.scope_ : Scope;
9 import dparse.ast;
10 import dparse.lexer;
11 
12 import std.algorithm;
13 import std.stdio;
14 
15 /**
16  * Checks for public declarations without a documented unittests.
17  * For now, variable and enum declarations aren't checked.
18  */
19 class HasPublicExampleCheck : BaseAnalyzer
20 {
21 	alias visit = BaseAnalyzer.visit;
22 
23 	this(string fileName, const(Scope)* sc, bool skipTests = false)
24 	{
25 		super(fileName, sc, skipTests);
26 	}
27 
28 	override void visit(const Module mod)
29 	{
30 		// the last seen declaration is memorized
31 		Declaration lastDecl;
32 
33 		// keep track of ddoced unittests after visiting lastDecl
34 		bool hasNoDdocUnittest;
35 
36 		// on lastDecl reset we check for seen ddoced unittests since lastDecl was observed
37 		void checkLastDecl()
38 		{
39 			if (lastDecl !is null && hasNoDdocUnittest)
40 				triggerError(lastDecl);
41 			lastDecl = null;
42 		}
43 
44 		// check all public top-level declarations
45 		foreach (decl; mod.declarations)
46 		{
47 			if (!isPublic(decl.attributes))
48 			{
49 				checkLastDecl();
50 				continue;
51 			}
52 
53 			const bool hasDdocHeader = hasDdocHeader(decl);
54 
55 			// check the documentation of a unittest declaration
56 			if (decl.unittest_ !is null)
57 			{
58 				if (hasDdocHeader)
59 					hasNoDdocUnittest = false;
60 			}
61 			// add all declarations that could be publicly documented to the lastDecl "stack"
62 			else if (hasDittableDecl(decl))
63 			{
64 				// ignore dittoed declarations
65 				if (hasDittos(decl))
66 					continue;
67 
68 				// new public symbol -> check the previous decl
69 				checkLastDecl;
70 
71 				lastDecl = hasDdocHeader ? cast(Declaration) decl : null;
72 				hasNoDdocUnittest = true;
73 			}
74 			else
75 			// ran into variableDeclaration or something else -> reset & validate current lastDecl "stack"
76 				checkLastDecl;
77 		}
78 		checkLastDecl;
79 	}
80 
81 private:
82 
83 	bool hasDitto(Decl)(const Decl decl)
84 	{
85 		import ddoc.comments : parseComment;
86 		if (decl is null || decl.comment is null)
87 			return false;
88 
89 		return parseComment(decl.comment, null).isDitto;
90 	}
91 
92 	bool hasDittos(Decl)(const Decl decl)
93 	{
94 		foreach (property; possibleDeclarations)
95 			if (mixin("hasDitto(decl." ~ property ~ ")"))
96 				return true;
97 		return false;
98 	}
99 
100 	bool hasDittableDecl(Decl)(const Decl decl)
101 	{
102 		foreach (property; possibleDeclarations)
103 			if (mixin("decl." ~ property ~ " !is null"))
104 				return true;
105 		return false;
106 	}
107 
108 	import std.meta : AliasSeq;
109 	alias possibleDeclarations = AliasSeq!(
110 		"classDeclaration",
111 		"enumDeclaration",
112 		"functionDeclaration",
113 		"interfaceDeclaration",
114 		"structDeclaration",
115 		"templateDeclaration",
116 		"unionDeclaration",
117 		//"variableDeclaration",
118 	);
119 
120 	bool hasDdocHeader(const Declaration decl)
121 	{
122 		if (decl.declarations !is null)
123 			return false;
124 
125 		// unittest can have ddoc headers as well, but don't have a name
126 		if (decl.unittest_ !is null && decl.unittest_.comment.ptr !is null)
127 			return true;
128 
129 		foreach (property; possibleDeclarations)
130 			if (mixin("decl." ~ property ~ " !is null && decl." ~ property ~ ".comment.ptr !is null"))
131 				return true;
132 
133 		return false;
134 	}
135 
136 	bool isPublic(const Attribute[] attrs)
137 	{
138 		import dparse.lexer : tok;
139 
140 		enum tokPrivate = tok!"private", tokProtected = tok!"protected", tokPackage = tok!"package";
141 
142 		if (attrs.map!`a.attribute`.any!(x => x == tokPrivate || x == tokProtected || x == tokPackage))
143 			return false;
144 
145 		return true;
146 	}
147 
148 	void triggerError(const Declaration decl)
149 	{
150 		foreach (property; possibleDeclarations)
151 			if (auto fn = mixin("decl." ~ property))
152 				addMessage(fn.name.line, fn.name.column, fn.name.text);
153 	}
154 
155 	void addMessage(size_t line, size_t column, string name)
156 	{
157 		import std..string : format;
158 
159 		addErrorMessage(line, column, "dscanner.style.has_public_example", name is null
160 				? "Public declaration has no documented example."
161 				: format("Public declaration '%s' has no documented example.", name));
162 	}
163 }
164 
165 unittest
166 {
167 	import std.stdio : stderr;
168 	import std.format : format;
169 	import analysis.config : StaticAnalysisConfig, Check, disabledConfig;
170 	import analysis.helpers : assertAnalyzerWarnings;
171 
172 	StaticAnalysisConfig sac = disabledConfig();
173 	sac.has_public_example = Check.enabled;
174 
175 	assertAnalyzerWarnings(q{
176 		/// C
177 		class C{}
178 		///
179 		unittest {}
180 
181 		/// I
182 		interface I{}
183 		///
184 		unittest {}
185 
186 		/// e
187 		enum e = 0;
188 		///
189 		unittest {}
190 
191 		/// f
192 		void f(){}
193 		///
194 		unittest {}
195 
196 		/// S
197 		struct S{}
198 		///
199 		unittest {}
200 
201 		/// T
202 		template T(){}
203 		///
204 		unittest {}
205 
206 		/// U
207 		union U{}
208 		///
209 		unittest {}
210 	}, sac);
211 
212 	// enums or variables don't need to have public unittest
213 	assertAnalyzerWarnings(q{
214 		/// C
215 		class C{} // [warn]: Public declaration 'C' has no documented example.
216 		unittest {}
217 
218 		/// I
219 		interface I{} // [warn]: Public declaration 'I' has no documented example.
220 		unittest {}
221 
222 		/// f
223 		void f(){} // [warn]: Public declaration 'f' has no documented example.
224 		unittest {}
225 
226 		/// S
227 		struct S{} // [warn]: Public declaration 'S' has no documented example.
228 		unittest {}
229 
230 		/// T
231 		template T(){} // [warn]: Public declaration 'T' has no documented example.
232 		unittest {}
233 
234 		/// U
235 		union U{} // [warn]: Public declaration 'U' has no documented example.
236 		unittest {}
237 	}, sac);
238 
239 	// test module header unittest
240 	assertAnalyzerWarnings(q{
241 		unittest {}
242 		/// C
243 		class C{} // [warn]: Public declaration 'C' has no documented example.
244 	}, sac);
245 
246 	// test documented module header unittest
247 	assertAnalyzerWarnings(q{
248 		///
249 		unittest {}
250 		/// C
251 		class C{} // [warn]: Public declaration 'C' has no documented example.
252 	}, sac);
253 
254 	// test multiple unittest blocks
255 	assertAnalyzerWarnings(q{
256 		/// C
257 		class C{} // [warn]: Public declaration 'C' has no documented example.
258 		unittest {}
259 		unittest {}
260 		unittest {}
261 
262 		/// U
263 		union U{}
264 		unittest {}
265 		///
266 		unittest {}
267 		unittest {}
268 	}, sac);
269 
270 	/// check private
271 	assertAnalyzerWarnings(q{
272 		/// C
273 		private class C{}
274 
275 		/// I
276 		protected interface I{}
277 
278 		/// e
279 		package enum e = 0;
280 
281 		/// f
282 		package(std) void f(){}
283 
284 		/// S
285 		extern(C) struct S{}
286 		///
287 		unittest {}
288 	}, sac);
289 
290 	// check intermediate private declarations
291 	// removed for issue #500
292 	/*assertAnalyzerWarnings(q{
293 		/// C
294 		class C{}
295 		private void foo(){}
296 		///
297 		unittest {}
298 	}, sac);*/
299 
300 	// check intermediate ditto-ed declarations
301 	assertAnalyzerWarnings(q{
302 		/// I
303 		interface I{}
304 		/// ditto
305 		void f(){}
306 		///
307 		unittest {}
308 	}, sac);
309 
310 	// test reset on private symbols (#500)
311 	assertAnalyzerWarnings(q{
312 		///
313 		void dirName(C)(C[] path) {} // [warn]: Public declaration 'dirName' has no documented example.
314 		private void _dirName(R)(R path) {}
315 		///
316 		unittest {}
317 	}, sac);
318 
319 	stderr.writeln("Unittest for HasPublicExampleCheck passed.");
320 }
321