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