1 module workspaced.com.dscanner;
2 
3 import std.algorithm;
4 import std.array;
5 import std.conv;
6 import std.file;
7 import std.json;
8 import std.stdio;
9 import std.typecons;
10 
11 import core.sync.mutex;
12 import core.thread;
13 
14 import dscanner.analysis.base;
15 import dscanner.analysis.config;
16 import dscanner.analysis.run;
17 import dscanner.symbol_finder;
18 
19 import inifiled : INI, readINIFile;
20 
21 import dparse.ast;
22 import dparse.lexer;
23 import dparse.parser;
24 import dparse.rollback_allocator;
25 import dsymbol.builtin.names;
26 import dsymbol.modulecache : ASTAllocator, ModuleCache;
27 
28 import painlessjson;
29 
30 import workspaced.api;
31 import workspaced.dparseext;
32 
33 @component("dscanner")
34 class DscannerComponent : ComponentWrapper
35 {
36 	mixin DefaultComponentWrapper;
37 
38 	/// Asynchronously lints the file passed.
39 	/// If you provide code then the code will be used and file will be ignored.
40 	Future!(DScannerIssue[]) lint(string file = "", string ini = "dscanner.ini", scope const(char)[] code = "")
41 	{
42 		auto ret = new Future!(DScannerIssue[]);
43 		gthreads.create({
44 			mixin(traceTask);
45 			try
46 			{
47 				if (code.length && !file.length)
48 					file = "stdin";
49 				auto config = defaultStaticAnalysisConfig();
50 				if (getConfigPath("dscanner.ini", ini))
51 					stderr.writeln("Overriding Dscanner ini with workspace-d dscanner.ini config file");
52 				if (ini.exists)
53 					readINIFile(config, ini);
54 				if (!code.length)
55 					code = readText(file);
56 				DScannerIssue[] issues;
57 				if (!code.length)
58 				{
59 					ret.finish(issues);
60 					return;
61 				}
62 				RollbackAllocator r;
63 				const(Token)[] tokens;
64 				StringCache cache = StringCache(StringCache.defaultBucketCount);
65 				const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, issues);
66 				if (!m)
67 					throw new Exception(text("parseModule returned null?! - file: '",
68 						file, "', code: '", code, "'"));
69 				MessageSet results;
70 				auto alloc = scoped!ASTAllocator();
71 				auto moduleCache = ModuleCache(alloc);
72 				results = analyze(file, m, config, moduleCache, tokens, true);
73 				if (results is null)
74 				{
75 					ret.finish(issues);
76 					return;
77 				}
78 				foreach (msg; results)
79 				{
80 					DScannerIssue issue;
81 					issue.file = msg.fileName;
82 					issue.line = cast(int) msg.line;
83 					issue.column = cast(int) msg.column;
84 					issue.type = typeForWarning(msg.key);
85 					issue.description = msg.message;
86 					issue.key = msg.key;
87 					issues ~= issue;
88 				}
89 				ret.finish(issues);
90 			}
91 			catch (Throwable e)
92 			{
93 				ret.error(e);
94 			}
95 		});
96 		return ret;
97 	}
98 
99 	private const(Module) parseModule(string file, ubyte[] code, RollbackAllocator* p,
100 			ref StringCache cache, ref const(Token)[] tokens, ref DScannerIssue[] issues)
101 	{
102 		LexerConfig config;
103 		config.fileName = file;
104 		config.stringBehavior = StringBehavior.source;
105 		tokens = getTokensForParser(code, config, &cache);
106 
107 		void addIssue(string fileName, size_t line, size_t column, string message, bool isError)
108 		{
109 			issues ~= DScannerIssue(file, cast(int) line, cast(int) column, isError
110 					? "error" : "warn", message);
111 		}
112 
113 		uint err, warn;
114 		return dparse.parser.parseModule(tokens, file, p, &addIssue, &err, &warn);
115 	}
116 
117 	/// Asynchronously lists all definitions in the specified file.
118 	/// If you provide code the file wont be manually read.
119 	Future!(DefinitionElement[]) listDefinitions(string file, scope const(char)[] code = "")
120 	{
121 		auto ret = new Future!(DefinitionElement[]);
122 		gthreads.create({
123 			mixin(traceTask);
124 			try
125 			{
126 				if (code.length && !file.length)
127 					file = "stdin";
128 				if (!code.length)
129 					code = readText(file);
130 				if (!code.length)
131 				{
132 					DefinitionElement[] arr;
133 					ret.finish(arr);
134 					return;
135 				}
136 
137 				RollbackAllocator r;
138 				LexerConfig config;
139 				auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
140 
141 				auto m = dparse.parser.parseModule(tokens.array, file, &r);
142 
143 				auto defFinder = new DefinitionFinder();
144 				defFinder.visit(m);
145 
146 				ret.finish(defFinder.definitions);
147 			}
148 			catch (Throwable e)
149 			{
150 				ret.error(e);
151 			}
152 		});
153 		return ret;
154 	}
155 
156 	/// Asynchronously finds all definitions of a symbol in the import paths.
157 	Future!(FileLocation[]) findSymbol(string symbol)
158 	{
159 		auto ret = new Future!(FileLocation[]);
160 		gthreads.create({
161 			mixin(traceTask);
162 			try
163 			{
164 				import dscanner.utils : expandArgs;
165 
166 				string[] paths = expandArgs([""] ~ importPaths);
167 				foreach_reverse (i, path; paths)
168 					if (path == "stdin")
169 						paths = paths.remove(i);
170 				FileLocation[] files;
171 				findDeclarationOf((fileName, line, column) {
172 					FileLocation file;
173 					file.file = fileName;
174 					file.line = cast(int) line;
175 					file.column = cast(int) column;
176 					files ~= file;
177 				}, symbol, paths);
178 				ret.finish(files);
179 			}
180 			catch (Throwable e)
181 			{
182 				ret.error(e);
183 			}
184 		});
185 		return ret;
186 	}
187 
188 	/// Returns: all keys & documentation that can be used in a dscanner.ini
189 	INIEntry[] listAllIniFields()
190 	{
191 		import std.traits : getUDAs;
192 
193 		INIEntry[] ret;
194 		foreach (mem; __traits(allMembers, StaticAnalysisConfig))
195 			static if (is(typeof(__traits(getMember, StaticAnalysisConfig, mem)) == string))
196 			{
197 				alias docs = getUDAs!(__traits(getMember, StaticAnalysisConfig, mem), INI);
198 				ret ~= INIEntry(mem, docs.length ? docs[0].msg : "");
199 			}
200 		return ret;
201 	}
202 }
203 
204 /// dscanner.ini setting type
205 struct INIEntry
206 {
207 	///
208 	string name, documentation;
209 }
210 
211 /// Issue type returned by lint
212 struct DScannerIssue
213 {
214 	///
215 	string file;
216 	///
217 	int line, column;
218 	///
219 	string type;
220 	///
221 	string description;
222 	///
223 	string key;
224 }
225 
226 /// Returned by find-symbol
227 struct FileLocation
228 {
229 	///
230 	string file;
231 	///
232 	int line, column;
233 }
234 
235 /// Returned by list-definitions
236 struct DefinitionElement
237 {
238 	///
239 	string name;
240 	///
241 	int line;
242 	/// One of "c" (class), "s" (struct), "i" (interface), "T" (template), "f" (function/ctor/dtor), "g" (enum {}), "u" (union), "e" (enum member/definition), "v" (variable/invariant)
243 	string type;
244 	///
245 	string[string] attributes;
246 	///
247 	int[2] range;
248 }
249 
250 private:
251 
252 string typeForWarning(string key)
253 {
254 	switch (key)
255 	{
256 	case "dscanner.bugs.backwards_slices":
257 	case "dscanner.bugs.if_else_same":
258 	case "dscanner.bugs.logic_operator_operands":
259 	case "dscanner.bugs.self_assignment":
260 	case "dscanner.confusing.argument_parameter_mismatch":
261 	case "dscanner.confusing.brexp":
262 	case "dscanner.confusing.builtin_property_names":
263 	case "dscanner.confusing.constructor_args":
264 	case "dscanner.confusing.function_attributes":
265 	case "dscanner.confusing.lambda_returns_lambda":
266 	case "dscanner.confusing.logical_precedence":
267 	case "dscanner.confusing.struct_constructor_default_args":
268 	case "dscanner.deprecated.delete_keyword":
269 	case "dscanner.deprecated.floating_point_operators":
270 	case "dscanner.if_statement":
271 	case "dscanner.performance.enum_array_literal":
272 	case "dscanner.style.allman":
273 	case "dscanner.style.alias_syntax":
274 	case "dscanner.style.doc_missing_params":
275 	case "dscanner.style.doc_missing_returns":
276 	case "dscanner.style.doc_non_existing_params":
277 	case "dscanner.style.explicitly_annotated_unittest":
278 	case "dscanner.style.has_public_example":
279 	case "dscanner.style.imports_sortedness":
280 	case "dscanner.style.long_line":
281 	case "dscanner.style.number_literals":
282 	case "dscanner.style.phobos_naming_convention":
283 	case "dscanner.style.undocumented_declaration":
284 	case "dscanner.suspicious.auto_ref_assignment":
285 	case "dscanner.suspicious.catch_em_all":
286 	case "dscanner.suspicious.comma_expression":
287 	case "dscanner.suspicious.incomplete_operator_overloading":
288 	case "dscanner.suspicious.incorrect_infinite_range":
289 	case "dscanner.suspicious.label_var_same_name":
290 	case "dscanner.suspicious.length_subtraction":
291 	case "dscanner.suspicious.local_imports":
292 	case "dscanner.suspicious.missing_return":
293 	case "dscanner.suspicious.object_const":
294 	case "dscanner.suspicious.redundant_attributes":
295 	case "dscanner.suspicious.redundant_parens":
296 	case "dscanner.suspicious.static_if_else":
297 	case "dscanner.suspicious.unmodified":
298 	case "dscanner.suspicious.unused_label":
299 	case "dscanner.suspicious.unused_parameter":
300 	case "dscanner.suspicious.unused_variable":
301 	case "dscanner.suspicious.useless_assert":
302 	case "dscanner.unnecessary.duplicate_attribute":
303 	case "dscanner.useless.final":
304 	case "dscanner.useless-initializer":
305 	case "dscanner.vcall_ctor":
306 		return "warn";
307 	case "dscanner.syntax":
308 		return "error";
309 	default:
310 		stderr.writeln("Warning: unimplemented DScanner reason, assuming warning: ", key);
311 		return "warn";
312 	}
313 }
314 
315 final class DefinitionFinder : ASTVisitor
316 {
317 	override void visit(const ClassDeclaration dec)
318 	{
319 		if (!dec.structBody)
320 			return;
321 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context,
322 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
323 		auto c = context;
324 		context = ContextType(["class" : dec.name.text], "public");
325 		dec.accept(this);
326 		context = c;
327 	}
328 
329 	override void visit(const StructDeclaration dec)
330 	{
331 		if (!dec.structBody)
332 			return;
333 		if (dec.name == tok!"")
334 		{
335 			dec.accept(this);
336 			return;
337 		}
338 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "s", context,
339 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
340 		auto c = context;
341 		context = ContextType(["struct" : dec.name.text], "public");
342 		dec.accept(this);
343 		context = c;
344 	}
345 
346 	override void visit(const InterfaceDeclaration dec)
347 	{
348 		if (!dec.structBody)
349 			return;
350 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context,
351 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
352 		auto c = context;
353 		context = ContextType(["interface:" : dec.name.text], context.access);
354 		dec.accept(this);
355 		context = c;
356 	}
357 
358 	override void visit(const TemplateDeclaration dec)
359 	{
360 		auto def = makeDefinition(dec.name.text, dec.name.line, "T", context,
361 				[cast(int) dec.startLocation, cast(int) dec.endLocation]);
362 		def.attributes["signature"] = paramsToString(dec);
363 		definitions ~= def;
364 		auto c = context;
365 		context = ContextType(["template" : dec.name.text], context.access);
366 		dec.accept(this);
367 		context = c;
368 	}
369 
370 	override void visit(const FunctionDeclaration dec)
371 	{
372 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement)
373 			return;
374 		auto def = makeDefinition(dec.name.text, dec.name.line, "f", context,
375 				[cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
376 				cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]);
377 		def.attributes["signature"] = paramsToString(dec);
378 		if (dec.returnType !is null)
379 			def.attributes["return"] = astToString(dec.returnType);
380 		definitions ~= def;
381 	}
382 
383 	override void visit(const Constructor dec)
384 	{
385 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement)
386 			return;
387 		auto def = makeDefinition("this", dec.line, "f", context,
388 				[cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
389 				cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]);
390 		def.attributes["signature"] = paramsToString(dec);
391 		definitions ~= def;
392 	}
393 
394 	override void visit(const Destructor dec)
395 	{
396 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody || !dec.functionBody.specifiedFunctionBody.blockStatement)
397 			return;
398 		definitions ~= makeDefinition("~this", dec.line, "f", context,
399 				[cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
400 				cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation]);
401 	}
402 
403 	override void visit(const EnumDeclaration dec)
404 	{
405 		if (!dec.enumBody)
406 			return;
407 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context,
408 				[cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]);
409 		auto c = context;
410 		context = ContextType(["enum" : dec.name.text], context.access);
411 		dec.accept(this);
412 		context = c;
413 	}
414 
415 	override void visit(const UnionDeclaration dec)
416 	{
417 		if (!dec.structBody)
418 			return;
419 		if (dec.name == tok!"")
420 		{
421 			dec.accept(this);
422 			return;
423 		}
424 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context,
425 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
426 		auto c = context;
427 		context = ContextType(["union" : dec.name.text], context.access);
428 		dec.accept(this);
429 		context = c;
430 	}
431 
432 	override void visit(const AnonymousEnumMember mem)
433 	{
434 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
435 				[cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]);
436 	}
437 
438 	override void visit(const EnumMember mem)
439 	{
440 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
441 				[cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]);
442 	}
443 
444 	override void visit(const VariableDeclaration dec)
445 	{
446 		foreach (d; dec.declarators)
447 			definitions ~= makeDefinition(d.name.text, d.name.line, "v", context,
448 					[cast(int) d.name.index, cast(int) d.name.index + cast(int) d.name.text.length]);
449 		dec.accept(this);
450 	}
451 
452 	override void visit(const AutoDeclaration dec)
453 	{
454 		foreach (i; dec.parts.map!(a => a.identifier))
455 			definitions ~= makeDefinition(i.text, i.line, "v", context,
456 					[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
457 		dec.accept(this);
458 	}
459 
460 	override void visit(const Invariant dec)
461 	{
462 		if (!dec.blockStatement)
463 			return;
464 		definitions ~= makeDefinition("invariant", dec.line, "v", context,
465 				[cast(int) dec.index, cast(int) dec.blockStatement.endLocation]);
466 	}
467 
468 	override void visit(const ModuleDeclaration dec)
469 	{
470 		context = ContextType(null, "public");
471 		dec.accept(this);
472 	}
473 
474 	override void visit(const Attribute attribute)
475 	{
476 		if (attribute.attribute != tok!"")
477 		{
478 			switch (attribute.attribute.type)
479 			{
480 			case tok!"export":
481 				context.access = "public";
482 				break;
483 			case tok!"public":
484 				context.access = "public";
485 				break;
486 			case tok!"package":
487 				context.access = "protected";
488 				break;
489 			case tok!"protected":
490 				context.access = "protected";
491 				break;
492 			case tok!"private":
493 				context.access = "private";
494 				break;
495 			default:
496 			}
497 		}
498 		else if (attribute.deprecated_ !is null)
499 		{
500 			// TODO: find out how to get deprecation message
501 			context.attr["deprecation"] = "";
502 		}
503 
504 		attribute.accept(this);
505 	}
506 
507 	override void visit(const AttributeDeclaration dec)
508 	{
509 		accessSt = AccessState.Keep;
510 		dec.accept(this);
511 	}
512 
513 	override void visit(const Declaration dec)
514 	{
515 		auto c = context;
516 		dec.accept(this);
517 
518 		final switch (accessSt) with (AccessState)
519 		{
520 		case Reset:
521 			context = c;
522 			break;
523 		case Keep:
524 			break;
525 		}
526 		accessSt = AccessState.Reset;
527 	}
528 
529 	override void visit(const Unittest dec)
530 	{
531 		// skipping symbols inside a unit test to not clutter the ctags file
532 		// with "temporary" symbols.
533 		// TODO when phobos have a unittest library investigate how that could
534 		// be used to describe the tests.
535 		// Maybe with UDA's to give the unittest a "name".
536 	}
537 
538 	override void visit(const AliasDeclaration dec)
539 	{
540 		// Old style alias
541 		if (dec.declaratorIdentifierList)
542 			foreach (i; dec.declaratorIdentifierList.identifiers)
543 				definitions ~= makeDefinition(i.text, i.line, "a", context,
544 						[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
545 		dec.accept(this);
546 	}
547 
548 	override void visit(const AliasInitializer dec)
549 	{
550 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context,
551 				[cast(int) dec.name.index, cast(int) dec.name.index + cast(int) dec.name.text.length]);
552 
553 		dec.accept(this);
554 	}
555 
556 	override void visit(const AliasThisDeclaration dec)
557 	{
558 		auto name = dec.identifier;
559 		definitions ~= makeDefinition(name.text, name.line, "a", context,
560 				[cast(int) name.index, cast(int) name.index + cast(int) name.text.length]);
561 
562 		dec.accept(this);
563 	}
564 
565 	alias visit = ASTVisitor.visit;
566 
567 	ContextType context;
568 	AccessState accessSt;
569 	DefinitionElement[] definitions;
570 }
571 
572 DefinitionElement makeDefinition(string name, size_t line, string type,
573 		ContextType context, int[2] range)
574 {
575 	string[string] attr = context.attr;
576 	if (context.access.length)
577 		attr["access"] = context.access;
578 	return DefinitionElement(name, cast(int) line, type, attr, range);
579 }
580 
581 enum AccessState
582 {
583 	Reset, /// when ascending the AST reset back to the previous access.
584 	Keep /// when ascending the AST keep the new access.
585 }
586 
587 struct ContextType
588 {
589 	string[string] attr;
590 	string access;
591 }