1 /** 2 Copyright: Copyright (c) 2016-2017, Joakim Brännström. All rights reserved. 3 License: MPL-2 4 Author: Joakim Brännström (joakim.brannstrom@gmx.com) 5 6 This Source Code Form is subject to the terms of the Mozilla Public License, 7 v.2.0. If a copy of the MPL was not distributed with this file, You can obtain 8 one at http://mozilla.org/MPL/2.0/. 9 10 Utility functions for Clang Compilation Databases. 11 12 # Usage 13 Call the function `fromArgCompileDb` to create one, merged database. 14 15 Extract flags the flags for a file by calling `appendOrError`. 16 17 Example: 18 --- 19 auto dbs = fromArgCompileDb(["foo.json]); 20 auto flags = dbs.appendOrError(dbs, null, "foo.cpp", defaultCompilerFlagFilter); 21 --- 22 */ 23 module dextool.compilation_db; 24 25 import logger = std.experimental.logger; 26 import std.algorithm : map, filter, splitter, joiner; 27 import std.array : empty, array, appender; 28 import std.exception : collectException; 29 import std.json : JSONValue; 30 import std.path : buildPath; 31 import std.typecons : Nullable; 32 33 import dextool.type : AbsolutePath, Path; 34 35 public import dextool.compilation_db.user_filerange; 36 public import dextool.compilation_db.system_compiler : deduceSystemIncludes, 37 SystemIncludePath, Compiler; 38 39 version (unittest) { 40 import std.path : buildPath; 41 import unit_threaded : shouldEqual; 42 } 43 44 @safe: 45 46 /** Hold an entry from the compilation database. 47 * 48 * The following information is from the official specification. 49 * $(LINK2 http://clang.llvm.org/docs/JSONCompilationDatabase.html, Standard) 50 * 51 * directory: The working directory of the compilation. All paths specified in 52 * the command or file fields must be either absolute or relative to this 53 * directory. 54 * 55 * file: The main translation unit source processed by this compilation step. 56 * This is used by tools as the key into the compilation database. There can be 57 * multiple command objects for the same file, for example if the same source 58 * file is compiled with different configurations. 59 * 60 * command: The compile command executed. After JSON unescaping, this must be a 61 * valid command to rerun the exact compilation step for the translation unit 62 * in the environment the build system uses. Parameters use shell quoting and 63 * shell escaping of quotes, with ‘"‘ and ‘\‘ being the only special 64 * characters. Shell expansion is not supported. 65 * 66 * argumets: The compile command executed as list of strings. Either arguments 67 * or command is required. 68 * 69 * output: The name of the output created by this compilation step. This field 70 * is optional. It can be used to distinguish different processing modes of the 71 * same input file. 72 * 73 * Dextool additions. 74 * The standard do not specify how to treat "directory" when it is a relative 75 * path. The logic chosen in dextool is to treat it as relative to the path 76 * the compilation database file is read from. 77 */ 78 struct CompileCommand { 79 import dextool.type : Path, AbsolutePath; 80 81 /// The raw command from the tuples "command" or "arguments value. 82 static struct Command { 83 string[] payload; 84 alias payload this; 85 bool hasValue() @safe pure nothrow const @nogc { 86 return payload.length != 0; 87 } 88 } 89 90 /// File that where compiled. 91 Path file; 92 /// ditto. 93 AbsolutePath absoluteFile; 94 /// Working directory of the command that compiled the input. 95 AbsolutePath directory; 96 /// The executing command when compiling. 97 Command command; 98 /// The resulting object file. 99 Path output; 100 /// ditto. 101 AbsolutePath absoluteOutput; 102 } 103 104 /// The path to the compilation database. 105 struct CompileDbFile { 106 Path payload; 107 alias payload this; 108 109 this(string p) @safe nothrow { 110 payload = Path(p); 111 } 112 } 113 114 /// The absolute path to the directory the compilation database reside at. 115 struct AbsoluteCompileDbDirectory { 116 AbsolutePath payload; 117 alias payload this; 118 119 this(Path path) { 120 import std.path : dirName; 121 122 payload = AbsolutePath(path.dirName.Path); 123 } 124 } 125 126 /// A complete compilation database. 127 struct CompileCommandDB { 128 CompileCommand[] payload; 129 alias payload this; 130 131 bool empty() @safe pure nothrow const @nogc { 132 return payload.empty; 133 } 134 } 135 136 // The result of searching for a file in a compilation DB. 137 // The file may be occur more than one time therefor an array. 138 struct CompileCommandSearch { 139 CompileCommand[] payload; 140 alias payload this; 141 142 bool empty() @safe pure nothrow const @nogc { 143 return payload.empty; 144 } 145 } 146 147 /** 148 * Trusted: opIndex for JSONValue is @safe in DMD-2.077.0 149 * remove the trusted attribute when the minimal requirement is upgraded. 150 */ 151 private Nullable!CompileCommand toCompileCommand(JSONValue v, AbsoluteCompileDbDirectory db_dir) nothrow @trusted { 152 import std.exception : assumeUnique; 153 import std.range : only; 154 import std.utf : byUTF; 155 156 static if (__VERSION__ < 2085L) { 157 import std.json : JSON_TYPE; 158 159 alias JSONType = JSON_TYPE; 160 alias JSONType_array = JSON_TYPE.ARRAY; 161 alias JSONType_string = JSON_TYPE.STRING; 162 } else { 163 import std.json : JSONType; 164 165 alias JSONType_array = JSONType.array; 166 alias JSONType_string = JSONType..string; 167 } 168 169 string[] command = () { 170 string[] cmd; 171 try { 172 cmd = v["command"].str.splitter.filter!(a => a.length != 0).array; 173 } catch (Exception ex) { 174 } 175 176 // prefer command over arguments if both are present because of bugs in 177 // tools that produce compile_commands.json. 178 if (cmd.length != 0) 179 return cmd; 180 181 try { 182 enum j_arg = "arguments"; 183 const auto j_type = v[j_arg].type; 184 if (j_type == JSONType_string) 185 cmd = v[j_arg].str.splitter.filter!(a => a.length != 0).array; 186 else if (j_type == JSONType_array) { 187 import std.range; 188 189 cmd = v[j_arg].arrayNoRef 190 .filter!(a => a.type == JSONType_string) 191 .map!(a => a.str) 192 .filter!(a => a.length != 0) 193 .array; 194 } 195 } catch (Exception ex) { 196 } 197 198 return cmd; 199 }(); 200 201 if (command.length == 0) { 202 logger.error("Unable to parse the JSON tuple. Both command and arguments are empty") 203 .collectException; 204 return typeof(return)(); 205 } 206 207 string output; 208 try { 209 output = v["output"].str; 210 } catch (Exception ex) { 211 } 212 213 try { 214 const directory = v["directory"]; 215 const file = v["file"]; 216 217 foreach (a; only(directory, file).map!(a => !a.isNull && a.type == JSONType_string) 218 .filter!(a => !a)) { 219 // sanity check. 220 // if any element is false then break early. 221 return typeof(return)(); 222 } 223 224 return toCompileCommand(directory.str, file.str, command, db_dir, output); 225 } catch (Exception e) { 226 logger.info("Input JSON: ", v.toPrettyString).collectException; 227 logger.error("Unable to parse json: ", e.msg).collectException; 228 } 229 230 return typeof(return)(); 231 } 232 233 /** Transform a json entry to a CompileCommand. 234 * 235 * This function is under no circumstances meant to be exposed outside this module. 236 * The API is badly designed for common use because it relies on the position 237 * order of the strings for their meaning. 238 */ 239 Nullable!CompileCommand toCompileCommand(string directory, string file, 240 string[] command, AbsoluteCompileDbDirectory db_dir, string output) nothrow { 241 // expects that v is a tuple of 3 json values with the keys directory, 242 // command, file 243 244 Nullable!CompileCommand rval; 245 246 try { 247 auto abs_workdir = AbsolutePath(buildPath(db_dir, directory.Path)); 248 auto abs_file = AbsolutePath(buildPath(abs_workdir, file.Path)); 249 auto abs_output = AbsolutePath(buildPath(abs_workdir, output.Path)); 250 // dfmt off 251 rval = CompileCommand( 252 Path(file), 253 abs_file, 254 abs_workdir, 255 CompileCommand.Command(command), 256 Path(output), 257 abs_output); 258 // dfmt on 259 } catch (Exception ex) { 260 logger.error("Unable to parse json: ", ex.msg).collectException; 261 } 262 263 return rval; 264 } 265 266 /** Parse a CompilationDatabase. 267 * 268 * Params: 269 * raw_input = the content of the CompilationDatabase. 270 * db = path to the compilation database file. 271 * out_range = range to write the output to. 272 */ 273 private void parseCommands(T)(string raw_input, CompileDbFile db, ref T out_range) nothrow { 274 import std.json : parseJSON, JSONException; 275 276 static void put(T)(JSONValue v, AbsoluteCompileDbDirectory dbdir, ref T out_range) nothrow { 277 278 try { 279 // dfmt off 280 foreach (e; v.array() 281 // map the JSON tuples to D structs 282 .map!(a => toCompileCommand(a, dbdir)) 283 // remove invalid 284 .filter!(a => !a.isNull) 285 .map!(a => a.get)) { 286 out_range.put(e); 287 } 288 // dfmt on 289 } catch (Exception ex) { 290 logger.error("Unable to parse json:", ex.msg).collectException; 291 } 292 } 293 294 try { 295 // trusted: is@safe in DMD-2.077.0 296 // remove the trusted attribute when the minimal requirement is upgraded. 297 auto json = () @trusted { return parseJSON(raw_input); }(); 298 auto as_dir = AbsoluteCompileDbDirectory(db.AbsolutePath); 299 300 // trusted: this function is private so the only user of it is this module. 301 // the only problem would be in the out_range. It is assumed that the 302 // out_range takes care of the validation and other security aspects. 303 () @trusted { put(json, as_dir, out_range); }(); 304 } catch (Exception ex) { 305 logger.error("Error while parsing compilation database: " ~ ex.msg).collectException; 306 } 307 } 308 309 void fromFile(T)(CompileDbFile filename, ref T app) { 310 import std.file : readText; 311 312 auto raw = readText(filename); 313 if (raw.length == 0) 314 logger.warning("File is empty: ", filename); 315 316 raw.parseCommands(filename, app); 317 } 318 319 void fromFiles(T)(CompileDbFile[] fnames, ref T app) { 320 import std.file : exists; 321 322 foreach (f; fnames) { 323 if (!exists(f)) 324 throw new Exception("File do not exist: " ~ f); 325 f.fromFile(app); 326 } 327 } 328 329 /** Return default path if argument is null. 330 */ 331 CompileDbFile[] orDefaultDb(string[] cli_path) @safe nothrow { 332 if (cli_path.length == 0) { 333 return [CompileDbFile("compile_commands.json")]; 334 } 335 336 return cli_path.map!(a => CompileDbFile(a)).array(); 337 } 338 339 /** Find a best matching compile_command in the database against the path 340 * pattern `glob`. 341 * 342 * When searching for the compile command for a file, the compilation db can 343 * return several commands, as the file may have been compiled with different 344 * options in different parts of the project. 345 * 346 * Params: 347 * glob = glob pattern to find a matching file in the DB against 348 */ 349 CompileCommandSearch find(CompileCommandDB db, string glob) @safe { 350 foreach (a; db.filter!(a => isMatch(a, glob))) { 351 return CompileCommandSearch([a]); 352 } 353 return CompileCommandSearch.init; 354 } 355 356 /** Check if `glob` fuzzy matches `a`. 357 */ 358 bool isMatch(CompileCommand a, string glob) { 359 import std.path : globMatch; 360 361 if (a.absoluteFile == glob) 362 return true; 363 else if (a.file == glob) 364 return true; 365 else if (globMatch(a.absoluteFile, glob)) 366 return true; 367 else if (a.absoluteOutput == glob) 368 return true; 369 else if (a.output == glob) 370 return true; 371 else if (globMatch(a.absoluteOutput, glob)) 372 return true; 373 return false; 374 } 375 376 string toString(CompileCommand[] db) @safe pure { 377 import std.conv : text; 378 import std.format : formattedWrite; 379 380 auto app = appender!string(); 381 382 foreach (a; db) { 383 formattedWrite(app, "%s\n %s\n %s\n", a.directory, a.file, a.absoluteFile); 384 385 if (!a.output.empty) { 386 formattedWrite(app, " %s\n", a.output); 387 formattedWrite(app, " %s\n", a.absoluteOutput); 388 } 389 390 if (!a.command.empty) 391 formattedWrite(app, " %-(%s %)\n", a.command); 392 } 393 394 return app.data; 395 } 396 397 string toString(CompileCommandDB db) @safe pure { 398 return toString(db.payload); 399 } 400 401 string toString(CompileCommandSearch search) @safe pure { 402 return toString(search.payload); 403 } 404 405 CompileCommandFilter defaultCompilerFilter() { 406 return CompileCommandFilter(defaultCompilerFlagFilter, 0); 407 } 408 409 /// Returns: array of default flags to exclude. 410 auto defaultCompilerFlagFilter() @safe { 411 auto app = appender!(FilterClangFlag[])(); 412 413 // dfmt off 414 foreach (f; [ 415 // remove basic compile flag irrelevant for AST generation 416 "-c", "-o", 417 // machine dependent flags 418 "-m", 419 // machine dependent flags, AVR 420 "-nodevicelib", "-Waddr-space-convert", 421 // machine dependent flags, VxWorks 422 "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now", 423 // blacklist all -f because most aren not compatible with clang 424 "-f", 425 // linker flags, irrelevant for the AST 426 "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker", 427 // a linker flag with filename as one argument 428 "-l", 429 // remove some of the preprocessor flags, irrelevant for the AST 430 "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###", 431 ]) { 432 app.put(FilterClangFlag(f)); 433 } 434 // dfmt on 435 436 return app.data; 437 } 438 439 struct CompileCommandFilter { 440 FilterClangFlag[] filter; 441 int skipCompilerArgs = 0; 442 } 443 444 /// Parsed compiler flags. 445 struct ParseFlags { 446 /// The includes used in the compile command 447 static struct Include { 448 string payload; 449 alias payload this; 450 } 451 452 private { 453 bool forceSystemIncludes_; 454 } 455 456 /// The includes used in the compile command. 457 Include[] includes; 458 459 /// System include paths extracted from the compiler used for the file. 460 SystemIncludePath[] systemIncludes; 461 462 /// Specific flags for the file as parsed from the DB. 463 string[] cflags; 464 465 /// Compiler used to compile the item. 466 Compiler compiler; 467 468 void prependCflags(string[] v) { 469 this.cflags = v ~ this.cflags; 470 } 471 472 void appendCflags(string[] v) { 473 this.cflags ~= v; 474 } 475 476 /// Set to true to use -I instead of -isystem for system includes. 477 auto forceSystemIncludes(bool v) { 478 this.forceSystemIncludes_ = v; 479 return this; 480 } 481 482 bool hasSystemIncludes() @safe pure nothrow const @nogc { 483 return systemIncludes.length != 0; 484 } 485 486 string toString() @safe pure const { 487 import std.format : format; 488 489 return format("Compiler:%s flags: %-(%s %)", compiler, completeFlags); 490 } 491 492 /** Easy to use method that has the complete flags ready to use with a GCC 493 * complient compiler. 494 * 495 * This method assumes that -isystem is how to add system flags. 496 * 497 * Returns: flags with the system flags appended. 498 */ 499 string[] completeFlags() @safe pure nothrow const { 500 auto incl_param = forceSystemIncludes_ ? "-I" : "-isystem"; 501 502 return cflags.idup ~ systemIncludes.map!(a => [incl_param, a.value]).joiner.array; 503 } 504 505 alias completeFlags this; 506 507 this(Include[] incls, string[] flags) { 508 this(Compiler.init, incls, SystemIncludePath[].init, flags); 509 } 510 511 this(Compiler compiler, Include[] incls, string[] flags) { 512 this(compiler, incls, null, flags); 513 } 514 515 this(Compiler compiler, Include[] incls, SystemIncludePath[] sysincls, string[] flags) { 516 this.compiler = compiler; 517 this.includes = incls; 518 this.systemIncludes = sysincls; 519 this.cflags = flags; 520 } 521 } 522 523 /** Filter and normalize the compiler flags. 524 * 525 * - Sanitize the compiler command by removing flags matching the filter. 526 * - Remove excess white space. 527 * - Convert all filenames to absolute path. 528 */ 529 ParseFlags parseFlag(CompileCommand cmd, const CompileCommandFilter flag_filter) @safe { 530 import std.algorithm : among, strip, startsWith, count; 531 import std.string : empty, split; 532 533 static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe { 534 // the purpuse is to find if any of the flags in flag_filter matches 535 // the start of flag. 536 537 bool delegate(const FilterClangFlag) @safe cmp; 538 539 const parts = raw_flag.split('='); 540 if (parts.length == 2) { 541 // is a -foo=bar flag thus exact match is the only sensible 542 cmp = (const FilterClangFlag a) => raw_flag == a.payload; 543 } else { 544 // the flag has the argument merged thus have to check if the start match 545 cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload); 546 } 547 548 // dfmt off 549 return 0 != flag_filter 550 .filter!(a => a.kind == FilterClangFlag.Kind.exclude) 551 // keep flags that are at least the length of values 552 .filter!(a => raw_flag.length >= a.length) 553 // if the flag is any of those in filter 554 .filter!cmp 555 .count(); 556 // dfmt on 557 } 558 559 static bool isQuotationMark(char c) @safe { 560 return c == '"'; 561 } 562 563 static bool isBackslash(char c) @safe { 564 return c == '\\'; 565 } 566 567 static bool isInclude(string flag) @safe { 568 return flag.length >= 2 && flag[0 .. 2] == "-I"; 569 } 570 571 static bool isCombinedIncludeFlag(string flag) @safe { 572 // if an include flag make it absolute, as one argument by checking 573 // length. 3 is to only match those that are -Ixyz 574 return flag.length >= 3 && isInclude(flag); 575 } 576 577 static bool isNotAFlag(string flag) @safe { 578 // good enough if it seem to be a file 579 return flag.length >= 1 && flag[0] != '-'; 580 } 581 582 /// Flags that take an argument that is a path that need to be transformed 583 /// to an absolute path. 584 static bool isFlagAndPath(string flag) @safe { 585 // list derived from clang --help 586 return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros", "-include-pch", 587 "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem", "--sysroot"); 588 } 589 590 /// Flags that take an argument that is NOT a path. 591 static bool isFlagAndValue(string flag) @safe { 592 return 0 != flag.among("-D"); 593 } 594 595 /// Flags that are includes, but contains spaces, are wrapped in quotation marks (or slash). 596 static bool isIncludeWithQuotationMark(string flag) @safe { 597 // length is checked in isCombinedIncludeFlag 598 return isCombinedIncludeFlag(flag) && (isQuotationMark(flag[2]) || isBackslash(flag[2])); 599 } 600 601 /// Flags that are paths and contain spaces will start with a quotation mark (or slash). 602 static bool isStartingWithQuotationMark(string flag) @safe { 603 return !flag.empty && (isQuotationMark(flag[0]) || isBackslash(flag[0])); 604 } 605 606 /// When we know we are building a path that is space separated, 607 /// the last index of the last string should be a quotation mark. 608 static bool isEndingWithQuotationMark(string flag) @safe { 609 return !flag.empty && isQuotationMark(flag[$ - 1]); 610 } 611 612 static ParseFlags filterPair(string[] r, AbsolutePath workdir, 613 const FilterClangFlag[] flag_filter) @safe { 614 enum State { 615 /// keep the next flag IF none of the other transitions happens 616 keep, 617 /// forcefully keep the next argument as raw data 618 priorityKeepNextArg, 619 /// keep the next argument and transform to an absolute path 620 pathArgumentToAbsolute, 621 /// skip the next arg 622 skip, 623 /// skip the next arg, if it is not a flag 624 skipIfNotFlag, 625 /// use the next arg to create a complete path 626 checkingForEndQuotation, 627 } 628 629 import std.array : Appender, join; 630 import std.range : ElementType; 631 632 auto st = State.keep; 633 auto rval = appender!(string[]); 634 auto includes = appender!(string[]); 635 auto compiler = Compiler(r.length == 0 ? null : r[0]); 636 auto path = appender!(char[])(); 637 638 string removeBackslashesAndQuotes(string arg) { 639 import std.conv : text; 640 import std.uni : byCodePoint, byGrapheme, Grapheme; 641 642 return arg.byGrapheme.filter!(a => !a.among(Grapheme('\\'), 643 Grapheme('"'))).byCodePoint.text; 644 } 645 646 void putNormalizedAbsolute(string arg) { 647 import std.path : buildNormalizedPath, absolutePath; 648 649 auto p = buildNormalizedPath(workdir, removeBackslashesAndQuotes(arg)).absolutePath; 650 rval.put(p); 651 includes.put(p); 652 } 653 654 foreach (arg; r) { 655 // First states and how to handle those. 656 // Then transitions from the state keep, which is the default state. 657 // 658 // The user controlled excludeStartWith must be before any other 659 // conditions after the states. It is to give the user the ability 660 // to filter out any flag. 661 662 if (st == State.skip) { 663 st = State.keep; 664 } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) { 665 st = State.keep; 666 } else if (st == State.pathArgumentToAbsolute) { 667 if (isStartingWithQuotationMark(arg)) { 668 if (isEndingWithQuotationMark(arg)) { 669 st = State.keep; 670 putNormalizedAbsolute(arg); 671 } else { 672 st = State.checkingForEndQuotation; 673 path.put(arg); 674 } 675 } else { 676 st = State.keep; 677 putNormalizedAbsolute(arg); 678 } 679 } else if (st == State.priorityKeepNextArg) { 680 st = State.keep; 681 rval.put(arg); 682 } else if (st == State.checkingForEndQuotation) { 683 path.put(" "); 684 path.put(arg); 685 if (isEndingWithQuotationMark(arg)) { 686 // the end of a divided path 687 st = State.keep; 688 putNormalizedAbsolute(path.data.idup); 689 path.clear; 690 } 691 } else if (excludeStartWith(arg, flag_filter)) { 692 st = State.skipIfNotFlag; 693 } else if (isIncludeWithQuotationMark(arg)) { 694 rval.put("-I"); 695 if (arg.length >= 4) { 696 if (isEndingWithQuotationMark(arg)) { 697 // the path is wrapped in quotes (ex ['-I"path/to src"'] or ['-I\"path/to src\"']) 698 putNormalizedAbsolute(arg[2 .. $]); 699 } else { 700 // the path is divided (ex ['-I"path/to', 'src"'] or ['-I\"path/to', 'src\"']) 701 st = State.checkingForEndQuotation; 702 path.put(arg[2 .. $]); 703 } 704 } 705 } else if (isCombinedIncludeFlag(arg)) { 706 rval.put("-I"); 707 putNormalizedAbsolute(arg[2 .. $]); 708 } else if (isFlagAndPath(arg)) { 709 rval.put(arg); 710 st = State.pathArgumentToAbsolute; 711 } else if (isFlagAndValue(arg)) { 712 rval.put(arg); 713 st = State.priorityKeepNextArg; 714 } // parameter that seem to be filenames, remove 715 else if (isNotAFlag(arg)) { 716 // skipping 717 } else { 718 rval.put(arg); 719 } 720 } 721 return ParseFlags(compiler, includes.data.map!(a => ParseFlags.Include(a)).array, rval.data); 722 } 723 724 import std.algorithm : min; 725 726 string[] skipArgs = () @safe { 727 string[] args; 728 if (cmd.command.hasValue) 729 args = cmd.command.payload.dup; 730 if (args.length > flag_filter.skipCompilerArgs && flag_filter.skipCompilerArgs != 0) 731 args = args[min(flag_filter.skipCompilerArgs, args.length) .. $]; 732 return args; 733 }(); 734 735 auto pargs = filterPair(skipArgs, cmd.directory, flag_filter.filter); 736 737 return ParseFlags(pargs.compiler, pargs.includes, null, pargs.cflags); 738 } 739 740 /** Convert the string to a CompileCommandDB. 741 * 742 * Params: 743 * path = changes relative paths to be relative this parameter 744 * data = input to convert 745 */ 746 CompileCommandDB toCompileCommandDB(string data, Path path) @safe { 747 auto app = appender!(CompileCommand[])(); 748 data.parseCommands(CompileDbFile(cast(string) path), app); 749 return CompileCommandDB(app.data); 750 } 751 752 CompileCommandDB fromArgCompileDb(AbsolutePath[] paths) @safe { 753 return fromArgCompileDb(paths.map!(a => cast(string) a).array); 754 } 755 756 /// Import and merge many compilation databases into one DB. 757 CompileCommandDB fromArgCompileDb(string[] paths) @safe { 758 auto app = appender!(CompileCommand[])(); 759 paths.orDefaultDb.fromFiles(app); 760 761 return CompileCommandDB(app.data); 762 } 763 764 /// Flags to exclude from the flags passed on to the clang parser. 765 struct FilterClangFlag { 766 string payload; 767 alias payload this; 768 769 enum Kind { 770 exclude 771 } 772 773 Kind kind; 774 } 775 776 @("Should be cflags with all unnecessary flags removed") 777 unittest { 778 auto cmd = toCompileCommand("/home", "file1.cpp", [ 779 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c", 780 "a_filename.c" 781 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 782 auto s = cmd.get.parseFlag(defaultCompilerFilter); 783 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 784 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 785 } 786 787 @("Should be cflags with some excess spacing") 788 unittest { 789 auto cmd = toCompileCommand("/home", "file1.cpp", [ 790 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun" 791 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 792 793 auto s = cmd.get.parseFlag(defaultCompilerFilter); 794 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 795 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 796 } 797 798 @("Should be cflags with machine dependent removed") 799 unittest { 800 auto cmd = toCompileCommand("/home", "file1.cpp", [ 801 "g++", "-mfoo", "-m", "bar", "-MD", "-lfoo.a", "-l", "bar.a", "-I", 802 "bar", "-Igun", "-c", "a_filename.c" 803 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 804 805 auto s = cmd.get.parseFlag(defaultCompilerFilter); 806 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 807 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 808 } 809 810 @("Should be cflags with all -f removed") 811 unittest { 812 auto cmd = toCompileCommand("/home", "file1.cpp", [ 813 "g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun", "-flolol", 814 "-c", "a_filename.c" 815 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 816 817 auto s = cmd.get.parseFlag(defaultCompilerFilter); 818 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 819 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 820 } 821 822 @("shall NOT remove -std=xyz flags") 823 unittest { 824 auto cmd = toCompileCommand("/home", "file1.cpp", [ 825 "g++", "-std=c++11", "-c", "a_filename.c" 826 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 827 828 auto s = cmd.get.parseFlag(defaultCompilerFilter); 829 s.cflags.shouldEqual(["-std=c++11"]); 830 } 831 832 @("shall remove -mfloat-gprs=double") 833 unittest { 834 auto cmd = toCompileCommand("/home", "file1.cpp", [ 835 "g++", "-std=c++11", "-mfloat-gprs=double", "-c", "a_filename.c" 836 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 837 auto my_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 838 my_filter.filter ~= FilterClangFlag("-mfloat-gprs=double", FilterClangFlag.Kind.exclude); 839 auto s = cmd.get.parseFlag(my_filter); 840 s.cflags.shouldEqual(["-std=c++11"]); 841 } 842 843 @("Shall keep all compiler flags as they are") 844 unittest { 845 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D", 846 "b"], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 847 848 auto s = cmd.get.parseFlag(defaultCompilerFilter); 849 s.cflags.shouldEqual(["-Da", "-D", "b"]); 850 } 851 852 version (unittest) { 853 import std.file : getcwd; 854 import std.path : absolutePath; 855 import std.format : format; 856 857 // contains a bit of extra junk that is expected to be removed 858 immutable string dummy_path = "/path/to/../to/./db/compilation_db.json"; 859 immutable string dummy_dir = "/path/to/db"; 860 861 enum raw_dummy1 = `[ 862 { 863 "directory": "dir1/dir2", 864 "command": "g++ -Idir1 -c -o binary file1.cpp", 865 "file": "file1.cpp" 866 } 867 ]`; 868 869 enum raw_dummy2 = `[ 870 { 871 "directory": "dir", 872 "command": "g++ -Idir1 -c -o binary file1.cpp", 873 "file": "file1.cpp" 874 }, 875 { 876 "directory": "dir", 877 "command": "g++ -Idir1 -c -o binary file2.cpp", 878 "file": "file2.cpp" 879 } 880 ]`; 881 882 enum raw_dummy3 = `[ 883 { 884 "directory": "dir1", 885 "command": "g++ -Idir1 -c -o binary file3.cpp", 886 "file": "file3.cpp" 887 }, 888 { 889 "directory": "dir2", 890 "command": "g++ -Idir1 -c -o binary file3.cpp", 891 "file": "file3.cpp" 892 } 893 ]`; 894 895 enum raw_dummy4 = `[ 896 { 897 "directory": "dir1", 898 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 899 "file": "file3.cpp", 900 "output": "file3.o" 901 }, 902 { 903 "directory": "dir2", 904 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 905 "file": "file3.cpp", 906 "output": "file3.o" 907 } 908 ]`; 909 910 enum raw_dummy5 = `[ 911 { 912 "directory": "dir1", 913 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 914 "file": "file3.cpp", 915 "output": "file3.o" 916 }, 917 { 918 "directory": "dir2", 919 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 920 "file": "file3.cpp", 921 "output": "file3.o" 922 } 923 ]`; 924 } 925 926 @("Should be a compile command DB") 927 unittest { 928 auto app = appender!(CompileCommand[])(); 929 raw_dummy1.parseCommands(CompileDbFile(dummy_path), app); 930 auto cmds = app.data; 931 932 assert(cmds.length == 1); 933 (cast(string) cmds[0].directory).shouldEqual(dummy_dir ~ "/dir1/dir2"); 934 cmds[0].command.shouldEqual([ 935 "g++", "-Idir1", "-c", "-o", "binary", "file1.cpp" 936 ]); 937 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 938 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp"); 939 } 940 941 @("Should be a DB with two entries") 942 unittest { 943 auto app = appender!(CompileCommand[])(); 944 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 945 auto cmds = app.data; 946 947 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 948 (cast(string) cmds[1].file).shouldEqual("file2.cpp"); 949 } 950 951 @("Should find filename") 952 unittest { 953 auto app = appender!(CompileCommand[])(); 954 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 955 auto cmds = CompileCommandDB(app.data); 956 957 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 958 assert(found.length == 1); 959 (cast(string) found[0].file).shouldEqual("file2.cpp"); 960 } 961 962 @("Should find no match by using an absolute path that doesn't exist in DB") 963 unittest { 964 auto app = appender!(CompileCommand[])(); 965 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 966 auto cmds = CompileCommandDB(app.data); 967 968 auto found = cmds.find("./file2.cpp"); 969 assert(found.length == 0); 970 } 971 972 @("Should find one match by using the absolute filename to disambiguous") 973 unittest { 974 auto app = appender!(CompileCommand[])(); 975 raw_dummy3.parseCommands(CompileDbFile(dummy_path), app); 976 auto cmds = CompileCommandDB(app.data); 977 978 auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp"); 979 assert(found.length == 1); 980 981 found.toString.shouldEqual(format("%s/dir2 982 file3.cpp 983 %s/dir2/file3.cpp 984 g++ -Idir1 -c -o binary file3.cpp 985 ", dummy_dir, dummy_dir)); 986 } 987 988 @("Should be a pretty printed search result") 989 unittest { 990 auto app = appender!(CompileCommand[])(); 991 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 992 auto cmds = CompileCommandDB(app.data); 993 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 994 995 found.toString.shouldEqual(format("%s/dir 996 file2.cpp 997 %s/dir/file2.cpp 998 g++ -Idir1 -c -o binary file2.cpp 999 ", dummy_dir, dummy_dir)); 1000 } 1001 1002 @("Should be a compile command DB with relative path") 1003 unittest { 1004 enum raw = `[ 1005 { 1006 "directory": ".", 1007 "command": "g++ -Idir1 -c -o binary file1.cpp", 1008 "file": "file1.cpp" 1009 } 1010 ]`; 1011 auto app = appender!(CompileCommand[])(); 1012 raw.parseCommands(CompileDbFile(dummy_path), app); 1013 auto cmds = app.data; 1014 1015 assert(cmds.length == 1); 1016 (cast(string) cmds[0].directory).shouldEqual(dummy_dir); 1017 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 1018 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/file1.cpp"); 1019 } 1020 1021 @("Should be a DB read from a relative path with the contained paths adjusted appropriately") 1022 unittest { 1023 auto app = appender!(CompileCommand[])(); 1024 raw_dummy3.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1025 auto cmds = CompileCommandDB(app.data); 1026 1027 // trusted: constructing a path in memory which is never used for writing. 1028 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1029 1030 auto found = cmds.find(abs_path ~ "/dir2/file3.cpp"); 1031 assert(found.length == 1); 1032 1033 found.toString.shouldEqual(format("%s/dir2 1034 file3.cpp 1035 %s/dir2/file3.cpp 1036 g++ -Idir1 -c -o binary file3.cpp 1037 ", abs_path, abs_path)); 1038 } 1039 1040 @("shall extract arguments, file, directory and output with absolute paths") 1041 unittest { 1042 auto app = appender!(CompileCommand[])(); 1043 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1044 auto cmds = CompileCommandDB(app.data); 1045 1046 // trusted: constructing a path in memory which is never used for writing. 1047 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1048 1049 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1050 assert(found.length == 1); 1051 1052 found.toString.shouldEqual(format("%s/dir2 1053 file3.cpp 1054 %s/dir2/file3.cpp 1055 file3.o 1056 %s/dir2/file3.o 1057 g++ -Idir1 -c -o binary file3.cpp 1058 ", abs_path, abs_path, abs_path)); 1059 } 1060 1061 @("shall be the compiler flags derived from the arguments attribute") 1062 unittest { 1063 auto app = appender!(CompileCommand[])(); 1064 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1065 auto cmds = CompileCommandDB(app.data); 1066 1067 // trusted: constructing a path in memory which is never used for writing. 1068 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1069 1070 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1071 assert(found.length == 1); 1072 1073 found[0].parseFlag(defaultCompilerFilter).cflags.shouldEqual([ 1074 "-I", buildPath(abs_path, "dir2", "dir1") 1075 ]); 1076 } 1077 1078 @("shall find the entry based on an output match") 1079 unittest { 1080 auto app = appender!(CompileCommand[])(); 1081 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1082 auto cmds = CompileCommandDB(app.data); 1083 1084 // trusted: constructing a path in memory which is never used for writing. 1085 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1086 1087 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1088 assert(found.length == 1); 1089 1090 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1091 } 1092 1093 @("shall parse the compilation database when *arguments* is a json list") 1094 unittest { 1095 auto app = appender!(CompileCommand[])(); 1096 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1097 auto cmds = CompileCommandDB(app.data); 1098 1099 // trusted: constructing a path in memory which is never used for writing. 1100 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1101 1102 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1103 assert(found.length == 1); 1104 1105 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1106 } 1107 1108 @("shall parse the compilation database and find a match via the glob pattern") 1109 unittest { 1110 import std.path : baseName; 1111 1112 auto app = appender!(CompileCommand[])(); 1113 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1114 auto cmds = CompileCommandDB(app.data); 1115 1116 auto found = cmds.find("*/dir2/file3.cpp"); 1117 assert(found.length == 1); 1118 1119 found[0].absoluteFile.baseName.shouldEqual("file3.cpp"); 1120 } 1121 1122 @("shall extract filepath from includes correctly when there is spaces in the path") 1123 unittest { 1124 auto cmd = toCompileCommand("/home", "file.cpp", [ 1125 "-I", `"dir with spaces"`, "-I", `\"dir with spaces\"` 1126 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 1127 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1128 pargs.cflags.shouldEqual([ 1129 "-I", "/home/dir with spaces", "-I", "/home/dir with spaces" 1130 ]); 1131 pargs.includes.shouldEqual([ 1132 "/home/dir with spaces", "/home/dir with spaces" 1133 ]); 1134 } 1135 1136 @("shall handle path with spaces, both as separate string and combined with backslash") 1137 unittest { 1138 auto cmd = toCompileCommand("/project", "file.cpp", [ 1139 "-I", `"separate dir/with space"`, "-I", `\"separate dir/with space\"`, 1140 `-I"combined dir/with space"`, `-I\"combined dir/with space\"`, 1141 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1142 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1143 pargs.cflags.shouldEqual([ 1144 "-I", "/project/separate dir/with space", "-I", 1145 "/project/separate dir/with space", "-I", 1146 "/project/combined dir/with space", "-I", 1147 "/project/combined dir/with space" 1148 ]); 1149 pargs.includes.shouldEqual([ 1150 "/project/separate dir/with space", "/project/separate dir/with space", 1151 "/project/combined dir/with space", "/project/combined dir/with space" 1152 ]); 1153 } 1154 1155 @("shall handle path with consecutive spaces") 1156 unittest { 1157 auto cmd = toCompileCommand("/project", "file.cpp", 1158 [ 1159 `-I"one space/lots of space"`, 1160 `-I\"one space/lots of space\"`, `-I`, 1161 `"one space/lots of space"`, `-I`, 1162 `\"one space/lots of space\"`, 1163 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1164 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1165 pargs.cflags.shouldEqual([ 1166 "-I", "/project/one space/lots of space", "-I", 1167 "/project/one space/lots of space", "-I", 1168 "/project/one space/lots of space", "-I", 1169 "/project/one space/lots of space", 1170 ]); 1171 pargs.includes.shouldEqual([ 1172 "/project/one space/lots of space", 1173 "/project/one space/lots of space", 1174 "/project/one space/lots of space", 1175 "/project/one space/lots of space" 1176 ]); 1177 }