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