1 //          Copyright Brian Schott (Hackerpilot) 2012.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 module dscanner.main;
7 
8 import std.algorithm;
9 import std.array;
10 import std.conv;
11 import std.file;
12 import std.getopt;
13 import std.path;
14 import std.stdio;
15 import std.range;
16 import std.experimental.lexer;
17 import std.typecons : scoped;
18 import std.functional : toDelegate;
19 import dparse.lexer;
20 import dparse.parser;
21 import dparse.rollback_allocator;
22 
23 import dscanner.highlighter;
24 import dscanner.stats;
25 import dscanner.ctags;
26 import dscanner.etags;
27 import dscanner.astprinter;
28 import dscanner.imports;
29 import dscanner.outliner;
30 import dscanner.symbol_finder;
31 import dscanner.analysis.run;
32 import dscanner.analysis.config;
33 import dscanner.dscanner_version;
34 import dscanner.utils;
35 
36 import inifiled;
37 
38 import dsymbol.modulecache;
39 
40 version (unittest)
41 	void main()
42 {
43 }
44 else
45 	int main(string[] args)
46 {
47 	bool sloc;
48 	bool highlight;
49 	bool ctags;
50 	bool etags;
51 	bool etagsAll;
52 	bool help;
53 	bool tokenCount;
54 	bool syntaxCheck;
55 	bool ast;
56 	bool imports;
57 	bool recursiveImports;
58 	bool muffin;
59 	bool outline;
60 	bool tokenDump;
61 	bool styleCheck;
62 	bool defaultConfig;
63 	bool report;
64 	bool skipTests;
65 	string reportFormat;
66 	string reportFile;
67 	string symbolName;
68 	string configLocation;
69 	string[] importPaths;
70 	bool printVersion;
71 	bool explore;
72 	string errorFormat;
73 
74 	try
75 	{
76 		// dfmt off
77 		getopt(args, std.getopt.config.caseSensitive,
78 				"sloc|l", &sloc,
79 				"highlight", &highlight,
80 				"ctags|c", &ctags,
81 				"help|h", &help,
82 				"etags|e", &etags,
83 				"etagsAll", &etagsAll,
84 				"tokenCount|t", &tokenCount,
85 				"syntaxCheck|s", &syntaxCheck,
86 				"ast|xml", &ast,
87 				"imports|i", &imports,
88 				"recursiveImports", &recursiveImports,
89 				"outline|o", &outline,
90 				"tokenDump", &tokenDump,
91 				"styleCheck|S", &styleCheck,
92 				"defaultConfig", &defaultConfig,
93 				"declaration|d", &symbolName,
94 				"config", &configLocation,
95 				"report", &report,
96 				"reportFormat", &reportFormat,
97 				"reportFile", &reportFile,
98 				"I", &importPaths,
99 				"version", &printVersion,
100 				"muffinButton", &muffin,
101 				"explore", &explore,
102 				"skipTests", &skipTests,
103 				"errorFormat|f", &errorFormat);
104 		//dfmt on
105 	}
106 	catch (ConvException e)
107 	{
108 		stderr.writeln(e.msg);
109 		return 1;
110 	}
111 	catch (GetOptException e)
112 	{
113 		stderr.writeln(e.msg);
114 		return 1;
115 	}
116 
117 	if (muffin)
118 	{
119 		stdout.writeln(`       ___________
120     __(#*O 0** @%*)__
121   _(%*o#*O%*0 #O#%##@)_
122  (*#@%#o*@ #o%O*%@ #o #)
123  \=====================/
124   |I|I|I|I|I|I|I|I|I|I|
125   |I|I|I|I|I|I|I|I|I|I|
126   |I|I|I|I|I|I|I|I|I|I|
127   |I|I|I|I|I|I|I|I|I|I|`);
128 		return 0;
129 	}
130 
131 	if (explore)
132 	{
133 		stdout.writeln("D-Scanner: Scanning...");
134 		stderr.writeln("D-Scanner: No new astronomical objects discovered.");
135 		return 1;
136 	}
137 
138 	if (help)
139 	{
140 		printHelp(args[0]);
141 		return 0;
142 	}
143 
144 	if (printVersion)
145 	{
146 		writeln(DSCANNER_VERSION);
147 		return 0;
148 	}
149 
150 	if (!errorFormat.length)
151 		errorFormat = defaultErrorFormat;
152 
153 	const(string[]) absImportPaths = importPaths.map!(a => a.absolutePath()
154 			.buildNormalizedPath()).array();
155 
156 	auto alloc = scoped!(dsymbol.modulecache.ASTAllocator)();
157 	auto moduleCache = ModuleCache(alloc);
158 
159 	if (absImportPaths.length)
160 		moduleCache.addImportPaths(absImportPaths);
161 
162 	if (reportFormat.length || reportFile.length)
163 		report = true;
164 
165 	immutable optionCount = count!"a"([sloc, highlight, ctags, tokenCount, syntaxCheck, ast, imports,
166 			outline, tokenDump, styleCheck, defaultConfig, report,
167 			symbolName !is null, etags, etagsAll, recursiveImports]);
168 	if (optionCount > 1)
169 	{
170 		stderr.writeln("Too many options specified");
171 		return 1;
172 	}
173 	else if (optionCount < 1)
174 	{
175 		printHelp(args[0]);
176 		return 1;
177 	}
178 
179 	// --report implies --styleCheck
180 	if (report)
181 		styleCheck = true;
182 
183 	immutable usingStdin = args.length == 1;
184 
185 	StringCache cache = StringCache(StringCache.defaultBucketCount);
186 	if (defaultConfig)
187 	{
188 		string s = getConfigurationLocation();
189 		mkdirRecurse(findSplitBefore(s, "dscanner.ini")[0]);
190 		StaticAnalysisConfig saConfig = defaultStaticAnalysisConfig();
191 		writeln("Writing default config file to ", s);
192 		writeINIFile(saConfig, s);
193 	}
194 	else if (tokenDump || highlight)
195 	{
196 		ubyte[] bytes = usingStdin ? readStdin() : readFile(args[1]);
197 		LexerConfig config;
198 		config.stringBehavior = StringBehavior.source;
199 
200 		if (highlight)
201 		{
202 			auto tokens = byToken(bytes, config, &cache);
203 			dscanner.highlighter.highlight(tokens, args.length == 1 ? "stdin" : args[1]);
204 			return 0;
205 		}
206 		else if (tokenDump)
207 		{
208 			auto tokens = getTokensForParser(bytes, config, &cache);
209 			writeln(
210 					"text                    \tblank\tindex\tline\tcolumn\ttype\tcomment\ttrailingComment");
211 			foreach (token; tokens)
212 			{
213 				writefln("<<%20s>>\t%b\t%d\t%d\t%d\t%d\t%s\t%s",
214 						token.text is null ? str(token.type) : token.text,
215 						token.text is null,
216 						token.index,
217 						token.line,
218 						token.column,
219 						token.type,
220 						token.comment,
221 						token.trailingComment);
222 			}
223 			return 0;
224 		}
225 	}
226 	else if (symbolName !is null)
227 	{
228 		stdout.findDeclarationOf(symbolName, expandArgs(args));
229 	}
230 	else if (ctags)
231 	{
232 		stdout.printCtags(expandArgs(args));
233 	}
234 	else if (etags || etagsAll)
235 	{
236 		stdout.printEtags(etagsAll, expandArgs(args));
237 	}
238 	else if (styleCheck)
239 	{
240 		StaticAnalysisConfig config = defaultStaticAnalysisConfig();
241 		string s = configLocation is null ? getConfigurationLocation() : configLocation;
242 		if (s.exists())
243 			readINIFile(config, s);
244 		if (skipTests)
245 			config.enabled2SkipTests;
246 		if (report)
247 		{
248 			switch (reportFormat)
249 			{
250 				default:
251 					stderr.writeln("Unknown report format specified, using dscanner format");
252 					goto case;
253 				case "":
254 				case "dscanner":
255 					generateReport(expandArgs(args), config, cache, moduleCache, reportFile);
256 					break;
257 				case "sonarQubeGenericIssueData":
258 					generateSonarQubeGenericIssueDataReport(expandArgs(args), config, cache, moduleCache, reportFile);
259 					break;
260 			}
261 		}
262 		else
263 			return analyze(expandArgs(args), config, errorFormat, cache, moduleCache, true) ? 1 : 0;
264 	}
265 	else if (syntaxCheck)
266 	{
267 		return .syntaxCheck(usingStdin ? ["stdin"] : expandArgs(args), errorFormat, cache, moduleCache) ? 1 : 0;
268 	}
269 	else
270 	{
271 		if (sloc || tokenCount)
272 		{
273 			if (usingStdin)
274 			{
275 				LexerConfig config;
276 				config.stringBehavior = StringBehavior.source;
277 				auto tokens = byToken(readStdin(), config, &cache);
278 				if (tokenCount)
279 					printTokenCount(stdout, "stdin", tokens);
280 				else
281 					printLineCount(stdout, "stdin", tokens);
282 			}
283 			else
284 			{
285 				ulong count;
286 				foreach (f; expandArgs(args))
287 				{
288 
289 					LexerConfig config;
290 					config.stringBehavior = StringBehavior.source;
291 					auto tokens = byToken(readFile(f), config, &cache);
292 					if (tokenCount)
293 						count += printTokenCount(stdout, f, tokens);
294 					else
295 						count += printLineCount(stdout, f, tokens);
296 				}
297 				writefln("total:\t%d", count);
298 			}
299 		}
300 		else if (imports || recursiveImports)
301 		{
302 			printImports(usingStdin, args, importPaths, &cache, recursiveImports);
303 		}
304 		else if (ast || outline)
305 		{
306 			string fileName = usingStdin ? "stdin" : args[1];
307 			RollbackAllocator rba;
308 			LexerConfig config;
309 			config.fileName = fileName;
310 			config.stringBehavior = StringBehavior.source;
311 			auto tokens = getTokensForParser(usingStdin ? readStdin()
312 					: readFile(args[1]), config, &cache);
313 			auto mod = parseModule(tokens, fileName, &rba, toDelegate(&doNothing));
314 
315 			if (ast)
316 			{
317 				auto printer = new XMLPrinter;
318 				printer.output = stdout;
319 				printer.visit(mod);
320 			}
321 			else if (outline)
322 			{
323 				auto outliner = new Outliner(stdout);
324 				outliner.visit(mod);
325 			}
326 		}
327 	}
328 	return 0;
329 }
330 
331 void printHelp(string programName)
332 {
333 	stderr.writefln(`
334     Usage: %s <options>
335 
336 Options:
337     --help, -h
338         Prints this help message
339 
340     --version
341         Prints the program version
342 
343     --sloc <file | directory>..., -l <file | directory>...
344         Prints the number of logical lines of code in the given
345         source files. If no files are specified, input is read from stdin.
346 
347     --tokenCount <file | directory>..., -t <file | directory>...
348         Prints the number of tokens in the given source files. If no files are
349         specified, input is read from stdin.
350 
351     --tokenDump <file>
352         Dump token information from the lexer. This option is mostly useful for
353         developing D-Scanner or its supporting libraries itself. You probably
354         dont't want to use this, and this feature may be removed in the future.
355 
356     --highlight <file>
357         Syntax-highlight the given source file. The resulting HTML will be
358         written to standard output. If no file is specified, input is read
359         from stdin.
360 
361     --imports <file>, -i <file>
362         Prints modules imported by the given source file. If no files are
363         specified, input is read from stdin. Combine with "-I" arguments to
364         resolve import locations.
365 
366     --recursiveImports <file>
367         Similar to "--imports", but lists imports of imports recursively.
368 
369     -I <directory>
370         Specify that the given directory should be searched for imported
371         modules. This option can be passed multiple times to specify multiple
372         directories.
373 
374     --syntaxCheck <file>, -s <file>
375         Lexes and parses sourceFile, printing the line and column number of
376         any syntax errors to stdout. One error or warning is printed per line,
377         and formatted according to the pattern passed to "--errorFormat".
378         If no files are specified, input is read from stdin. %1$s will exit
379         with a status code of zero if no errors are found, 1 otherwise.
380 
381     --styleCheck|S <file | directory>..., <file | directory>...
382         Lexes and parses sourceFiles, printing the line and column number of
383         any static analysis check failures stdout. One error or warning is
384         printed per line, and formatted according to the pattern passed to
385         "--errorFormat". %1$s will exit with a status code of zero if no
386         warnings or errors are found, 1 otherwise.
387 
388     --errorFormat|f <pattern>
389         Format errors produced by the style/syntax checkers. The default
390         value for the pattern is: "%2$s".
391         Supported placeholders are: {filepath}, {line}, {column}, {type},
392         {message}, and {name}.
393 
394     --ctags <file | directory>..., -c <file | directory>...
395         Generates ctags information from the given source code file. Note that
396         ctags information requires a filename, so stdin cannot be used in place
397         of a filename.
398 
399     --etags <file | directory>..., -e <file | directory>...
400         Generates etags information from the given source code file. Note that
401         etags information requires a filename, so stdin cannot be used in place
402         of a filename.
403 
404     --etagsAll <file | directory>...
405         Same as --etags except private and package declarations are tagged too.
406 
407     --ast <file> | --xml <file>
408         Generates an XML representation of the source files abstract syntax
409         tree. If no files are specified, input is read from stdin.
410 
411     --declaration <symbolName> <file | directory>...,
412     -d <symbolName> <file | directory>...
413         Find the location where symbolName is declared. This should be more
414         accurate than "grep". Searches the given files and directories, or the
415         current working directory if none are specified.
416 
417     --report <file | directory>...
418         Generate a static analysis report in JSON format. Implies --styleCheck,
419         however the exit code will still be zero if errors or warnings are
420         found.
421 
422     --reportFile <file>
423         Write report into file instead of STDOUT.
424 
425     --reportFormat <dscanner | sonarQubeGenericIssueData>...
426         Specifies the format of the generated report.
427 
428     --config <file>
429         Use the given configuration file instead of the default located in
430         $HOME/.config/dscanner/dscanner.ini
431 
432     --defaultConfig
433         Generates a default configuration file for the static analysis checks,
434 
435     --skipTests
436         Does not analyze code in unittests. Only works if --styleCheck
437         is specified.`,
438 
439     programName, defaultErrorFormat);
440 }
441 
442 private void doNothing(string, size_t, size_t, string, bool)
443 {
444 }
445 
446 private enum CONFIG_FILE_NAME = "dscanner.ini";
447 version (linux) version = useXDG;
448 version (BSD) version = useXDG;
449 version (FreeBSD) version = useXDG;
450 version (OSX) version = useXDG;
451 
452 /**
453  * Locates the default configuration file
454  */
455 string getDefaultConfigurationLocation()
456 {
457 	import std.process : environment;
458 	import std.exception : enforce;
459 	version (useXDG)
460 	{
461 		string configDir = environment.get("XDG_CONFIG_HOME", null);
462 		if (configDir is null)
463 		{
464 			configDir = environment.get("HOME", null);
465 			enforce(configDir !is null, "Both $XDG_CONFIG_HOME and $HOME are unset");
466 			configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME);
467 		}
468 		else
469 			configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
470 		return configDir;
471 	}
472 	else version(Windows)
473 	{
474 		string configDir = environment.get("APPDATA", null);
475 		enforce(configDir !is null, "%APPDATA% is unset");
476 		configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
477 		return configDir;
478 	}
479 }
480 
481 /**
482  * Searches upwards from the CWD through the directory hierarchy
483  */
484 string tryFindConfigurationLocation()
485 {
486 	auto path = pathSplitter(getcwd());
487 	string result;
488 
489 	while (!path.empty)
490 	{
491 		result = buildPath(buildPath(path), CONFIG_FILE_NAME);
492 
493 		if (exists(result))
494 			break;
495 
496 		path.popBack();
497 	}
498 
499 	if (path.empty)
500 		return null;
501 
502 	return result;
503 }
504 
505 /**
506  * Tries to find a config file and returns the default one on failure
507  */
508 string getConfigurationLocation()
509 {
510 	immutable config = tryFindConfigurationLocation();
511 
512 	if (config !is null)
513 		return config;
514 
515 	return getDefaultConfigurationLocation();
516 }