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 		new Thread({
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 		}).start();
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 		new Thread({
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 				void doNothing(string, size_t, size_t, string, bool)
139 				{
140 				}
141 
142 				auto m = dparse.parser.parseModule(tokens.array, file, &r, &doNothing);
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 		}).start();
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 		new Thread({
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 		}).start();
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 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context,
320 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
321 		auto c = context;
322 		context = ContextType(["class" : dec.name.text], "public");
323 		dec.accept(this);
324 		context = c;
325 	}
326 
327 	override void visit(const StructDeclaration dec)
328 	{
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 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context,
345 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
346 		auto c = context;
347 		context = ContextType(["interface:" : dec.name.text], context.access);
348 		dec.accept(this);
349 		context = c;
350 	}
351 
352 	override void visit(const TemplateDeclaration dec)
353 	{
354 		auto def = makeDefinition(dec.name.text, dec.name.line, "T", context,
355 				[cast(int) dec.startLocation, cast(int) dec.endLocation]);
356 		def.attributes["signature"] = paramsToString(dec);
357 		definitions ~= def;
358 		auto c = context;
359 		context = ContextType(["template" : dec.name.text], context.access);
360 		dec.accept(this);
361 		context = c;
362 	}
363 
364 	override void visit(const FunctionDeclaration dec)
365 	{
366 		auto def = makeDefinition(dec.name.text, dec.name.line, "f", context,
367 				[cast(int) dec.functionBody.blockStatement.startLocation,
368 				cast(int) dec.functionBody.blockStatement.endLocation]);
369 		def.attributes["signature"] = paramsToString(dec);
370 		if (dec.returnType !is null)
371 			def.attributes["return"] = astToString(dec.returnType);
372 		definitions ~= def;
373 	}
374 
375 	override void visit(const Constructor dec)
376 	{
377 		auto def = makeDefinition("this", dec.line, "f", context,
378 				[cast(int) dec.functionBody.blockStatement.startLocation,
379 				cast(int) dec.functionBody.blockStatement.endLocation]);
380 		def.attributes["signature"] = paramsToString(dec);
381 		definitions ~= def;
382 	}
383 
384 	override void visit(const Destructor dec)
385 	{
386 		definitions ~= makeDefinition("~this", dec.line, "f", context,
387 				[cast(int) dec.functionBody.blockStatement.startLocation,
388 				cast(int) dec.functionBody.blockStatement.endLocation]);
389 	}
390 
391 	override void visit(const EnumDeclaration dec)
392 	{
393 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context,
394 				[cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]);
395 		auto c = context;
396 		context = ContextType(["enum" : dec.name.text], context.access);
397 		dec.accept(this);
398 		context = c;
399 	}
400 
401 	override void visit(const UnionDeclaration dec)
402 	{
403 		if (dec.name == tok!"")
404 		{
405 			dec.accept(this);
406 			return;
407 		}
408 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context,
409 				[cast(int) dec.structBody.startLocation, cast(int) dec.structBody.endLocation]);
410 		auto c = context;
411 		context = ContextType(["union" : dec.name.text], context.access);
412 		dec.accept(this);
413 		context = c;
414 	}
415 
416 	override void visit(const AnonymousEnumMember mem)
417 	{
418 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
419 				[cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]);
420 	}
421 
422 	override void visit(const EnumMember mem)
423 	{
424 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
425 				[cast(int) mem.name.index, cast(int) mem.name.index + cast(int) mem.name.text.length]);
426 	}
427 
428 	override void visit(const VariableDeclaration dec)
429 	{
430 		foreach (d; dec.declarators)
431 			definitions ~= makeDefinition(d.name.text, d.name.line, "v", context,
432 					[cast(int) d.name.index, cast(int) d.name.index + cast(int) d.name.text.length]);
433 		dec.accept(this);
434 	}
435 
436 	override void visit(const AutoDeclaration dec)
437 	{
438 		foreach (i; dec.parts.map!(a => a.identifier))
439 			definitions ~= makeDefinition(i.text, i.line, "v", context,
440 					[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
441 		dec.accept(this);
442 	}
443 
444 	override void visit(const Invariant dec)
445 	{
446 		definitions ~= makeDefinition("invariant", dec.line, "v", context,
447 				[cast(int) dec.index, cast(int) dec.blockStatement.endLocation]);
448 	}
449 
450 	override void visit(const ModuleDeclaration dec)
451 	{
452 		context = ContextType(null, "public");
453 		dec.accept(this);
454 	}
455 
456 	override void visit(const Attribute attribute)
457 	{
458 		if (attribute.attribute != tok!"")
459 		{
460 			switch (attribute.attribute.type)
461 			{
462 			case tok!"export":
463 				context.access = "public";
464 				break;
465 			case tok!"public":
466 				context.access = "public";
467 				break;
468 			case tok!"package":
469 				context.access = "protected";
470 				break;
471 			case tok!"protected":
472 				context.access = "protected";
473 				break;
474 			case tok!"private":
475 				context.access = "private";
476 				break;
477 			default:
478 			}
479 		}
480 		else if (attribute.deprecated_ !is null)
481 		{
482 			// TODO: find out how to get deprecation message
483 			context.attr["deprecation"] = "";
484 		}
485 
486 		attribute.accept(this);
487 	}
488 
489 	override void visit(const AttributeDeclaration dec)
490 	{
491 		accessSt = AccessState.Keep;
492 		dec.accept(this);
493 	}
494 
495 	override void visit(const Declaration dec)
496 	{
497 		auto c = context;
498 		dec.accept(this);
499 
500 		final switch (accessSt) with (AccessState)
501 		{
502 		case Reset:
503 			context = c;
504 			break;
505 		case Keep:
506 			break;
507 		}
508 		accessSt = AccessState.Reset;
509 	}
510 
511 	override void visit(const Unittest dec)
512 	{
513 		// skipping symbols inside a unit test to not clutter the ctags file
514 		// with "temporary" symbols.
515 		// TODO when phobos have a unittest library investigate how that could
516 		// be used to describe the tests.
517 		// Maybe with UDA's to give the unittest a "name".
518 	}
519 
520 	override void visit(const AliasDeclaration dec)
521 	{
522 		// Old style alias
523 		if (dec.declaratorIdentifierList)
524 			foreach (i; dec.declaratorIdentifierList.identifiers)
525 				definitions ~= makeDefinition(i.text, i.line, "a", context,
526 						[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
527 		dec.accept(this);
528 	}
529 
530 	override void visit(const AliasInitializer dec)
531 	{
532 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context,
533 				[cast(int) dec.name.index, cast(int) dec.name.index + cast(int) dec.name.text.length]);
534 
535 		dec.accept(this);
536 	}
537 
538 	override void visit(const AliasThisDeclaration dec)
539 	{
540 		auto name = dec.identifier;
541 		definitions ~= makeDefinition(name.text, name.line, "a", context,
542 				[cast(int) name.index, cast(int) name.index + cast(int) name.text.length]);
543 
544 		dec.accept(this);
545 	}
546 
547 	alias visit = ASTVisitor.visit;
548 
549 	ContextType context;
550 	AccessState accessSt;
551 	DefinitionElement[] definitions;
552 }
553 
554 DefinitionElement makeDefinition(string name, size_t line, string type,
555 		ContextType context, int[2] range)
556 {
557 	string[string] attr = context.attr;
558 	if (context.access.length)
559 		attr["access"] = context.access;
560 	return DefinitionElement(name, cast(int) line, type, attr, range);
561 }
562 
563 enum AccessState
564 {
565 	Reset, /// when ascending the AST reset back to the previous access.
566 	Keep /// when ascending the AST keep the new access.
567 }
568 
569 struct ContextType
570 {
571 	string[string] attr;
572 	string access;
573 }