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 writeln(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 345 any syntax errors to stdout. One error or warning is printed per line, 346 and formatted according to the pattern passed to "--errorFormat". 347 If no files are specified, input is read from stdin. %1$s will exit 348 with a status code of zero if no errors are found, 1 otherwise. 349 350 --styleCheck|S <file | directory>..., <file | directory>... 351 Lexes and parses sourceFiles, printing the line and column number of 352 any static analysis check failures stdout. One error or warning is 353 printed per line, and formatted according to the pattern passed to 354 "--errorFormat". %1$s will exit with a status code of zero if no 355 warnings or errors are found, 1 otherwise. 356 357 --errorFormat|f <pattern> 358 Format errors produced by the style/syntax checkers. The default 359 value for the pattern is: "%2$s". 360 361 --ctags <file | directory>..., -c <file | directory>... 362 Generates ctags information from the given source code file. Note that 363 ctags information requires a filename, so stdin cannot be used in place 364 of a filename. 365 366 --etags <file | directory>..., -e <file | directory>... 367 Generates etags information from the given source code file. Note that 368 etags information requires a filename, so stdin cannot be used in place 369 of a filename. 370 371 --etagsAll <file | directory>... 372 Same as --etags except private and package declarations are tagged too. 373 374 --ast <file> | --xml <file> 375 Generates an XML representation of the source files abstract syntax 376 tree. If no files are specified, input is read from stdin. 377 378 --declaration <symbolName> <file | directory>..., 379 -d <symbolName> <file | directory>... 380 Find the location where symbolName is declared. This should be more 381 accurate than "grep". Searches the given files and directories, or the 382 current working directory if none are specified. 383 384 --report <file | directory>... 385 Generate a static analysis report in JSON format. Implies --styleCheck, 386 however the exit code will still be zero if errors or warnings are 387 found. 388 389 --config <file> 390 Use the given configuration file instead of the default located in 391 $HOME/.config/dscanner/dscanner.ini 392 393 --defaultConfig 394 Generates a default configuration file for the static analysis checks, 395 396 --skipTests 397 Does not analyze in the unittests. Only works if --styleCheck.`, 398 399 programName, defaultErrorFormat); 400 } 401 402 private void doNothing(string, size_t, size_t, string, bool) 403 { 404 } 405 406 private enum CONFIG_FILE_NAME = "dscanner.ini"; 407 version (linux) version = useXDG; 408 version (BSD) version = useXDG; 409 version (FreeBSD) version = useXDG; 410 version (OSX) version = useXDG; 411 412 /** 413 * Locates the default configuration file 414 */ 415 string getDefaultConfigurationLocation() 416 { 417 import std.process : environment; 418 import std.exception : enforce; 419 version (useXDG) 420 { 421 string configDir = environment.get("XDG_CONFIG_HOME", null); 422 if (configDir is null) 423 { 424 configDir = environment.get("HOME", null); 425 enforce(configDir !is null, "Both $XDG_CONFIG_HOME and $HOME are unset"); 426 configDir = buildPath(configDir, ".config", "dscanner", CONFIG_FILE_NAME); 427 } 428 else 429 configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME); 430 return configDir; 431 } 432 else version(Windows) 433 { 434 string configDir = environment.get("APPDATA", null); 435 enforce(configDir !is null, "%APPDATA% is unset"); 436 configDir = buildPath(configDir, "dscanner", CONFIG_FILE_NAME); 437 return configDir; 438 } 439 } 440 441 /** 442 * Searches upwards from the CWD through the directory hierarchy 443 */ 444 string tryFindConfigurationLocation() 445 { 446 auto path = pathSplitter(getcwd()); 447 string result; 448 449 while (!path.empty) 450 { 451 result = buildPath(buildPath(path), CONFIG_FILE_NAME); 452 453 if (exists(result)) 454 break; 455 456 path.popBack(); 457 } 458 459 if (path.empty) 460 return null; 461 462 return result; 463 } 464 465 /** 466 * Tries to find a config file and returns the default one on failure 467 */ 468 string getConfigurationLocation() 469 { 470 immutable config = tryFindConfigurationLocation(); 471 472 if (config !is null) 473 return config; 474 475 return getDefaultConfigurationLocation(); 476 }