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 		writeln(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
345         any syntax errors to stdout. One error or warning is printed per line,
346         and formatted according to the pattern passed to "--errorFormat".
347         If no files are specified, input is read from stdin. %1$s will exit
348         with a status code of zero if no errors are found, 1 otherwise.
349 
350     --styleCheck|S <file | directory>..., <file | directory>...
351         Lexes and parses sourceFiles, printing the line and column number of
352         any static analysis check failures stdout. One error or warning is
353         printed per line, and formatted according to the pattern passed to
354         "--errorFormat". %1$s will exit with a status code of zero if no
355         warnings or errors are found, 1 otherwise.
356 
357     --errorFormat|f <pattern>
358         Format errors produced by the style/syntax checkers. The default
359         value for the pattern is: "%2$s".
360 
361     --ctags <file | directory>..., -c <file | directory>...
362         Generates ctags information from the given source code file. Note that
363         ctags information requires a filename, so stdin cannot be used in place
364         of a filename.
365 
366     --etags <file | directory>..., -e <file | directory>...
367         Generates etags information from the given source code file. Note that
368         etags information requires a filename, so stdin cannot be used in place
369         of a filename.
370 
371     --etagsAll <file | directory>...
372         Same as --etags except private and package declarations are tagged too.
373 
374     --ast <file> | --xml <file>
375         Generates an XML representation of the source files abstract syntax
376         tree. If no files are specified, input is read from stdin.
377 
378     --declaration <symbolName> <file | directory>...,
379 	-d <symbolName> <file | directory>...
380         Find the location where symbolName is declared. This should be more
381         accurate than "grep". Searches the given files and directories, or the
382         current working directory if none are specified.
383 
384     --report <file | directory>...
385         Generate a static analysis report in JSON format. Implies --styleCheck,
386         however the exit code will still be zero if errors or warnings are
387         found.
388 
389     --config <file>
390         Use the given configuration file instead of the default located in
391         $HOME/.config/dscanner/dscanner.ini
392 
393     --defaultConfig
394         Generates a default configuration file for the static analysis checks,
395 
396     --skipTests
397         Does not analyze in the unittests. Only works if --styleCheck.`,
398 
399     programName, defaultErrorFormat);
400 }
401 
402 private void doNothing(string, size_t, size_t, string, bool)
403 {
404 }
405 
406 private enum CONFIG_FILE_NAME = "dscanner.ini";
407 version (linux) version = useXDG;
408 version (BSD) version = useXDG;
409 version (FreeBSD) version = useXDG;
410 version (OSX) version = useXDG;
411 
412 /**
413  * Locates the default configuration file
414  */
415 string getDefaultConfigurationLocation()
416 {
417 	import std.process : environment;
418 	import std.exception : enforce;
419 	version (useXDG)
420 	{
421 		string configDir = environment.get("XDG_CONFIG_HOME", null);
422 		if (configDir is null)
423 		{
424 			configDir = environment.get("HOME", null);
425 			enforce(configDir !is null, "Both $XDG_CONFIG_HOME and $HOME are unset");
426 			configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME);
427 		}
428 		else
429 			configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
430 		return configDir;
431 	}
432 	else version(Windows)
433 	{
434 		string configDir = environment.get("APPDATA", null);
435 		enforce(configDir !is null, "%APPDATA% is unset");
436 		configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME);
437 		return configDir;
438 	}
439 }
440 
441 /**
442  * Searches upwards from the CWD through the directory hierarchy
443  */
444 string tryFindConfigurationLocation()
445 {
446 	auto path = pathSplitter(getcwd());
447 	string result;
448 
449 	while (!path.empty)
450 	{
451 		result = buildPath(buildPath(path), CONFIG_FILE_NAME);
452 
453 		if (exists(result))
454 			break;
455 
456 		path.popBack();
457 	}
458 
459 	if (path.empty)
460 		return null;
461 
462 	return result;
463 }
464 
465 /**
466  * Tries to find a config file and returns the default one on failure
467  */
468 string getConfigurationLocation()
469 {
470 	immutable config = tryFindConfigurationLocation();
471 
472 	if (config !is null)
473 		return config;
474 
475 	return getDefaultConfigurationLocation();
476 }