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