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