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 }