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                    blank\tindex\tline\tcolumn\ttype\tcomment\ttrailingComment");
198 			foreach (token; tokens)
199 			{
200 				writefln("<<%20s>>%b\t%d\t%d\t%d\t%d\t%s", token.text is null
201 						? str(token.type) : token.text, token.text is null, token.index,
202 						token.line, token.column, token.type, token.comment);
203 			}
204 			return 0;
205 		}
206 	}
207 	else if (symbolName !is null)
208 	{
209 		stdout.findDeclarationOf(symbolName, expandArgs(args));
210 	}
211 	else if (ctags)
212 	{
213 		stdout.printCtags(expandArgs(args));
214 	}
215 	else if (etags || etagsAll)
216 	{
217 		stdout.printEtags(etagsAll, expandArgs(args));
218 	}
219 	else if (styleCheck)
220 	{
221 		StaticAnalysisConfig config = defaultStaticAnalysisConfig();
222 		string s = configLocation is null ? getConfigurationLocation() : configLocation;
223 		if (s.exists())
224 			readINIFile(config, s);
225         if (skipTests)
226             config.fillConfig!(Check.skipTests);
227 		if (report)
228 			generateReport(expandArgs(args), config, cache, moduleCache);
229 		else
230 			return analyze(expandArgs(args), config, cache, moduleCache, true) ? 1 : 0;
231 	}
232 	else if (syntaxCheck)
233 	{
234 		return .syntaxCheck(usingStdin ? ["stdin"] : expandArgs(args), cache, moduleCache) ? 1 : 0;
235 	}
236 	else
237 	{
238 		if (sloc || tokenCount)
239 		{
240 			if (usingStdin)
241 			{
242 				LexerConfig config;
243 				config.stringBehavior = StringBehavior.source;
244 				auto tokens = byToken(readStdin(), config, &cache);
245 				if (tokenCount)
246 					printTokenCount(stdout, "stdin", tokens);
247 				else
248 					printLineCount(stdout, "stdin", tokens);
249 			}
250 			else
251 			{
252 				ulong count;
253 				foreach (f; expandArgs(args))
254 				{
255 
256 					LexerConfig config;
257 					config.stringBehavior = StringBehavior.source;
258 					auto tokens = byToken(readFile(f), config, &cache);
259 					if (tokenCount)
260 						count += printTokenCount(stdout, f, tokens);
261 					else
262 						count += printLineCount(stdout, f, tokens);
263 				}
264 				writefln("total:\t%d", count);
265 			}
266 		}
267 		else if (imports || recursiveImports)
268 		{
269 			printImports(usingStdin, args, importPaths, &cache, recursiveImports);
270 		}
271 		else if (ast || outline)
272 		{
273 			string fileName = usingStdin ? "stdin" : args[1];
274 			RollbackAllocator rba;
275 			LexerConfig config;
276 			config.fileName = fileName;
277 			config.stringBehavior = StringBehavior.source;
278 			auto tokens = getTokensForParser(usingStdin ? readStdin()
279 					: readFile(args[1]), config, &cache);
280 			auto mod = parseModule(tokens, fileName, &rba, &doNothing);
281 
282 			if (ast)
283 			{
284 				auto printer = new XMLPrinter;
285 				printer.output = stdout;
286 				printer.visit(mod);
287 			}
288 			else if (outline)
289 			{
290 				auto outliner = new Outliner(stdout);
291 				outliner.visit(mod);
292 			}
293 		}
294 	}
295 	return 0;
296 }
297 
298 void printHelp(string programName)
299 {
300 	stderr.writefln(`
301     Usage: %s <options>
302 
303 Options:
304     --help, -h
305         Prints this help message
306 
307     --version
308         Prints the program version
309 
310     --sloc <file | directory>..., -l <file | directory>...
311         Prints the number of logical lines of code in the given
312         source files. If no files are specified, input is read from stdin.
313 
314     --tokenCount <file | directory>..., -t <file | directory>...
315         Prints the number of tokens in the given source files. If no files are
316         specified, input is read from stdin.
317 
318     --highlight <file>
319         Syntax-highlight the given source file. The resulting HTML will be
320         written to standard output. If no file is specified, input is read
321         from stdin.
322 
323     --imports <file>, -i <file>
324         Prints modules imported by the given source file. If no files are
325         specified, input is read from stdin. Combine with "-I" arguments to
326         resolve import locations.
327 
328     --recursive-imports <file>
329         Similar to "--imports", but lists imports of imports recursively.
330 
331     --syntaxCheck <file>, -s <file>
332         Lexes and parses sourceFile, printing the line and column number of any
333         syntax errors to stdout. One error or warning is printed per line.
334         If no files are specified, input is read from stdin. %1$s will exit with
335         a status code of zero if no errors are found, 1 otherwise.
336 
337     --styleCheck|S <file | directory>..., <file | directory>...
338         Lexes and parses sourceFiles, printing the line and column number of any
339         static analysis check failures stdout. %1$s will exit with a status code
340         of zero if no warnings or errors are found, 1 otherwise.
341 
342     --ctags <file | directory>..., -c <file | directory>...
343         Generates ctags information from the given source code file. Note that
344         ctags information requires a filename, so stdin cannot be used in place
345         of a filename.
346 
347     --etags <file | directory>..., -e <file | directory>...
348         Generates etags information from the given source code file. Note that
349         etags information requires a filename, so stdin cannot be used in place
350         of a filename.
351 
352     --etagsAll <file | directory>...
353         Same as --etags except private and package declarations are tagged too.
354 
355     --ast <file> | --xml <file>
356         Generates an XML representation of the source files abstract syntax
357         tree. If no files are specified, input is read from stdin.
358 
359     --declaration <symbolName> <file | directory>...,
360 	-d <symbolName> <file | directory>...
361         Find the location where symbolName is declared. This should be more
362         accurate than "grep". Searches the given files and directories, or the
363         current working directory if none are specified.
364 
365     --report <file | directory>...
366         Generate a static analysis report in JSON format. Implies --styleCheck,
367         however the exit code will still be zero if errors or warnings are
368         found.
369 
370     --config <file>
371         Use the given configuration file instead of the default located in
372         $HOME/.config/dscanner/dscanner.ini
373 
374     --defaultConfig
375         Generates a default configuration file for the static analysis checks,
376 
377     --skipTests
378         Does not analyze in the unittests. Only works if --styleCheck.`,
379 
380     programName);
381 }
382 
383 private void doNothing(string, size_t, size_t, string, bool)
384 {
385 }
386 
387 private enum CONFIG_FILE_NAME = "dscanner.ini";
388 version (linux) version = useXDG;
389 version (BSD) version = useXDG;
390 version (FreeBSD) version = useXDG;
391 version (OSX) version = useXDG;
392 
393 /**
394  * Locates the default configuration file
395  */
396 string getDefaultConfigurationLocation()
397 {
398 	version (useXDG)
399 	{
400 		import std.process : environment;
401 
402 		string configDir = environment.get("XDG_CONFIG_HOME", null);
403 		if (configDir is null)
404 		{
405 			configDir = environment.get("HOME", null);
406 			if (configDir is null)
407 				throw new Exception("Both $XDG_CONFIG_HOME and $HOME are unset");
408 			configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME);
409 		}
410 		else
411 			configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
412 		return configDir;
413 	}
414 	else version (Windows)
415 		return CONFIG_FILE_NAME;
416 }
417 
418 /**
419  * Searches upwards from the CWD through the directory hierarchy
420  */
421 string tryFindConfigurationLocation()
422 {
423 	auto path = pathSplitter(getcwd());
424 	string result;
425 
426 	while (!path.empty)
427 	{
428 		result = buildPath(buildPath(path), CONFIG_FILE_NAME);
429 
430 		if (exists(result))
431 			break;
432 
433 		path.popBack();
434 	}
435 
436 	if (path.empty)
437 		return null;
438 
439 	return result;
440 }
441 
442 /**
443  * Tries to find a config file and returns the default one on failure
444  */
445 string getConfigurationLocation()
446 {
447 	immutable config = tryFindConfigurationLocation();
448 
449 	if (config !is null)
450 		return config;
451 
452 	return getDefaultConfigurationLocation();
453 }