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