1 //          Copyright Brian Schott (Hackerpilot) 2014.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 module dscanner.analysis.undocumented;
7 
8 import dscanner.analysis.base;
9 import dsymbol.scope_ : Scope;
10 import dparse.ast;
11 import dparse.lexer;
12 
13 import std.regex : ctRegex, matchAll;
14 import std.stdio;
15 
16 /**
17  * Checks for undocumented public declarations. Ignores some operator overloads,
18  * main functions, and functions whose name starts with "get" or "set".
19  */
20 final class UndocumentedDeclarationCheck : BaseAnalyzer
21 {
22 	alias visit = BaseAnalyzer.visit;
23 
24 	mixin AnalyzerInfo!"undocumented_declaration_check";
25 
26 	this(string fileName, const(Scope)* sc, bool skipTests = false)
27 	{
28 		super(fileName, sc, skipTests);
29 	}
30 
31 	override void visit(const Module mod)
32 	{
33 		push(tok!"public");
34 		mod.accept(this);
35 	}
36 
37 	override void visit(const Declaration dec)
38 	{
39 		if (dec.attributeDeclaration)
40 		{
41 			auto attr = dec.attributeDeclaration.attribute;
42 			if (isProtection(attr.attribute.type))
43 				set(attr.attribute.type);
44 			else if (attr.attribute == tok!"override")
45 				setOverride(true);
46 			else if (attr.deprecated_ !is null)
47 				setDeprecated(true);
48 			else if (attr.atAttribute !is null && attr.atAttribute.identifier.text == "disable")
49 				setDisabled(true);
50 		}
51 
52 		immutable bool shouldPop = dec.attributeDeclaration is null;
53 		immutable bool prevOverride = getOverride();
54 		immutable bool prevDisabled = getDisabled();
55 		immutable bool prevDeprecated = getDeprecated();
56 		bool dis;
57 		bool dep;
58 		bool ovr;
59 		bool pushed;
60 		foreach (attribute; dec.attributes)
61 		{
62 			if (isProtection(attribute.attribute.type))
63 			{
64 				if (shouldPop)
65 				{
66 					pushed = true;
67 					push(attribute.attribute.type);
68 				}
69 				else
70 					set(attribute.attribute.type);
71 			}
72 			else if (attribute.attribute == tok!"override")
73 				ovr = true;
74 			else if (attribute.deprecated_ !is null)
75 				dep = true;
76 			else if (attribute.atAttribute !is null
77 					&& attribute.atAttribute.identifier.text == "disable")
78 				dis = true;
79 		}
80 		if (ovr)
81 			setOverride(true);
82 		if (dis)
83 			setDisabled(true);
84 		if (dep)
85 			setDeprecated(true);
86 		dec.accept(this);
87 		if (shouldPop && pushed)
88 			pop();
89 		if (ovr)
90 			setOverride(prevOverride);
91 		if (dis)
92 			setDisabled(prevDisabled);
93 		if (dep)
94 			setDeprecated(prevDeprecated);
95 	}
96 
97 	override void visit(const VariableDeclaration variable)
98 	{
99 		if (!currentIsInteresting() || variable.comment.ptr !is null)
100 			return;
101 		if (variable.autoDeclaration !is null)
102 		{
103 			addMessage(variable.autoDeclaration.parts[0].identifier.line,
104 					variable.autoDeclaration.parts[0].identifier.column,
105 					variable.autoDeclaration.parts[0].identifier.text);
106 			return;
107 		}
108 		foreach (dec; variable.declarators)
109 		{
110 			if (dec.comment.ptr is null)
111 				addMessage(dec.name.line, dec.name.column, dec.name.text);
112 			return;
113 		}
114 	}
115 
116 	override void visit(const ConditionalDeclaration cond)
117 	{
118 		const VersionCondition ver = cond.compileCondition.versionCondition;
119 		if (ver is null || (ver.token != tok!"unittest" && ver.token.text != "none"))
120 			cond.accept(this);
121 		else if (cond.falseDeclarations.length > 0)
122 			foreach (f; cond.falseDeclarations)
123 				visit(f);
124 	}
125 
126 	override void visit(const FunctionBody fb)
127 	{
128 	}
129 
130 	override void visit(const Unittest u)
131 	{
132 	}
133 
134 	override void visit(const TraitsExpression t)
135 	{
136 	}
137 
138 	mixin V!AnonymousEnumMember;
139 	mixin V!ClassDeclaration;
140 	mixin V!EnumDeclaration;
141 	mixin V!InterfaceDeclaration;
142 	mixin V!StructDeclaration;
143 	mixin V!UnionDeclaration;
144 	mixin V!TemplateDeclaration;
145 	mixin V!FunctionDeclaration;
146 	mixin V!Constructor;
147 
148 private:
149 
150 	mixin template V(T)
151 	{
152 		override void visit(const T declaration)
153 		{
154 			import std.traits : hasMember;
155 			static if (hasMember!(T, "storageClasses"))
156 			{
157 				// stop at declarations with a deprecated in their storage classes
158 				foreach (sc; declaration.storageClasses)
159 					if (sc.deprecated_ !is null)
160 						return;
161 			}
162 
163 			if (currentIsInteresting())
164 			{
165 				if (declaration.comment.ptr is null)
166 				{
167 					static if (hasMember!(T, "name"))
168 					{
169 						static if (is(T == FunctionDeclaration))
170 						{
171 							import std.algorithm : canFind;
172 
173 							if (!(ignoredFunctionNames.canFind(declaration.name.text)
174 									|| isGetterOrSetter(declaration.name.text)
175 									|| isProperty(declaration)))
176 							{
177 								addMessage(declaration.name.line,
178 										declaration.name.column, declaration.name.text);
179 							}
180 						}
181 						else
182 						{
183 							if (declaration.name.type != tok!"")
184 								addMessage(declaration.name.line,
185 										declaration.name.column, declaration.name.text);
186 						}
187 					}
188 					else
189 					{
190 						addMessage(declaration.line, declaration.column, null);
191 					}
192 				}
193 				static if (!(is(T == TemplateDeclaration) || is(T == FunctionDeclaration)))
194 				{
195 					declaration.accept(this);
196 				}
197 			}
198 		}
199 	}
200 
201 	static bool isGetterOrSetter(string name)
202 	{
203 		return !matchAll(name, getSetRe).empty;
204 	}
205 
206 	static bool isProperty(const FunctionDeclaration dec)
207 	{
208 		if (dec.memberFunctionAttributes is null)
209 			return false;
210 		foreach (attr; dec.memberFunctionAttributes)
211 		{
212 			if (attr.atAttribute && attr.atAttribute.identifier.text == "property")
213 				return true;
214 		}
215 		return false;
216 	}
217 
218 	void addMessage(size_t line, size_t column, string name)
219 	{
220 		import std..string : format;
221 
222 		addErrorMessage(line, column, "dscanner.style.undocumented_declaration", name is null
223 				? "Public declaration is undocumented."
224 				: format("Public declaration '%s' is undocumented.", name));
225 	}
226 
227 	bool getOverride()
228 	{
229 		return stack[$ - 1].isOverride;
230 	}
231 
232 	void setOverride(bool o = true)
233 	{
234 		stack[$ - 1].isOverride = o;
235 	}
236 
237 	bool getDisabled()
238 	{
239 		return stack[$ - 1].isDisabled;
240 	}
241 
242 	void setDisabled(bool d = true)
243 	{
244 		stack[$ - 1].isDisabled = d;
245 	}
246 
247 	bool getDeprecated()
248 	{
249 		return stack[$ - 1].isDeprecated;
250 	}
251 
252 	void setDeprecated(bool d = true)
253 	{
254 		stack[$ - 1].isDeprecated = d;
255 	}
256 
257 	bool currentIsInteresting()
258 	{
259 		return stack[$ - 1].protection == tok!"public"
260 			&& !getOverride() && !getDisabled() && !getDeprecated();
261 	}
262 
263 	void set(IdType p)
264 	in
265 	{
266 		assert(isProtection(p));
267 	}
268 	body
269 	{
270 		stack[$ - 1].protection = p;
271 	}
272 
273 	void push(IdType p)
274 	in
275 	{
276 		assert(isProtection(p));
277 	}
278 	body
279 	{
280 		stack ~= ProtectionInfo(p, false);
281 	}
282 
283 	void pop()
284 	{
285 		assert(stack.length > 1);
286 		stack = stack[0 .. $ - 1];
287 	}
288 
289 	static struct ProtectionInfo
290 	{
291 		IdType protection;
292 		bool isOverride;
293 		bool isDeprecated;
294 		bool isDisabled;
295 	}
296 
297 	ProtectionInfo[] stack;
298 }
299 
300 // Ignore undocumented symbols with these names
301 private immutable string[] ignoredFunctionNames = [
302 	"opCmp", "opEquals", "toString", "toHash", "main"
303 ];
304 
305 private enum getSetRe = ctRegex!`^(?:get|set)(?:\p{Lu}|_).*`;
306 
307 unittest
308 {
309 	import std.stdio : stderr;
310 	import std.format : format;
311 	import dscanner.analysis.config : StaticAnalysisConfig, Check, disabledConfig;
312 	import dscanner.analysis.helpers : assertAnalyzerWarnings;
313 
314 	StaticAnalysisConfig sac = disabledConfig();
315 	sac.undocumented_declaration_check = Check.enabled;
316 
317 	assertAnalyzerWarnings(q{
318 		class C{} // [warn]: Public declaration 'C' is undocumented.
319 		interface I{} // [warn]: Public declaration 'I' is undocumented.
320 		enum e = 0; // [warn]: Public declaration 'e' is undocumented.
321 		void f(){} // [warn]: Public declaration 'f' is undocumented.
322 		struct S{} // [warn]: Public declaration 'S' is undocumented.
323 		template T(){} // [warn]: Public declaration 'T' is undocumented.
324 		union U{} // [warn]: Public declaration 'U' is undocumented.
325 	}, sac);
326 
327 	assertAnalyzerWarnings(q{
328 		/// C
329 		class C{}
330 		/// I
331 		interface I{}
332 		/// e
333 		enum e = 0;
334 		/// f
335 		void f(){}
336 		/// S
337 		struct S{}
338 		/// T
339 		template T(){}
340 		/// U
341 		union U{}
342 	}, sac);
343 
344 	// https://github.com/dlang-community/D-Scanner/issues/760
345 	assertAnalyzerWarnings(q{
346 		deprecated auto func(){}
347 		deprecated auto func()(){}
348 	}, sac);
349 
350 	stderr.writeln("Unittest for UndocumentedDeclarationCheck passed.");
351 }
352