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