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