1 module workspaced.com.dscanner;
2 
3 version (unittest)
4 debug = ResolveRange;
5 
6 import std.algorithm;
7 import std.array;
8 import std.conv;
9 import std.experimental.logger;
10 import std.file;
11 import std.json;
12 import std.stdio;
13 import std.typecons;
14 import std.meta : AliasSeq;
15 
16 import core.sync.mutex;
17 import core.thread;
18 
19 import dscanner.analysis.base;
20 import dscanner.analysis.config;
21 import dscanner.analysis.run;
22 import dscanner.symbol_finder;
23 
24 import inifiled : INI, readINIFile;
25 
26 import dparse.ast;
27 import dparse.lexer;
28 import dparse.parser;
29 import dparse.rollback_allocator;
30 import dsymbol.builtin.names;
31 import dsymbol.modulecache : ASTAllocator, ModuleCache;
32 
33 import painlessjson;
34 
35 import workspaced.api;
36 import workspaced.dparseext;
37 import workspaced.helpers;
38 
39 static immutable LocalImportCheckKEY = "dscanner.suspicious.local_imports";
40 static immutable LongLineCheckKEY = "dscanner.style.long_line";
41 
42 @component("dscanner")
43 class DscannerComponent : ComponentWrapper
44 {
45 	mixin DefaultComponentWrapper;
46 
47 	/// Asynchronously lints the file passed.
48 	/// If you provide code then the code will be used and file will be ignored.
49 	/// See_Also: $(LREF getConfig)
50 	Future!(DScannerIssue[]) lint(string file = "", string ini = "dscanner.ini",
51 			scope const(char)[] code = "", bool skipWorkspacedPaths = false,
52 			const StaticAnalysisConfig defaultConfig = StaticAnalysisConfig.init,
53 			bool resolveRanges = false)
54 	{
55 		auto ret = new typeof(return);
56 		gthreads.create({
57 			mixin(traceTask);
58 			try
59 			{
60 				if (code.length && !file.length)
61 					file = "stdin";
62 				auto config = getConfig(ini, skipWorkspacedPaths, defaultConfig);
63 				if (!code.length)
64 					code = readText(file);
65 				DScannerIssue[] issues;
66 				if (!code.length)
67 				{
68 					ret.finish(issues);
69 					return;
70 				}
71 				RollbackAllocator r;
72 				const(Token)[] tokens;
73 				StringCache cache = StringCache(StringCache.defaultBucketCount);
74 				const Module m = parseModule(file, cast(ubyte[]) code, &r, cache, tokens, issues);
75 				if (!m)
76 					throw new Exception(text("parseModule returned null?! - file: '",
77 						file, "', code: '", code, "'"));
78 
79 				// resolve syntax errors (immediately set by parseModule)
80 				if (resolveRanges)
81 				{
82 					foreach_reverse (i, ref issue; issues)
83 					{
84 						if (!resolveRange(tokens, issue))
85 							issues = issues.remove(i);
86 					}
87 				}
88 
89 				MessageSet results;
90 				auto alloc = scoped!ASTAllocator();
91 				auto moduleCache = ModuleCache(alloc);
92 				results = analyze(file, m, config, moduleCache, tokens, true);
93 				if (results is null)
94 				{
95 					ret.finish(issues);
96 					return;
97 				}
98 				foreach (msg; results)
99 				{
100 					DScannerIssue issue;
101 					issue.file = msg.fileName;
102 					issue.line = cast(int) msg.line;
103 					issue.column = cast(int) msg.column;
104 					issue.type = typeForWarning(msg.key);
105 					issue.description = msg.message;
106 					issue.key = msg.key;
107 					if (resolveRanges)
108 					{
109 						if (!this.resolveRange(tokens, issue))
110 							continue;
111 					}
112 					issues ~= issue;
113 				}
114 				ret.finish(issues);
115 			}
116 			catch (Throwable e)
117 			{
118 				ret.error(e);
119 			}
120 		});
121 		return ret;
122 	}
123 
124 	/// Takes line & column from the D-Scanner issue array and resolves the
125 	/// start & end locations for the issues by changing the values in-place.
126 	/// In the JSON RPC this returns the modified array, in workspace-d as a
127 	/// library this changes the parameter values in place.
128 	void resolveRanges(scope const(char)[] code, scope ref DScannerIssue[] issues)
129 	{
130 		LexerConfig config;
131 		auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
132 		if (!tokens.length)
133 			return;
134 
135 		foreach_reverse (i, ref issue; issues)
136 		{
137 			if (!resolveRange(tokens, issue))
138 				issues = issues.remove(i);
139 		}
140 	}
141 
142 	/// Adjusts a D-Scanner line:column location to a start & end range, potentially
143 	/// improving the error message through tokens nearby.
144 	/// Returns: `false` if this issue should be discarded (handled by other issues)
145 	private bool resolveRange(scope const(Token)[] tokens, ref DScannerIssue issue)
146 	out
147 	{
148 		debug (ResolveRange) if (issue.range != typeof(issue.range).init)
149 		{
150 			assert(issue.range[0].line > 0);
151 			assert(issue.range[0].column > 0);
152 			assert(issue.range[1].line > 0);
153 			assert(issue.range[1].column > 0);
154 		}
155 	}
156 	do
157 	{
158 		auto tokenIndex = tokens.tokenIndexAtPosition(issue.line, issue.column);
159 		if (tokenIndex >= tokens.length)
160 		{
161 			if (tokens.length)
162 				issue.range = makeTokenRange(tokens[$ - 1]);
163 			else
164 				issue.range = typeof(issue.range).init;
165 			return true;
166 		}
167 
168 		switch (issue.key)
169 		{
170 		case null:
171 			// syntax errors
172 			if (!adjustRangeForSyntaxError(tokens, tokenIndex, issue))
173 				return false;
174 			improveErrorMessage(issue);
175 			return true;
176 		case LocalImportCheckKEY:
177 			if (adjustRangeForLocalImportsError(tokens, tokenIndex, issue))
178 				return true;
179 			goto default;
180 		case LongLineCheckKEY:
181 			issue.range = makeTokenRange(tokens[tokenIndex], tokens[min($ - 1, tokens.tokenIndexAtPosition(issue.line, 1000))]);
182 			return true;
183 		default:
184 			issue.range = makeTokenRange(tokens[tokenIndex]);
185 			return true;
186 		}
187 	}
188 
189 	private void improveErrorMessage(ref DScannerIssue issue)
190 	{
191 		// identifier is not literally expected
192 		issue.description = issue.description.replace("`identifier`", "identifier");
193 
194 		static immutable expectedIdentifierStart = "Expected identifier instead of `";
195 		static immutable keywordReplacement = "Expected identifier instead of reserved keyword `";
196 		if (issue.description.startsWith(expectedIdentifierStart))
197 		{
198 			if (issue.description.length > expectedIdentifierStart.length + 1
199 				&& issue.description[expectedIdentifierStart.length].isIdentifierChar)
200 			{
201 				// expected identifier instead of keyword (probably) here because
202 				// first character of "instead of `..." is an identifier character.
203 				issue.description = keywordReplacement ~ issue.description[expectedIdentifierStart.length .. $];
204 			}
205 		}
206 	}
207 
208 	private bool adjustRangeForSyntaxError(scope const(Token)[] tokens, size_t currentToken, ref DScannerIssue issue)
209 	{
210 		auto s = issue.description;
211 
212 		if (s.startsWith("Expected `"))
213 		{
214 			s = s["Expected ".length .. $];
215 			if (s.startsWith("`;`"))
216 			{
217 				// span after last word
218 				size_t issueStartExclusive = currentToken;
219 				foreach_reverse (i, token; tokens[0 .. currentToken])
220 				{
221 					if (token.type == tok!";")
222 					{
223 						// this ain't right, expected semicolon issue but
224 						// semicolon is the first thing before this token
225 						// happens when syntax before is broken, let's discard!
226 						// for example in `foo.foreach(a;b)`
227 						return false;
228 					}
229 					issueStartExclusive = i;
230 					if (token.isLikeIdentifier)
231 						break;
232 				}
233 
234 				size_t issueEnd = issueStartExclusive;
235 				auto line = tokens[issueEnd].line;
236 
237 				// span until newline or next word character
238 				foreach (i, token; tokens[issueStartExclusive + 1 .. $])
239 				{
240 					if (token.line != line || token.isLikeIdentifier)
241 						break;
242 					issueEnd = issueStartExclusive + 1 + i;
243 				}
244 
245 				issue.range = [makeTokenEnd(tokens[issueStartExclusive]), makeTokenEnd(tokens[issueEnd])];
246 				return true;
247 			}
248 			else if (s.startsWith("`identifier` instead of `"))
249 			{
250 				auto wanted = s["`identifier` instead of `".length .. $];
251 				if (wanted.length && wanted[0].isIdentifierChar)
252 				{
253 					// wants identifier instead of some keyword (probably)
254 					// happens e.g. after a . and then nothing written and next line contains a keyword
255 					// want to remove the "instead of" in case it's not in the same line
256 					if (currentToken > 0 && tokens[currentToken - 1].line != tokens[currentToken].line)
257 					{
258 						issue.description = "Expected identifier";
259 						issue.range = [makeTokenEnd(tokens[currentToken - 1]), makeTokenStart(tokens[currentToken])];
260 						return true;
261 					}
262 				}
263 			}
264 
265 			// span from start of last word
266 			size_t issueStart = min(max(0, cast(ptrdiff_t)tokens.length - 1), currentToken + 1);
267 			// if a non-identifier was expected, include word before
268 			if (issueStart > 0 && s.length > 2 && s[1].isIdentifierSeparatingChar)
269 				issueStart--;
270 			foreach_reverse (i, token; tokens[0 .. issueStart])
271 			{
272 				issueStart = i;
273 				if (token.isLikeIdentifier)
274 					break;
275 			}
276 
277 			// span to end of next word
278 			size_t searchStart = issueStart;
279 			if (tokens[searchStart].column + tokens[searchStart].tokenText.length <= issue.column)
280 				searchStart++;
281 			size_t issueEnd = min(max(0, cast(ptrdiff_t)tokens.length - 1), searchStart);
282 			foreach (i, token; tokens[searchStart .. $])
283 			{
284 				if (token.isLikeIdentifier)
285 					break;
286 				issueEnd = searchStart + i;
287 			}
288 
289 			issue.range = makeTokenRange(tokens[issueStart], tokens[issueEnd]);
290 		}
291 		else
292 		{
293 			if (tokens[currentToken].type == tok!"auto")
294 			{
295 				// syntax error on the word "auto"
296 				// check for foreach (auto key; value)
297 
298 				if (currentToken >= 2
299 					&& tokens[currentToken - 1].type == tok!"("
300 					&& (tokens[currentToken - 2].type == tok!"foreach" || tokens[currentToken - 2].type == tok!"foreach_reverse"))
301 				{
302 					// this is foreach (auto
303 					issue.key = "workspaced.foreach-auto";
304 					issue.description = "foreach (auto key; value) is not valid D "
305 						~ "syntax. Use foreach (key; value) instead.";
306 					// range is used in code_actions to remove auto
307 					issue.range = makeTokenRange(tokens[currentToken]);
308 					return true;
309 				}
310 			}
311 
312 			issue.range = makeTokenRange(tokens[currentToken]);
313 		}
314 		return true;
315 	}
316 
317 	// adjusts error location of
318 	// import |std.stdio;
319 	// to
320 	// ~import std.stdio;~
321 	private bool adjustRangeForLocalImportsError(scope const(Token)[] tokens, size_t currentToken, ref DScannerIssue issue)
322 	{
323 		size_t startIndex = currentToken;
324 		size_t endIndex = currentToken;
325 
326 		while (startIndex > 0 && tokens[startIndex].type != tok!"import")
327 			startIndex--;
328 		while (endIndex < tokens.length && tokens[endIndex].type != tok!";")
329 			endIndex++;
330 
331 		issue.range = makeTokenRange(tokens[startIndex], tokens[endIndex]);
332 		return true;
333 	}
334 
335 	/// Gets the used D-Scanner config, optionally reading from a given
336 	/// dscanner.ini file.
337 	/// Params:
338 	///   ini = an ini to load. Only reading from it if it exists. If this is
339 	///         relative, this function will try both in getcwd and in the
340 	///         instance.cwd, if an instance is set.
341 	///   skipWorkspacedPaths = if true, don't attempt to override the given ini
342 	///         with workspace-d user configs.
343 	///   defaultConfig = default D-Scanner configuration to use if no user
344 	///         config exists (workspace-d specific or ini argument)
345 	StaticAnalysisConfig getConfig(string ini = "dscanner.ini",
346 		bool skipWorkspacedPaths = false,
347 		const StaticAnalysisConfig defaultConfig = StaticAnalysisConfig.init)
348 	{
349 		import std.path : buildPath;
350 
351 		StaticAnalysisConfig config = defaultConfig is StaticAnalysisConfig.init
352 			? defaultStaticAnalysisConfig()
353 			: cast()defaultConfig;
354 		if (!skipWorkspacedPaths && getConfigPath("dscanner.ini", ini))
355 		{
356 			static bool didWarn = false;
357 			if (!didWarn)
358 			{
359 				warning("Overriding Dscanner ini with workspace-d dscanner.ini config file");
360 				didWarn = true;
361 			}
362 		}
363 		string cwd = getcwd;
364 		if (refInstance !is null)
365 			cwd = refInstance.cwd;
366 
367 		if (ini.exists)
368 		{
369 			readINIFile(config, ini);
370 		}
371 		else
372 		{
373 			auto p = buildPath(cwd, ini);
374 			if (p != ini && p.exists)
375 				readINIFile(config, p);
376 		}
377 		return config;
378 	}
379 
380 	private const(Module) parseModule(string file, ubyte[] code, RollbackAllocator* p,
381 			ref StringCache cache, ref const(Token)[] tokens, ref DScannerIssue[] issues)
382 	{
383 		LexerConfig config;
384 		config.fileName = file;
385 		config.stringBehavior = StringBehavior.source;
386 		tokens = getTokensForParser(code, config, &cache);
387 
388 		void addIssue(string fileName, size_t line, size_t column, string message, bool isError)
389 		{
390 			issues ~= DScannerIssue(file, cast(int) line, cast(int) column, isError
391 					? "error" : "warn", message);
392 		}
393 
394 		uint err, warn;
395 		return dparse.parser.parseModule(tokens, file, p, &addIssue, &err, &warn);
396 	}
397 
398 	/// Asynchronously lists all definitions in the specified file.
399 	///
400 	/// If you provide code the file wont be manually read.
401 	///
402 	/// Set verbose to true if you want to receive more temporary symbols and
403 	/// things that could be considered clutter as well.
404 	Future!(DefinitionElement[]) listDefinitions(string file,
405 		scope const(char)[] code = "", bool verbose = false)
406 	{
407 		auto ret = new typeof(return);
408 		gthreads.create({
409 			mixin(traceTask);
410 			try
411 			{
412 				if (code.length && !file.length)
413 					file = "stdin";
414 				if (!code.length)
415 					code = readText(file);
416 				if (!code.length)
417 				{
418 					DefinitionElement[] arr;
419 					ret.finish(arr);
420 					return;
421 				}
422 
423 				RollbackAllocator r;
424 				LexerConfig config;
425 				auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache);
426 
427 				auto m = dparse.parser.parseModule(tokens.array, file, &r);
428 
429 				auto defFinder = new DefinitionFinder();
430 				defFinder.verbose = verbose;
431 				defFinder.visit(m);
432 
433 				ret.finish(defFinder.definitions);
434 			}
435 			catch (Throwable e)
436 			{
437 				ret.error(e);
438 			}
439 		});
440 		return ret;
441 	}
442 
443 	/// Asynchronously finds all definitions of a symbol in the import paths.
444 	Future!(FileLocation[]) findSymbol(string symbol)
445 	{
446 		auto ret = new typeof(return);
447 		gthreads.create({
448 			mixin(traceTask);
449 			try
450 			{
451 				import dscanner.utils : expandArgs;
452 
453 				string[] paths = expandArgs([""] ~ importPaths);
454 				foreach_reverse (i, path; paths)
455 					if (path == "stdin")
456 						paths = paths.remove(i);
457 				FileLocation[] files;
458 				findDeclarationOf((fileName, line, column) {
459 					FileLocation file;
460 					file.file = fileName;
461 					file.line = cast(int) line;
462 					file.column = cast(int) column;
463 					files ~= file;
464 				}, symbol, paths);
465 				ret.finish(files);
466 			}
467 			catch (Throwable e)
468 			{
469 				ret.error(e);
470 			}
471 		});
472 		return ret;
473 	}
474 
475 	/// Returns: all keys & documentation that can be used in a dscanner.ini
476 	INIEntry[] listAllIniFields()
477 	{
478 		import std.traits : getUDAs;
479 
480 		INIEntry[] ret;
481 		foreach (mem; __traits(allMembers, StaticAnalysisConfig))
482 			static if (is(typeof(__traits(getMember, StaticAnalysisConfig, mem)) == string))
483 			{
484 				alias docs = getUDAs!(__traits(getMember, StaticAnalysisConfig, mem), INI);
485 				ret ~= INIEntry(mem, docs.length ? docs[0].msg : "");
486 			}
487 		return ret;
488 	}
489 }
490 
491 /// dscanner.ini setting type
492 struct INIEntry
493 {
494 	///
495 	string name, documentation;
496 }
497 
498 /// Issue type returned by lint
499 struct DScannerIssue
500 {
501 	///
502 	string file;
503 	/// one-based line & column (in bytes) of this diagnostic location
504 	int line, column;
505 	///
506 	string type;
507 	///
508 	string description;
509 	///
510 	string key;
511 	/// Resolved range for content that can be filled with a call to resolveRanges
512 	ResolvedLocation[2] range;
513 
514 	/// Converts this object to a JSONValue
515 	JSONValue _toJSON() const
516 	{
517 		JSONValue[] rangeObj = [
518 			range[0].toJSON,
519 			range[1].toJSON
520 		];
521 		//dfmt off
522 		return JSONValue([
523 			"file": JSONValue(file),
524 			"line": JSONValue(line),
525 			"column": JSONValue(column),
526 			"type": JSONValue(type),
527 			"description": JSONValue(description),
528 			"key": JSONValue(key),
529 			"range": JSONValue(rangeObj),
530 		]);
531 		//dfmt on
532 	}
533 }
534 
535 /// Describes a code location in exact byte offset, line number and column for a
536 /// given source code this was resolved against.
537 struct ResolvedLocation
538 {
539 	/// byte offset of the character in question - may be 0 if line and column are set
540 	ulong index;
541 	/// one-based line
542 	uint line;
543 	/// one-based character offset inside the line in bytes
544 	uint column;
545 }
546 
547 ResolvedLocation[2] makeTokenRange(const Token token)
548 {
549 	return makeTokenRange(token, token);
550 }
551 
552 ResolvedLocation[2] makeTokenRange(const Token start, const Token end)
553 {
554 	return [makeTokenStart(start), makeTokenEnd(end)];
555 }
556 
557 ResolvedLocation makeTokenStart(const Token token)
558 {
559 	ResolvedLocation ret;
560 	ret.index = cast(uint) token.index;
561 	ret.line = cast(uint) token.line;
562 	ret.column = cast(uint) token.column;
563 	return ret;
564 }
565 
566 ResolvedLocation makeTokenEnd(const Token token)
567 {
568 	import std..string : lineSplitter;
569 
570 	ResolvedLocation ret;
571 	auto text = tokenText(token);
572 	ret.index = token.index + text.length;
573 	int numLines;
574 	size_t lastLength;
575 	foreach (line; lineSplitter(text))
576 	{
577 		numLines++;
578 		lastLength = line.length;
579 	}
580 	if (numLines > 1)
581 	{
582 		ret.line = cast(uint)(token.line + numLines - 1);
583 		ret.column = cast(uint)(lastLength + 1);
584 	}
585 	else
586 	{
587 		ret.line = cast(uint)(token.line);
588 		ret.column = cast(uint)(token.column + text.length);
589 	}
590 	return ret;
591 }
592 
593 /// Returned by find-symbol
594 struct FileLocation
595 {
596 	///
597 	string file;
598 	/// 1-based line number and column byte offset
599 	int line, column;
600 }
601 
602 /// Returned by list-definitions
603 struct DefinitionElement
604 {
605 	///
606 	string name;
607 	/// 1-based line number
608 	int line;
609 	/// One of
610 	/// * `c` = class
611 	/// * `s` = struct
612 	/// * `i` = interface
613 	/// * `T` = template
614 	/// * `f` = function/ctor/dtor
615 	/// * `g` = enum {}
616 	/// * `u` = union
617 	/// * `e` = enum member/definition
618 	/// * `v` = variable/invariant
619 	/// * `a` = alias
620 	/// * `U` = unittest (only in verbose mode)
621 	/// * `D` = debug specification (only in verbose mode)
622 	/// * `V` = version specification (only in verbose mode)
623 	/// * `C` = static module ctor (only in verbose mode)
624 	/// * `S` = shared static module ctor (only in verbose mode)
625 	/// * `Q` = static module dtor (only in verbose mode)
626 	/// * `W` = shared static module dtor (only in verbose mode)
627 	/// * `P` = postblit/copy ctor (only in verbose mode)
628 	string type;
629 	///
630 	string[string] attributes;
631 	///
632 	int[2] range;
633 
634 	bool isVerboseType() const
635 	{
636 		import std.ascii : isUpper;
637 
638 		return type.length == 1 && type[0] != 'T' && isUpper(type[0]);
639 	}
640 }
641 
642 private:
643 
644 string typeForWarning(string key)
645 {
646 	switch (key)
647 	{
648 	case "dscanner.bugs.backwards_slices":
649 	case "dscanner.bugs.if_else_same":
650 	case "dscanner.bugs.logic_operator_operands":
651 	case "dscanner.bugs.self_assignment":
652 	case "dscanner.confusing.argument_parameter_mismatch":
653 	case "dscanner.confusing.brexp":
654 	case "dscanner.confusing.builtin_property_names":
655 	case "dscanner.confusing.constructor_args":
656 	case "dscanner.confusing.function_attributes":
657 	case "dscanner.confusing.lambda_returns_lambda":
658 	case "dscanner.confusing.logical_precedence":
659 	case "dscanner.confusing.struct_constructor_default_args":
660 	case "dscanner.deprecated.delete_keyword":
661 	case "dscanner.deprecated.floating_point_operators":
662 	case "dscanner.if_statement":
663 	case "dscanner.performance.enum_array_literal":
664 	case "dscanner.style.allman":
665 	case "dscanner.style.alias_syntax":
666 	case "dscanner.style.doc_missing_params":
667 	case "dscanner.style.doc_missing_returns":
668 	case "dscanner.style.doc_non_existing_params":
669 	case "dscanner.style.explicitly_annotated_unittest":
670 	case "dscanner.style.has_public_example":
671 	case "dscanner.style.imports_sortedness":
672 	case "dscanner.style.long_line":
673 	case "dscanner.style.number_literals":
674 	case "dscanner.style.phobos_naming_convention":
675 	case "dscanner.style.undocumented_declaration":
676 	case "dscanner.suspicious.auto_ref_assignment":
677 	case "dscanner.suspicious.catch_em_all":
678 	case "dscanner.suspicious.comma_expression":
679 	case "dscanner.suspicious.incomplete_operator_overloading":
680 	case "dscanner.suspicious.incorrect_infinite_range":
681 	case "dscanner.suspicious.label_var_same_name":
682 	case "dscanner.suspicious.length_subtraction":
683 	case "dscanner.suspicious.local_imports":
684 	case "dscanner.suspicious.missing_return":
685 	case "dscanner.suspicious.object_const":
686 	case "dscanner.suspicious.redundant_attributes":
687 	case "dscanner.suspicious.redundant_parens":
688 	case "dscanner.suspicious.static_if_else":
689 	case "dscanner.suspicious.unmodified":
690 	case "dscanner.suspicious.unused_label":
691 	case "dscanner.suspicious.unused_parameter":
692 	case "dscanner.suspicious.unused_variable":
693 	case "dscanner.suspicious.useless_assert":
694 	case "dscanner.unnecessary.duplicate_attribute":
695 	case "dscanner.useless.final":
696 	case "dscanner.useless-initializer":
697 	case "dscanner.vcall_ctor":
698 		return "warn";
699 	case "dscanner.syntax":
700 		return "error";
701 	default:
702 		stderr.writeln("Warning: unimplemented DScanner reason, assuming warning: ", key);
703 		return "warn";
704 	}
705 }
706 
707 final class DefinitionFinder : ASTVisitor
708 {
709 	override void visit(const ClassDeclaration dec)
710 	{
711 		if (!dec.structBody)
712 			return;
713 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "c", context,
714 				[
715 					cast(int) dec.structBody.startLocation,
716 					cast(int) dec.structBody.endLocation
717 				]);
718 		auto c = context;
719 		context = ContextType(["class": dec.name.text], null, "public");
720 		dec.accept(this);
721 		context = c;
722 	}
723 
724 	override void visit(const StructDeclaration dec)
725 	{
726 		if (!dec.structBody)
727 			return;
728 		if (dec.name == tok!"")
729 		{
730 			dec.accept(this);
731 			return;
732 		}
733 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "s", context,
734 				[
735 					cast(int) dec.structBody.startLocation,
736 					cast(int) dec.structBody.endLocation
737 				]);
738 		auto c = context;
739 		context = ContextType(["struct": dec.name.text], null, "public");
740 		dec.accept(this);
741 		context = c;
742 	}
743 
744 	override void visit(const InterfaceDeclaration dec)
745 	{
746 		if (!dec.structBody)
747 			return;
748 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "i", context,
749 				[
750 					cast(int) dec.structBody.startLocation,
751 					cast(int) dec.structBody.endLocation
752 				]);
753 		auto c = context;
754 		context = ContextType(["interface:": dec.name.text], null, context.access);
755 		dec.accept(this);
756 		context = c;
757 	}
758 
759 	override void visit(const TemplateDeclaration dec)
760 	{
761 		auto def = makeDefinition(dec.name.text, dec.name.line, "T", context,
762 				[cast(int) dec.startLocation, cast(int) dec.endLocation]);
763 		def.attributes["signature"] = paramsToString(dec);
764 		definitions ~= def;
765 		auto c = context;
766 		context = ContextType(["template": dec.name.text], null, context.access);
767 		dec.accept(this);
768 		context = c;
769 	}
770 
771 	override void visit(const FunctionDeclaration dec)
772 	{
773 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody
774 				|| !dec.functionBody.specifiedFunctionBody.blockStatement)
775 			return;
776 		auto def = makeDefinition(dec.name.text, dec.name.line, "f", context,
777 				[
778 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
779 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation
780 				]);
781 		def.attributes["signature"] = paramsToString(dec);
782 		if (dec.returnType !is null)
783 			def.attributes["return"] = astToString(dec.returnType);
784 		definitions ~= def;
785 	}
786 
787 	override void visit(const Constructor dec)
788 	{
789 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody
790 				|| !dec.functionBody.specifiedFunctionBody.blockStatement)
791 			return;
792 		auto def = makeDefinition("this", dec.line, "f", context,
793 				[
794 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
795 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation
796 				]);
797 		def.attributes["signature"] = paramsToString(dec);
798 		definitions ~= def;
799 	}
800 
801 	override void visit(const Destructor dec)
802 	{
803 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody
804 				|| !dec.functionBody.specifiedFunctionBody.blockStatement)
805 			return;
806 		definitions ~= makeDefinition("~this", dec.line, "f", context,
807 				[
808 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
809 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation
810 				]);
811 	}
812 
813 	override void visit(const Postblit dec)
814 	{
815 		if (!verbose)
816 			return;
817 
818 		if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody
819 				|| !dec.functionBody.specifiedFunctionBody.blockStatement)
820 			return;
821 		definitions ~= makeDefinition("this(this)", dec.line, "f", context,
822 				[
823 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
824 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation
825 				]);
826 	}
827 
828 	override void visit(const EnumDeclaration dec)
829 	{
830 		if (!dec.enumBody)
831 			return;
832 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context,
833 				[cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]);
834 		auto c = context;
835 		context = ContextType(["enum": dec.name.text], null, context.access);
836 		dec.accept(this);
837 		context = c;
838 	}
839 
840 	override void visit(const UnionDeclaration dec)
841 	{
842 		if (!dec.structBody)
843 			return;
844 		if (dec.name == tok!"")
845 		{
846 			dec.accept(this);
847 			return;
848 		}
849 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context,
850 				[
851 					cast(int) dec.structBody.startLocation,
852 					cast(int) dec.structBody.endLocation
853 				]);
854 		auto c = context;
855 		context = ContextType(["union": dec.name.text], null, context.access);
856 		dec.accept(this);
857 		context = c;
858 	}
859 
860 	override void visit(const AnonymousEnumMember mem)
861 	{
862 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
863 				[
864 					cast(int) mem.name.index,
865 					cast(int) mem.name.index + cast(int) mem.name.text.length
866 				]);
867 	}
868 
869 	override void visit(const EnumMember mem)
870 	{
871 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
872 				[
873 					cast(int) mem.name.index,
874 					cast(int) mem.name.index + cast(int) mem.name.text.length
875 				]);
876 	}
877 
878 	override void visit(const VariableDeclaration dec)
879 	{
880 		foreach (d; dec.declarators)
881 			definitions ~= makeDefinition(d.name.text, d.name.line, "v", context,
882 					[
883 						cast(int) d.name.index,
884 						cast(int) d.name.index + cast(int) d.name.text.length
885 					]);
886 		dec.accept(this);
887 	}
888 
889 	override void visit(const AutoDeclaration dec)
890 	{
891 		foreach (i; dec.parts.map!(a => a.identifier))
892 			definitions ~= makeDefinition(i.text, i.line, "v", context,
893 					[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
894 		dec.accept(this);
895 	}
896 
897 	override void visit(const Invariant dec)
898 	{
899 		if (!dec.blockStatement)
900 			return;
901 		definitions ~= makeDefinition("invariant", dec.line, "v", context,
902 				[cast(int) dec.index, cast(int) dec.blockStatement.endLocation]);
903 	}
904 
905 	override void visit(const ModuleDeclaration dec)
906 	{
907 		context = ContextType(null, null, "public");
908 		dec.accept(this);
909 	}
910 
911 	override void visit(const Attribute attribute)
912 	{
913 		if (attribute.attribute != tok!"")
914 		{
915 			switch (attribute.attribute.type)
916 			{
917 			case tok!"export":
918 				context.access = "public";
919 				break;
920 			case tok!"public":
921 				context.access = "public";
922 				break;
923 			case tok!"package":
924 				context.access = "protected";
925 				break;
926 			case tok!"protected":
927 				context.access = "protected";
928 				break;
929 			case tok!"private":
930 				context.access = "private";
931 				break;
932 			default:
933 			}
934 		}
935 		else if (attribute.deprecated_ !is null)
936 		{
937 			string reason;
938 			if (attribute.deprecated_.assignExpression)
939 				reason = evaluateExpressionString(attribute.deprecated_.assignExpression);
940 			context.attr["deprecation"] = reason.length ? reason : "";
941 		}
942 
943 		attribute.accept(this);
944 	}
945 
946 	override void visit(const AtAttribute atAttribute)
947 	{
948 		if (atAttribute.argumentList)
949 		{
950 			foreach (item; atAttribute.argumentList.items)
951 			{
952 				auto str = evaluateExpressionString(item);
953 
954 				if (str !is null)
955 					context.privateAttr["utName"] = str;
956 			}
957 		}
958 		atAttribute.accept(this);
959 	}
960 
961 	override void visit(const AttributeDeclaration dec)
962 	{
963 		accessSt = AccessState.Keep;
964 		dec.accept(this);
965 	}
966 
967 	override void visit(const Declaration dec)
968 	{
969 		auto c = context;
970 		dec.accept(this);
971 
972 		final switch (accessSt) with (AccessState)
973 		{
974 		case Reset:
975 			context = c;
976 			break;
977 		case Keep:
978 			break;
979 		}
980 		accessSt = AccessState.Reset;
981 	}
982 
983 	override void visit(const DebugSpecification dec)
984 	{
985 		if (!verbose)
986 			return;
987 
988 		auto tok = dec.identifierOrInteger;
989 		auto def = makeDefinition(tok.tokenText, tok.line, "D", context,
990 				[
991 					cast(int) tok.index,
992 					cast(int) tok.index + cast(int) tok.text.length
993 				]);
994 
995 		definitions ~= def;
996 		dec.accept(this);
997 	}
998 
999 	override void visit(const VersionSpecification dec)
1000 	{
1001 		if (!verbose)
1002 			return;
1003 
1004 		auto tok = dec.token;
1005 		auto def = makeDefinition(tok.tokenText, tok.line, "V", context,
1006 				[
1007 					cast(int) tok.index,
1008 					cast(int) tok.index + cast(int) tok.text.length
1009 				]);
1010 
1011 		definitions ~= def;
1012 		dec.accept(this);
1013 	}
1014 
1015 	override void visit(const Unittest dec)
1016 	{
1017 		if (!verbose)
1018 			return;
1019 
1020 		if (!dec.blockStatement)
1021 			return;
1022 		string testName = text("__unittest_L", dec.line, "_C", dec.column);
1023 		definitions ~= makeDefinition(testName, dec.line, "U", context,
1024 				[
1025 					cast(int) dec.tokens[0].index,
1026 					cast(int) dec.blockStatement.endLocation
1027 				], "U");
1028 
1029 		// TODO: decide if we want to include types nested in unittests
1030 		// dec.accept(this);
1031 	}
1032 
1033 	private static immutable CtorTypes = ["C", "S", "Q", "W"];
1034 	private static immutable CtorNames = [
1035 		"static this()", "shared static this()",
1036 		"static ~this()", "shared static ~this()"
1037 	];
1038 	static foreach (i, T; AliasSeq!(StaticConstructor, SharedStaticConstructor,
1039 			StaticDestructor, SharedStaticDestructor))
1040 	{
1041 		override void visit(const T dec)
1042 		{
1043 			if (!verbose)
1044 				return;
1045 
1046 			if (!dec.functionBody || !dec.functionBody.specifiedFunctionBody
1047 					|| !dec.functionBody.specifiedFunctionBody.blockStatement)
1048 				return;
1049 			definitions ~= makeDefinition(CtorNames[i], dec.line, CtorTypes[i], context,
1050 				[
1051 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.startLocation,
1052 					cast(int) dec.functionBody.specifiedFunctionBody.blockStatement.endLocation
1053 				]);
1054 			dec.accept(this);
1055 		}
1056 	}
1057 
1058 	override void visit(const AliasDeclaration dec)
1059 	{
1060 		// Old style alias
1061 		if (dec.declaratorIdentifierList)
1062 			foreach (i; dec.declaratorIdentifierList.identifiers)
1063 				definitions ~= makeDefinition(i.text, i.line, "a", context,
1064 						[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
1065 		dec.accept(this);
1066 	}
1067 
1068 	override void visit(const AliasInitializer dec)
1069 	{
1070 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context,
1071 				[
1072 					cast(int) dec.name.index,
1073 					cast(int) dec.name.index + cast(int) dec.name.text.length
1074 				]);
1075 
1076 		dec.accept(this);
1077 	}
1078 
1079 	override void visit(const AliasThisDeclaration dec)
1080 	{
1081 		auto name = dec.identifier;
1082 		definitions ~= makeDefinition(name.text, name.line, "a", context,
1083 				[cast(int) name.index, cast(int) name.index + cast(int) name.text.length]);
1084 
1085 		dec.accept(this);
1086 	}
1087 
1088 	alias visit = ASTVisitor.visit;
1089 
1090 	ContextType context;
1091 	AccessState accessSt;
1092 	DefinitionElement[] definitions;
1093 	bool verbose;
1094 }
1095 
1096 DefinitionElement makeDefinition(string name, size_t line, string type,
1097 		ContextType context, int[2] range, string forType = null)
1098 {
1099 	string[string] attr = context.attr.dup;
1100 	if (context.access.length)
1101 		attr["access"] = context.access;
1102 
1103 	if (forType == "U")
1104 	{
1105 		if (auto utName = "utName" in context.privateAttr)
1106 			attr["name"] = *utName;
1107 	}
1108 	return DefinitionElement(name, cast(int) line, type, attr, range);
1109 }
1110 
1111 enum AccessState
1112 {
1113 	Reset, /// when ascending the AST reset back to the previous access.
1114 	Keep /// when ascending the AST keep the new access.
1115 }
1116 
1117 struct ContextType
1118 {
1119 	string[string] attr;
1120 	string[string] privateAttr;
1121 	string access;
1122 }
1123 
1124 unittest
1125 {
1126 	StaticAnalysisConfig check = StaticAnalysisConfig.init;
1127 	assert(check is StaticAnalysisConfig.init);
1128 }
1129 
1130 unittest
1131 {
1132 	scope backend = new WorkspaceD();
1133 	auto workspace = makeTemporaryTestingWorkspace;
1134 	auto instance = backend.addInstance(workspace.directory);
1135 	backend.register!DscannerComponent;
1136 	DscannerComponent dscanner = instance.get!DscannerComponent;
1137 
1138 	string code = `module foo.bar;
1139 
1140 version = Foo;
1141 debug = Bar;
1142 
1143 void hello() {
1144 	int x = 1;
1145 }
1146 
1147 int y = 2;
1148 
1149 int
1150 bar()
1151 {
1152 }
1153 
1154 unittest
1155 {
1156 }
1157 
1158 @( "named" )
1159 unittest
1160 {
1161 }
1162 
1163 class X
1164 {
1165 	this(int x) {}
1166 	this(this) {}
1167 	~this() {}
1168 
1169 	unittest
1170 	{
1171 	}
1172 }
1173 
1174 shared static this()
1175 {
1176 }
1177 
1178 `;
1179 
1180 	auto defs = dscanner.listDefinitions("stdin", code, false).getBlocking();
1181 
1182 	assert(defs == [
1183 			DefinitionElement("hello", 6, "f", [
1184 					"signature": "()",
1185 					"access": "public",
1186 					"return": "void"
1187 				], [59, 73]),
1188 			DefinitionElement("y", 10, "v", ["access": "public"], [80, 81]),
1189 			DefinitionElement("bar", 13, "f", [
1190 					"signature": "()",
1191 					"access": "public",
1192 					"return": "int"
1193 				], [98, 100]),
1194 			DefinitionElement("X", 26, "c", ["access": "public"], [152,
1195 					214]),
1196 			DefinitionElement("this", 28, "f", [
1197 					"signature": "(int x)",
1198 					"access": "public",
1199 					"class": "X"
1200 				], [167, 168]),
1201 			DefinitionElement("~this", 30, "f", [
1202 					"access": "public",
1203 					"class": "X"
1204 				], [194, 195])
1205 			]);
1206 
1207 	// verbose definitions
1208 	defs = dscanner.listDefinitions("stdin", code, true).getBlocking();
1209 
1210 	assert(defs == [
1211 			DefinitionElement("Foo", 3, "V", ["access": "public"], [27, 30]),
1212 			DefinitionElement("Bar", 4, "D", ["access": "public"], [40, 43]),
1213 			DefinitionElement("hello", 6, "f", [
1214 					"signature": "()",
1215 					"access": "public",
1216 					"return": "void"
1217 				], [59, 73]),
1218 			DefinitionElement("y", 10, "v", ["access": "public"], [80, 81]),
1219 			DefinitionElement("bar", 13, "f", [
1220 					"signature": "()",
1221 					"access": "public",
1222 					"return": "int"
1223 				], [98, 100]),
1224 			DefinitionElement("__unittest_L17_C1", 17, "U",
1225 				["access": "public"], [103,
1226 					114]),
1227 			DefinitionElement("__unittest_L22_C1", 22, "U",
1228 				["access": "public", "name": "named"],
1229 				[130, 141]),
1230 			DefinitionElement("X", 26, "c", ["access": "public"], [152,
1231 					214]),
1232 			DefinitionElement("this", 28, "f", [
1233 					"signature": "(int x)",
1234 					"access": "public",
1235 					"class": "X"
1236 				], [167, 168]),
1237 			DefinitionElement("this(this)", 29, "f", [
1238 					"access": "public",
1239 					"class": "X"
1240 				], [182, 183]),
1241 			DefinitionElement("~this", 30, "f", [
1242 					"access": "public",
1243 					"class": "X"
1244 				], [194, 195]),
1245 			DefinitionElement("__unittest_L32_C2", 32, "U", [
1246 					"access": "public",
1247 					"class": "X"
1248 				], [199, 212]),
1249 			DefinitionElement("shared static this()", 37, "S", [
1250 					"access": "public"
1251 				], [238, 240])
1252 			]);
1253 
1254 }