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 		auto def = makeDefinition(dec.name.text, dec.name.line, "f", context,
774 				[
775 					cast(int) dec.functionBody.startLocation,
776 					cast(int) dec.functionBody.endLocation
777 				]);
778 		def.attributes["signature"] = paramsToString(dec);
779 		if (dec.returnType !is null)
780 			def.attributes["return"] = astToString(dec.returnType);
781 		definitions ~= def;
782 	}
783 
784 	override void visit(const Constructor dec)
785 	{
786 		auto def = makeDefinition("this", dec.line, "f", context,
787 				[
788 					cast(int) dec.functionBody.startLocation,
789 					cast(int) dec.functionBody.endLocation
790 				]);
791 		def.attributes["signature"] = paramsToString(dec);
792 		definitions ~= def;
793 	}
794 
795 	override void visit(const Destructor dec)
796 	{
797 		definitions ~= makeDefinition("~this", dec.line, "f", context,
798 				[
799 					cast(int) dec.functionBody.startLocation,
800 					cast(int) dec.functionBody.endLocation
801 				]);
802 	}
803 
804 	override void visit(const Postblit dec)
805 	{
806 		if (!verbose)
807 			return;
808 
809 		definitions ~= makeDefinition("this(this)", dec.line, "f", context,
810 				[
811 					cast(int) dec.functionBody.startLocation,
812 					cast(int) dec.functionBody.endLocation
813 				]);
814 	}
815 
816 	override void visit(const EnumDeclaration dec)
817 	{
818 		if (!dec.enumBody)
819 			return;
820 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "g", context,
821 				[cast(int) dec.enumBody.startLocation, cast(int) dec.enumBody.endLocation]);
822 		auto c = context;
823 		context = ContextType(["enum": dec.name.text], null, context.access);
824 		dec.accept(this);
825 		context = c;
826 	}
827 
828 	override void visit(const UnionDeclaration dec)
829 	{
830 		if (!dec.structBody)
831 			return;
832 		if (dec.name == tok!"")
833 		{
834 			dec.accept(this);
835 			return;
836 		}
837 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "u", context,
838 				[
839 					cast(int) dec.structBody.startLocation,
840 					cast(int) dec.structBody.endLocation
841 				]);
842 		auto c = context;
843 		context = ContextType(["union": dec.name.text], null, context.access);
844 		dec.accept(this);
845 		context = c;
846 	}
847 
848 	override void visit(const AnonymousEnumMember mem)
849 	{
850 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
851 				[
852 					cast(int) mem.name.index,
853 					cast(int) mem.name.index + cast(int) mem.name.text.length
854 				]);
855 	}
856 
857 	override void visit(const EnumMember mem)
858 	{
859 		definitions ~= makeDefinition(mem.name.text, mem.name.line, "e", context,
860 				[
861 					cast(int) mem.name.index,
862 					cast(int) mem.name.index + cast(int) mem.name.text.length
863 				]);
864 	}
865 
866 	override void visit(const VariableDeclaration dec)
867 	{
868 		foreach (d; dec.declarators)
869 			definitions ~= makeDefinition(d.name.text, d.name.line, "v", context,
870 					[
871 						cast(int) d.name.index,
872 						cast(int) d.name.index + cast(int) d.name.text.length
873 					]);
874 		dec.accept(this);
875 	}
876 
877 	override void visit(const AutoDeclaration dec)
878 	{
879 		foreach (i; dec.parts.map!(a => a.identifier))
880 			definitions ~= makeDefinition(i.text, i.line, "v", context,
881 					[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
882 		dec.accept(this);
883 	}
884 
885 	override void visit(const Invariant dec)
886 	{
887 		if (!dec.blockStatement)
888 			return;
889 		definitions ~= makeDefinition("invariant", dec.line, "v", context,
890 				[cast(int) dec.index, cast(int) dec.blockStatement.endLocation]);
891 	}
892 
893 	override void visit(const ModuleDeclaration dec)
894 	{
895 		context = ContextType(null, null, "public");
896 		dec.accept(this);
897 	}
898 
899 	override void visit(const Attribute attribute)
900 	{
901 		if (attribute.attribute != tok!"")
902 		{
903 			switch (attribute.attribute.type)
904 			{
905 			case tok!"export":
906 				context.access = "public";
907 				break;
908 			case tok!"public":
909 				context.access = "public";
910 				break;
911 			case tok!"package":
912 				context.access = "protected";
913 				break;
914 			case tok!"protected":
915 				context.access = "protected";
916 				break;
917 			case tok!"private":
918 				context.access = "private";
919 				break;
920 			default:
921 			}
922 		}
923 		else if (attribute.deprecated_ !is null)
924 		{
925 			string reason;
926 			if (attribute.deprecated_.assignExpression)
927 				reason = evaluateExpressionString(attribute.deprecated_.assignExpression);
928 			context.attr["deprecation"] = reason.length ? reason : "";
929 		}
930 
931 		attribute.accept(this);
932 	}
933 
934 	override void visit(const AtAttribute atAttribute)
935 	{
936 		if (atAttribute.argumentList)
937 		{
938 			foreach (item; atAttribute.argumentList.items)
939 			{
940 				auto str = evaluateExpressionString(item);
941 
942 				if (str !is null)
943 					context.privateAttr["utName"] = str;
944 			}
945 		}
946 		atAttribute.accept(this);
947 	}
948 
949 	override void visit(const AttributeDeclaration dec)
950 	{
951 		accessSt = AccessState.Keep;
952 		dec.accept(this);
953 	}
954 
955 	override void visit(const Declaration dec)
956 	{
957 		auto c = context;
958 		dec.accept(this);
959 
960 		final switch (accessSt) with (AccessState)
961 		{
962 		case Reset:
963 			context = c;
964 			break;
965 		case Keep:
966 			break;
967 		}
968 		accessSt = AccessState.Reset;
969 	}
970 
971 	override void visit(const DebugSpecification dec)
972 	{
973 		if (!verbose)
974 			return;
975 
976 		auto tok = dec.identifierOrInteger;
977 		auto def = makeDefinition(tok.tokenText, tok.line, "D", context,
978 				[
979 					cast(int) tok.index,
980 					cast(int) tok.index + cast(int) tok.text.length
981 				]);
982 
983 		definitions ~= def;
984 		dec.accept(this);
985 	}
986 
987 	override void visit(const VersionSpecification dec)
988 	{
989 		if (!verbose)
990 			return;
991 
992 		auto tok = dec.token;
993 		auto def = makeDefinition(tok.tokenText, tok.line, "V", context,
994 				[
995 					cast(int) tok.index,
996 					cast(int) tok.index + cast(int) tok.text.length
997 				]);
998 
999 		definitions ~= def;
1000 		dec.accept(this);
1001 	}
1002 
1003 	override void visit(const Unittest dec)
1004 	{
1005 		if (!verbose)
1006 			return;
1007 
1008 		if (!dec.blockStatement)
1009 			return;
1010 		string testName = text("__unittest_L", dec.line, "_C", dec.column);
1011 		definitions ~= makeDefinition(testName, dec.line, "U", context,
1012 				[
1013 					cast(int) dec.tokens[0].index,
1014 					cast(int) dec.blockStatement.endLocation
1015 				], "U");
1016 
1017 		// TODO: decide if we want to include types nested in unittests
1018 		// dec.accept(this);
1019 	}
1020 
1021 	private static immutable CtorTypes = ["C", "S", "Q", "W"];
1022 	private static immutable CtorNames = [
1023 		"static this()", "shared static this()",
1024 		"static ~this()", "shared static ~this()"
1025 	];
1026 	static foreach (i, T; AliasSeq!(StaticConstructor, SharedStaticConstructor,
1027 			StaticDestructor, SharedStaticDestructor))
1028 	{
1029 		override void visit(const T dec)
1030 		{
1031 			if (!verbose)
1032 				return;
1033 
1034 			definitions ~= makeDefinition(CtorNames[i], dec.line, CtorTypes[i], context,
1035 				[
1036 					cast(int) dec.functionBody.startLocation,
1037 					cast(int) dec.functionBody.endLocation
1038 				]);
1039 		}
1040 	}
1041 
1042 	override void visit(const AliasDeclaration dec)
1043 	{
1044 		// Old style alias
1045 		if (dec.declaratorIdentifierList)
1046 			foreach (i; dec.declaratorIdentifierList.identifiers)
1047 				definitions ~= makeDefinition(i.text, i.line, "a", context,
1048 						[cast(int) i.index, cast(int) i.index + cast(int) i.text.length]);
1049 		dec.accept(this);
1050 	}
1051 
1052 	override void visit(const AliasInitializer dec)
1053 	{
1054 		definitions ~= makeDefinition(dec.name.text, dec.name.line, "a", context,
1055 				[
1056 					cast(int) dec.name.index,
1057 					cast(int) dec.name.index + cast(int) dec.name.text.length
1058 				]);
1059 
1060 		dec.accept(this);
1061 	}
1062 
1063 	override void visit(const AliasThisDeclaration dec)
1064 	{
1065 		auto name = dec.identifier;
1066 		definitions ~= makeDefinition(name.text, name.line, "a", context,
1067 				[cast(int) name.index, cast(int) name.index + cast(int) name.text.length]);
1068 
1069 		dec.accept(this);
1070 	}
1071 
1072 	alias visit = ASTVisitor.visit;
1073 
1074 	ContextType context;
1075 	AccessState accessSt;
1076 	DefinitionElement[] definitions;
1077 	bool verbose;
1078 }
1079 
1080 DefinitionElement makeDefinition(string name, size_t line, string type,
1081 		ContextType context, int[2] range, string forType = null)
1082 {
1083 	string[string] attr = context.attr.dup;
1084 	if (context.access.length)
1085 		attr["access"] = context.access;
1086 
1087 	if (forType == "U")
1088 	{
1089 		if (auto utName = "utName" in context.privateAttr)
1090 			attr["name"] = *utName;
1091 	}
1092 	return DefinitionElement(name, cast(int) line, type, attr, range);
1093 }
1094 
1095 enum AccessState
1096 {
1097 	Reset, /// when ascending the AST reset back to the previous access.
1098 	Keep /// when ascending the AST keep the new access.
1099 }
1100 
1101 struct ContextType
1102 {
1103 	string[string] attr;
1104 	string[string] privateAttr;
1105 	string access;
1106 }
1107 
1108 unittest
1109 {
1110 	StaticAnalysisConfig check = StaticAnalysisConfig.init;
1111 	assert(check is StaticAnalysisConfig.init);
1112 }
1113 
1114 unittest
1115 {
1116 	scope backend = new WorkspaceD();
1117 	auto workspace = makeTemporaryTestingWorkspace;
1118 	auto instance = backend.addInstance(workspace.directory);
1119 	backend.register!DscannerComponent;
1120 	DscannerComponent dscanner = instance.get!DscannerComponent;
1121 
1122 	bool verbose;
1123 	DefinitionElement[] expectedDefinitions;
1124 	runTestDataFileTests("test/data/list_definition",
1125 		() {
1126 			verbose = false;
1127 			expectedDefinitions = null;
1128 		},
1129 		(code, variable, value) {
1130 			switch (variable)
1131 			{
1132 			case "verbose":
1133 				verbose = value.boolean;
1134 				break;
1135 			default:
1136 				assert(false, "Unknown test variable " ~ variable);
1137 			}
1138 		},
1139 		(code, parts, line) {
1140 			assert(parts.length == 6, "malformed definition test line: " ~ line);
1141 
1142 			string[string] dict;
1143 			foreach (k, v; parseJSON(parts[3]).object)
1144 				dict[k] = v.str;
1145 
1146 			expectedDefinitions ~= DefinitionElement(
1147 				parts[0],
1148 				parts[1].to!int,
1149 				parts[2],
1150 				dict,
1151 				[parts[4].to!int, parts[5].to!int]
1152 			);
1153 		},
1154 		(code) {
1155 			auto defs = dscanner.listDefinitions("stdin", code, verbose).getBlocking();
1156 			assert(defs == expectedDefinitions);
1157 		});
1158 }
1159 
1160 size_t startLocation(const FunctionBody b)
1161 {
1162 	return b.tokens.length > 0 ? b.tokens[0].index : 0;
1163 }
1164 
1165 size_t endLocation(const FunctionBody b)
1166 {
1167 	return b.tokens.length > 0 ? (b.tokens[$ - 1].index + b.tokens[$ - 1].tokenText.length) : 0;
1168 }