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 }