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