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