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