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