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