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 analysis.undocumented;
7 
8 import 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 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 = false;
55 		bool dep = false;
56 		bool ovr = false;
57 		bool pushed = false;
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 
154 			if (currentIsInteresting())
155 			{
156 				if (declaration.comment.ptr is null)
157 				{
158 					static if (hasMember!(T, "name"))
159 					{
160 						static if (is(T == FunctionDeclaration))
161 						{
162 							import std.algorithm : canFind;
163 
164 							if (!(ignoredFunctionNames.canFind(declaration.name.text)
165 									|| isGetterOrSetter(declaration.name.text)
166 									|| isProperty(declaration)))
167 							{
168 								addMessage(declaration.name.line,
169 										declaration.name.column, declaration.name.text);
170 							}
171 						}
172 						else
173 						{
174 							if (declaration.name.type != tok!"")
175 								addMessage(declaration.name.line,
176 										declaration.name.column, declaration.name.text);
177 						}
178 					}
179 					else
180 					{
181 						addMessage(declaration.line, declaration.column, null);
182 					}
183 				}
184 				static if (!(is(T == TemplateDeclaration) || is(T == FunctionDeclaration)))
185 				{
186 					declaration.accept(this);
187 				}
188 			}
189 		}
190 	}
191 
192 	static bool isGetterOrSetter(string name)
193 	{
194 		return !matchAll(name, getSetRe).empty;
195 	}
196 
197 	static bool isProperty(const FunctionDeclaration dec)
198 	{
199 		if (dec.memberFunctionAttributes is null)
200 			return false;
201 		foreach (attr; dec.memberFunctionAttributes)
202 		{
203 			if (attr.atAttribute && attr.atAttribute.identifier.text == "property")
204 				return true;
205 		}
206 		return false;
207 	}
208 
209 	void addMessage(size_t line, size_t column, string name)
210 	{
211 		import std..string : format;
212 
213 		addErrorMessage(line, column, "dscanner.style.undocumented_declaration", name is null
214 				? "Public declaration is undocumented."
215 				: format("Public declaration '%s' is undocumented.", name));
216 	}
217 
218 	bool getOverride()
219 	{
220 		return stack[$ - 1].isOverride;
221 	}
222 
223 	void setOverride(bool o = true)
224 	{
225 		stack[$ - 1].isOverride = o;
226 	}
227 
228 	bool getDisabled()
229 	{
230 		return stack[$ - 1].isDisabled;
231 	}
232 
233 	void setDisabled(bool d = true)
234 	{
235 		stack[$ - 1].isDisabled = d;
236 	}
237 
238 	bool getDeprecated()
239 	{
240 		return stack[$ - 1].isDeprecated;
241 	}
242 
243 	void setDeprecated(bool d = true)
244 	{
245 		stack[$ - 1].isDeprecated = d;
246 	}
247 
248 	bool currentIsInteresting()
249 	{
250 		return stack[$ - 1].protection == tok!"public"
251 			&& !stack[$ - 1].isOverride && !stack[$ - 1].isDisabled && !stack[$ - 1].isDeprecated;
252 	}
253 
254 	void set(IdType p)
255 	in
256 	{
257 		assert(isProtection(p));
258 	}
259 	body
260 	{
261 		stack[$ - 1].protection = p;
262 	}
263 
264 	void push(IdType p)
265 	in
266 	{
267 		assert(isProtection(p));
268 	}
269 	body
270 	{
271 		stack ~= ProtectionInfo(p, false);
272 	}
273 
274 	void pop()
275 	{
276 		assert(stack.length > 1);
277 		stack = stack[0 .. $ - 1];
278 	}
279 
280 	static struct ProtectionInfo
281 	{
282 		IdType protection;
283 		bool isOverride;
284 		bool isDeprecated;
285 		bool isDisabled;
286 	}
287 
288 	ProtectionInfo[] stack;
289 }
290 
291 // Ignore undocumented symbols with these names
292 private immutable string[] ignoredFunctionNames = [
293 	"opCmp", "opEquals", "toString", "toHash", "main"
294 ];
295 
296 private enum getSetRe = ctRegex!`^(?:get|set)(?:\p{Lu}|_).*`;