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.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 	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 
108 	if (muffin)
109 	{
110 		stdout.writeln(`       ___________
111     __(#*O 0** @%*)__
112   _(%*o#*O%*0 #O#%##@)_
113  (*#@%#o*@ #o%O*%@ #o #)
114  \=====================/
115   |I|I|I|I|I|I|I|I|I|I|
116   |I|I|I|I|I|I|I|I|I|I|
117   |I|I|I|I|I|I|I|I|I|I|
118   |I|I|I|I|I|I|I|I|I|I|`);
119 		return 0;
120 	}
121 
122 	if (explore)
123 	{
124 		stdout.writeln("D-Scanner: Scanning...");
125 		stderr.writeln("D-Scanner: No new astronomical objects discovered.");
126 		return 1;
127 	}
128 
129 	if (help)
130 	{
131 		printHelp(args[0]);
132 		return 0;
133 	}
134 
135 	if (printVersion)
136 	{
137 		version (Windows)
138 			writeln(DSCANNER_VERSION);
139 		else version (built_with_dub)
140 			writeln(DSCANNER_VERSION);
141 		else
142 			write(DSCANNER_VERSION, " ", GIT_HASH);
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 	version (useXDG)
411 	{
412 		import std.process : environment;
413 
414 		string configDir = environment.get("XDG_CONFIG_HOME", null);
415 		if (configDir is null)
416 		{
417 			configDir = environment.get("HOME", null);
418 			if (configDir is null)
419 				throw new Exception("Both $XDG_CONFIG_HOME and $HOME are unset");
420 			configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME);
421 		}
422 		else
423 			configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
424 		return configDir;
425 	}
426 	else version (Windows)
427 		return CONFIG_FILE_NAME;
428 }
429 
430 /**
431  * Searches upwards from the CWD through the directory hierarchy
432  */
433 string tryFindConfigurationLocation()
434 {
435 	auto path = pathSplitter(getcwd());
436 	string result;
437 
438 	while (!path.empty)
439 	{
440 		result = buildPath(buildPath(path), CONFIG_FILE_NAME);
441 
442 		if (exists(result))
443 			break;
444 
445 		path.popBack();
446 	}
447 
448 	if (path.empty)
449 		return null;
450 
451 	return result;
452 }
453 
454 /**
455  * Tries to find a config file and returns the default one on failure
456  */
457 string getConfigurationLocation()
458 {
459 	immutable config = tryFindConfigurationLocation();
460 
461 	if (config !is null)
462 		return config;
463 
464 	return getDefaultConfigurationLocation();
465 }