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