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