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