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