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