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