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.absoluteFile == AbsolutePath(glob)) 364 return true; 365 else if (a.file == glob) 366 return true; 367 else if (globMatch(a.absoluteFile, glob)) 368 return true; 369 else if (a.absoluteOutput == glob) 370 return true; 371 else if (a.output == glob) 372 return true; 373 else if (globMatch(a.absoluteOutput, glob)) 374 return true; 375 return false; 376 } 377 378 string toString(CompileCommand[] db) @safe pure { 379 import std.conv : text; 380 import std.format : formattedWrite; 381 382 auto app = appender!string(); 383 384 foreach (a; db) { 385 formattedWrite(app, "%s\n %s\n %s\n", a.directory, a.file, a.absoluteFile); 386 387 if (!a.output.empty) { 388 formattedWrite(app, " %s\n", a.output); 389 formattedWrite(app, " %s\n", a.absoluteOutput); 390 } 391 392 if (!a.command.empty) 393 formattedWrite(app, " %-(%s %)\n", a.command); 394 } 395 396 return app.data; 397 } 398 399 string toString(CompileCommandDB db) @safe pure { 400 return toString(db.payload); 401 } 402 403 string toString(CompileCommandSearch search) @safe pure { 404 return toString(search.payload); 405 } 406 407 CompileCommandFilter defaultCompilerFilter() { 408 return CompileCommandFilter(defaultCompilerFlagFilter, 0); 409 } 410 411 /// Returns: array of default flags to exclude. 412 auto defaultCompilerFlagFilter() @safe { 413 auto app = appender!(FilterClangFlag[])(); 414 415 // dfmt off 416 foreach (f; [ 417 // remove basic compile flag irrelevant for AST generation 418 "-c", "-o", 419 // machine dependent flags 420 "-m", 421 // machine dependent flags, AVR 422 "-nodevicelib", "-Waddr-space-convert", 423 // machine dependent flags, VxWorks 424 "-non-static", "-Bstatic", "-Bdynamic", "-Xbind-lazy", "-Xbind-now", 425 // blacklist all -f because most aren not compatible with clang 426 "-f", 427 // linker flags, irrelevant for the AST 428 "-static", "-shared", "-rdynamic", "-s", "-l", "-L", "-z", "-u", "-T", "-Xlinker", 429 // a linker flag with filename as one argument 430 "-l", 431 // remove some of the preprocessor flags, irrelevant for the AST 432 "-MT", "-MF", "-MD", "-MQ", "-MMD", "-MP", "-MG", "-E", "-cc1", "-S", "-M", "-MM", "-###", 433 ]) { 434 app.put(FilterClangFlag(f)); 435 } 436 // dfmt on 437 438 return app.data; 439 } 440 441 struct CompileCommandFilter { 442 FilterClangFlag[] filter; 443 int skipCompilerArgs = 0; 444 } 445 446 /// Parsed compiler flags. 447 struct ParseFlags { 448 /// The includes used in the compile command 449 static struct Include { 450 string payload; 451 alias payload this; 452 } 453 454 private { 455 bool forceSystemIncludes_; 456 } 457 458 /// The includes used in the compile command. 459 Include[] includes; 460 461 /// System include paths extracted from the compiler used for the file. 462 SystemIncludePath[] systemIncludes; 463 464 /// Specific flags for the file as parsed from the DB. 465 string[] cflags; 466 467 /// Compiler used to compile the item. 468 Compiler compiler; 469 470 void prependCflags(string[] v) { 471 this.cflags = v ~ this.cflags; 472 } 473 474 void appendCflags(string[] v) { 475 this.cflags ~= v; 476 } 477 478 /// Set to true to use -I instead of -isystem for system includes. 479 auto forceSystemIncludes(bool v) { 480 this.forceSystemIncludes_ = v; 481 return this; 482 } 483 484 bool hasSystemIncludes() @safe pure nothrow const @nogc { 485 return systemIncludes.length != 0; 486 } 487 488 string toString() @safe pure const { 489 import std.format : format; 490 491 return format("Compiler:%s flags: %-(%s %)", compiler, completeFlags); 492 } 493 494 /** Easy to use method that has the complete flags ready to use with a GCC 495 * complient compiler. 496 * 497 * This method assumes that -isystem is how to add system flags. 498 * 499 * Returns: flags with the system flags appended. 500 */ 501 string[] completeFlags() @safe pure nothrow const { 502 auto incl_param = forceSystemIncludes_ ? "-I" : "-isystem"; 503 504 return cflags.idup ~ systemIncludes.map!(a => [incl_param, a.value]).joiner.array; 505 } 506 507 alias completeFlags this; 508 509 this(Include[] incls, string[] flags) { 510 this(Compiler.init, incls, SystemIncludePath[].init, flags); 511 } 512 513 this(Compiler compiler, Include[] incls, string[] flags) { 514 this(compiler, incls, null, flags); 515 } 516 517 this(Compiler compiler, Include[] incls, SystemIncludePath[] sysincls, string[] flags) { 518 this.compiler = compiler; 519 this.includes = incls; 520 this.systemIncludes = sysincls; 521 this.cflags = flags; 522 } 523 } 524 525 /** Filter and normalize the compiler flags. 526 * 527 * - Sanitize the compiler command by removing flags matching the filter. 528 * - Remove excess white space. 529 * - Convert all filenames to absolute path. 530 */ 531 ParseFlags parseFlag(CompileCommand cmd, const CompileCommandFilter flag_filter) @safe { 532 import std.algorithm : among, strip, startsWith, count; 533 import std..string : empty, split; 534 535 static bool excludeStartWith(const string raw_flag, const FilterClangFlag[] flag_filter) @safe { 536 // the purpuse is to find if any of the flags in flag_filter matches 537 // the start of flag. 538 539 bool delegate(const FilterClangFlag) @safe cmp; 540 541 const parts = raw_flag.split('='); 542 if (parts.length == 2) { 543 // is a -foo=bar flag thus exact match is the only sensible 544 cmp = (const FilterClangFlag a) => raw_flag == a.payload; 545 } else { 546 // the flag has the argument merged thus have to check if the start match 547 cmp = (const FilterClangFlag a) => raw_flag.startsWith(a.payload); 548 } 549 550 // dfmt off 551 return 0 != flag_filter 552 .filter!(a => a.kind == FilterClangFlag.Kind.exclude) 553 // keep flags that are at least the length of values 554 .filter!(a => raw_flag.length >= a.length) 555 // if the flag is any of those in filter 556 .filter!cmp 557 .count(); 558 // dfmt on 559 } 560 561 static bool isQuotationMark(char c) @safe { 562 return c == '"'; 563 } 564 565 static bool isBackslash(char c) @safe { 566 return c == '\\'; 567 } 568 569 static bool isInclude(string flag) @safe { 570 return flag.length >= 2 && flag[0 .. 2] == "-I"; 571 } 572 573 static bool isCombinedIncludeFlag(string flag) @safe { 574 // if an include flag make it absolute, as one argument by checking 575 // length. 3 is to only match those that are -Ixyz 576 return flag.length >= 3 && isInclude(flag); 577 } 578 579 static bool isNotAFlag(string flag) @safe { 580 // good enough if it seem to be a file 581 return flag.length >= 1 && flag[0] != '-'; 582 } 583 584 /// Flags that take an argument that is a path that need to be transformed 585 /// to an absolute path. 586 static bool isFlagAndPath(string flag) @safe { 587 // list derived from clang --help 588 return 0 != flag.among("-I", "-idirafter", "-iframework", "-imacros", "-include-pch", 589 "-include", "-iquote", "-isysroot", "-isystem-after", "-isystem", "--sysroot"); 590 } 591 592 /// Flags that take an argument that is NOT a path. 593 static bool isFlagAndValue(string flag) @safe { 594 return 0 != flag.among("-D"); 595 } 596 597 /// Flags that are includes, but contains spaces, are wrapped in quotation marks (or slash). 598 static bool isIncludeWithQuotationMark(string flag) @safe { 599 // length is checked in isCombinedIncludeFlag 600 return isCombinedIncludeFlag(flag) && (isQuotationMark(flag[2]) || isBackslash(flag[2])); 601 } 602 603 /// Flags that are paths and contain spaces will start with a quotation mark (or slash). 604 static bool isStartingWithQuotationMark(string flag) @safe { 605 return !flag.empty && (isQuotationMark(flag[0]) || isBackslash(flag[0])); 606 } 607 608 /// When we know we are building a path that is space separated, 609 /// the last index of the last string should be a quotation mark. 610 static bool isEndingWithQuotationMark(string flag) @safe { 611 return !flag.empty && isQuotationMark(flag[$ - 1]); 612 } 613 614 static ParseFlags filterPair(string[] r, AbsolutePath workdir, 615 const FilterClangFlag[] flag_filter) @safe { 616 enum State { 617 /// keep the next flag IF none of the other transitions happens 618 keep, 619 /// forcefully keep the next argument as raw data 620 priorityKeepNextArg, 621 /// keep the next argument and transform to an absolute path 622 pathArgumentToAbsolute, 623 /// skip the next arg 624 skip, 625 /// skip the next arg, if it is not a flag 626 skipIfNotFlag, 627 /// use the next arg to create a complete path 628 checkingForEndQuotation, 629 } 630 631 import std.array : Appender, join; 632 import std.range : ElementType; 633 634 auto st = State.keep; 635 auto rval = appender!(string[]); 636 auto includes = appender!(string[]); 637 auto compiler = Compiler(r.length == 0 ? null : r[0]); 638 auto path = appender!(char[])(); 639 640 string removeBackslashesAndQuotes(string arg) { 641 import std.conv : text; 642 import std.uni : byCodePoint, byGrapheme, Grapheme; 643 644 return arg.byGrapheme.filter!(a => !a.among(Grapheme('\\'), 645 Grapheme('"'))).byCodePoint.text; 646 } 647 648 void putNormalizedAbsolute(string arg) { 649 import std.path : buildNormalizedPath, absolutePath; 650 651 auto p = buildNormalizedPath(workdir, removeBackslashesAndQuotes(arg)).absolutePath; 652 rval.put(p); 653 includes.put(p); 654 } 655 656 foreach (arg; r) { 657 // First states and how to handle those. 658 // Then transitions from the state keep, which is the default state. 659 // 660 // The user controlled excludeStartWith must be before any other 661 // conditions after the states. It is to give the user the ability 662 // to filter out any flag. 663 664 if (st == State.skip) { 665 st = State.keep; 666 } else if (st == State.skipIfNotFlag && isNotAFlag(arg)) { 667 st = State.keep; 668 } else if (st == State.pathArgumentToAbsolute) { 669 if (isStartingWithQuotationMark(arg)) { 670 if (isEndingWithQuotationMark(arg)) { 671 st = State.keep; 672 putNormalizedAbsolute(arg); 673 } else { 674 st = State.checkingForEndQuotation; 675 path.put(arg); 676 } 677 } else { 678 st = State.keep; 679 putNormalizedAbsolute(arg); 680 } 681 } else if (st == State.priorityKeepNextArg) { 682 st = State.keep; 683 rval.put(arg); 684 } else if (st == State.checkingForEndQuotation) { 685 path.put(" "); 686 path.put(arg); 687 if (isEndingWithQuotationMark(arg)) { 688 // the end of a divided path 689 st = State.keep; 690 putNormalizedAbsolute(path.data.idup); 691 path.clear; 692 } 693 } else if (excludeStartWith(arg, flag_filter)) { 694 st = State.skipIfNotFlag; 695 } else if (isIncludeWithQuotationMark(arg)) { 696 rval.put("-I"); 697 if (arg.length >= 4) { 698 if (isEndingWithQuotationMark(arg)) { 699 // the path is wrapped in quotes (ex ['-I"path/to src"'] or ['-I\"path/to src\"']) 700 putNormalizedAbsolute(arg[2 .. $]); 701 } else { 702 // the path is divided (ex ['-I"path/to', 'src"'] or ['-I\"path/to', 'src\"']) 703 st = State.checkingForEndQuotation; 704 path.put(arg[2 .. $]); 705 } 706 } 707 } else if (isCombinedIncludeFlag(arg)) { 708 rval.put("-I"); 709 putNormalizedAbsolute(arg[2 .. $]); 710 } else if (isFlagAndPath(arg)) { 711 rval.put(arg); 712 st = State.pathArgumentToAbsolute; 713 } else if (isFlagAndValue(arg)) { 714 rval.put(arg); 715 st = State.priorityKeepNextArg; 716 } // parameter that seem to be filenames, remove 717 else if (isNotAFlag(arg)) { 718 // skipping 719 } else { 720 rval.put(arg); 721 } 722 } 723 return ParseFlags(compiler, includes.data.map!(a => ParseFlags.Include(a)).array, rval.data); 724 } 725 726 import std.algorithm : min; 727 728 string[] skipArgs = () @safe { 729 string[] args; 730 if (cmd.command.hasValue) 731 args = cmd.command.payload.dup; 732 if (args.length > flag_filter.skipCompilerArgs && flag_filter.skipCompilerArgs != 0) 733 args = args[min(flag_filter.skipCompilerArgs, args.length) .. $]; 734 return args; 735 }(); 736 737 auto pargs = filterPair(skipArgs, cmd.directory, flag_filter.filter); 738 739 return ParseFlags(pargs.compiler, pargs.includes, null, pargs.cflags); 740 } 741 742 /** Convert the string to a CompileCommandDB. 743 * 744 * Params: 745 * path = changes relative paths to be relative this parameter 746 * data = input to convert 747 */ 748 CompileCommandDB toCompileCommandDB(string data, Path path) @safe { 749 auto app = appender!(CompileCommand[])(); 750 data.parseCommands(CompileDbFile(cast(string) path), app); 751 return CompileCommandDB(app.data); 752 } 753 754 CompileCommandDB fromArgCompileDb(AbsolutePath[] paths) @safe { 755 return fromArgCompileDb(paths.map!(a => cast(string) a).array); 756 } 757 758 /// Import and merge many compilation databases into one DB. 759 CompileCommandDB fromArgCompileDb(string[] paths) @safe { 760 auto app = appender!(CompileCommand[])(); 761 paths.orDefaultDb.fromFiles(app); 762 763 return CompileCommandDB(app.data); 764 } 765 766 /// Flags to exclude from the flags passed on to the clang parser. 767 struct FilterClangFlag { 768 string payload; 769 alias payload this; 770 771 enum Kind { 772 exclude 773 } 774 775 Kind kind; 776 } 777 778 @("Should be cflags with all unnecessary flags removed") 779 unittest { 780 auto cmd = toCompileCommand("/home", "file1.cpp", [ 781 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun", "-c", 782 "a_filename.c" 783 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 784 auto s = cmd.get.parseFlag(defaultCompilerFilter); 785 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 786 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 787 } 788 789 @("Should be cflags with some excess spacing") 790 unittest { 791 auto cmd = toCompileCommand("/home", "file1.cpp", [ 792 "g++", "-MD", "-lfoo.a", "-l", "bar.a", "-I", "bar", "-Igun" 793 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 794 795 auto s = cmd.get.parseFlag(defaultCompilerFilter); 796 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 797 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 798 } 799 800 @("Should be cflags with machine dependent removed") 801 unittest { 802 auto cmd = toCompileCommand("/home", "file1.cpp", [ 803 "g++", "-mfoo", "-m", "bar", "-MD", "-lfoo.a", "-l", "bar.a", "-I", 804 "bar", "-Igun", "-c", "a_filename.c" 805 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 806 807 auto s = cmd.get.parseFlag(defaultCompilerFilter); 808 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 809 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 810 } 811 812 @("Should be cflags with all -f removed") 813 unittest { 814 auto cmd = toCompileCommand("/home", "file1.cpp", [ 815 "g++", "-fmany-fooo", "-I", "bar", "-fno-fooo", "-Igun", "-flolol", 816 "-c", "a_filename.c" 817 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 818 819 auto s = cmd.get.parseFlag(defaultCompilerFilter); 820 s.cflags.shouldEqual(["-I", "/home/bar", "-I", "/home/gun"]); 821 s.includes.shouldEqual(["/home/bar", "/home/gun"]); 822 } 823 824 @("shall NOT remove -std=xyz flags") 825 unittest { 826 auto cmd = toCompileCommand("/home", "file1.cpp", [ 827 "g++", "-std=c++11", "-c", "a_filename.c" 828 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 829 830 auto s = cmd.get.parseFlag(defaultCompilerFilter); 831 s.cflags.shouldEqual(["-std=c++11"]); 832 } 833 834 @("shall remove -mfloat-gprs=double") 835 unittest { 836 auto cmd = toCompileCommand("/home", "file1.cpp", [ 837 "g++", "-std=c++11", "-mfloat-gprs=double", "-c", "a_filename.c" 838 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 839 auto my_filter = CompileCommandFilter(defaultCompilerFlagFilter, 0); 840 my_filter.filter ~= FilterClangFlag("-mfloat-gprs=double", FilterClangFlag.Kind.exclude); 841 auto s = cmd.get.parseFlag(my_filter); 842 s.cflags.shouldEqual(["-std=c++11"]); 843 } 844 845 @("Shall keep all compiler flags as they are") 846 unittest { 847 auto cmd = toCompileCommand("/home", "file1.cpp", ["g++", "-Da", "-D", 848 "b"], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 849 850 auto s = cmd.get.parseFlag(defaultCompilerFilter); 851 s.cflags.shouldEqual(["-Da", "-D", "b"]); 852 } 853 854 version (unittest) { 855 import std.file : getcwd; 856 import std.path : absolutePath; 857 import std.format : format; 858 859 // contains a bit of extra junk that is expected to be removed 860 immutable string dummy_path = "/path/to/../to/./db/compilation_db.json"; 861 immutable string dummy_dir = "/path/to/db"; 862 863 enum raw_dummy1 = `[ 864 { 865 "directory": "dir1/dir2", 866 "command": "g++ -Idir1 -c -o binary file1.cpp", 867 "file": "file1.cpp" 868 } 869 ]`; 870 871 enum raw_dummy2 = `[ 872 { 873 "directory": "dir", 874 "command": "g++ -Idir1 -c -o binary file1.cpp", 875 "file": "file1.cpp" 876 }, 877 { 878 "directory": "dir", 879 "command": "g++ -Idir1 -c -o binary file2.cpp", 880 "file": "file2.cpp" 881 } 882 ]`; 883 884 enum raw_dummy3 = `[ 885 { 886 "directory": "dir1", 887 "command": "g++ -Idir1 -c -o binary file3.cpp", 888 "file": "file3.cpp" 889 }, 890 { 891 "directory": "dir2", 892 "command": "g++ -Idir1 -c -o binary file3.cpp", 893 "file": "file3.cpp" 894 } 895 ]`; 896 897 enum raw_dummy4 = `[ 898 { 899 "directory": "dir1", 900 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 901 "file": "file3.cpp", 902 "output": "file3.o" 903 }, 904 { 905 "directory": "dir2", 906 "arguments": "g++ -Idir1 -c -o binary file3.cpp", 907 "file": "file3.cpp", 908 "output": "file3.o" 909 } 910 ]`; 911 912 enum raw_dummy5 = `[ 913 { 914 "directory": "dir1", 915 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 916 "file": "file3.cpp", 917 "output": "file3.o" 918 }, 919 { 920 "directory": "dir2", 921 "arguments": ["g++", "-Idir1", "-c", "-o", "binary", "file3.cpp"], 922 "file": "file3.cpp", 923 "output": "file3.o" 924 } 925 ]`; 926 } 927 928 @("Should be a compile command DB") 929 unittest { 930 auto app = appender!(CompileCommand[])(); 931 raw_dummy1.parseCommands(CompileDbFile(dummy_path), app); 932 auto cmds = app.data; 933 934 assert(cmds.length == 1); 935 (cast(string) cmds[0].directory).shouldEqual(dummy_dir ~ "/dir1/dir2"); 936 cmds[0].command.shouldEqual([ 937 "g++", "-Idir1", "-c", "-o", "binary", "file1.cpp" 938 ]); 939 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 940 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/dir1/dir2/file1.cpp"); 941 } 942 943 @("Should be a DB with two entries") 944 unittest { 945 auto app = appender!(CompileCommand[])(); 946 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 947 auto cmds = app.data; 948 949 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 950 (cast(string) cmds[1].file).shouldEqual("file2.cpp"); 951 } 952 953 @("Should find filename") 954 unittest { 955 auto app = appender!(CompileCommand[])(); 956 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 957 auto cmds = CompileCommandDB(app.data); 958 959 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 960 assert(found.length == 1); 961 (cast(string) found[0].file).shouldEqual("file2.cpp"); 962 } 963 964 @("Should find no match by using an absolute path that doesn't exist in DB") 965 unittest { 966 auto app = appender!(CompileCommand[])(); 967 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 968 auto cmds = CompileCommandDB(app.data); 969 970 auto found = cmds.find("./file2.cpp"); 971 assert(found.length == 0); 972 } 973 974 @("Should find one match by using the absolute filename to disambiguous") 975 unittest { 976 auto app = appender!(CompileCommand[])(); 977 raw_dummy3.parseCommands(CompileDbFile(dummy_path), app); 978 auto cmds = CompileCommandDB(app.data); 979 980 auto found = cmds.find(dummy_dir ~ "/dir2/file3.cpp"); 981 assert(found.length == 1); 982 983 found.toString.shouldEqual(format("%s/dir2 984 file3.cpp 985 %s/dir2/file3.cpp 986 g++ -Idir1 -c -o binary file3.cpp 987 ", dummy_dir, dummy_dir)); 988 } 989 990 @("Should be a pretty printed search result") 991 unittest { 992 auto app = appender!(CompileCommand[])(); 993 raw_dummy2.parseCommands(CompileDbFile(dummy_path), app); 994 auto cmds = CompileCommandDB(app.data); 995 auto found = cmds.find(dummy_dir ~ "/dir/file2.cpp"); 996 997 found.toString.shouldEqual(format("%s/dir 998 file2.cpp 999 %s/dir/file2.cpp 1000 g++ -Idir1 -c -o binary file2.cpp 1001 ", dummy_dir, dummy_dir)); 1002 } 1003 1004 @("Should be a compile command DB with relative path") 1005 unittest { 1006 enum raw = `[ 1007 { 1008 "directory": ".", 1009 "command": "g++ -Idir1 -c -o binary file1.cpp", 1010 "file": "file1.cpp" 1011 } 1012 ]`; 1013 auto app = appender!(CompileCommand[])(); 1014 raw.parseCommands(CompileDbFile(dummy_path), app); 1015 auto cmds = app.data; 1016 1017 assert(cmds.length == 1); 1018 (cast(string) cmds[0].directory).shouldEqual(dummy_dir); 1019 (cast(string) cmds[0].file).shouldEqual("file1.cpp"); 1020 (cast(string) cmds[0].absoluteFile).shouldEqual(dummy_dir ~ "/file1.cpp"); 1021 } 1022 1023 @("Should be a DB read from a relative path with the contained paths adjusted appropriately") 1024 unittest { 1025 auto app = appender!(CompileCommand[])(); 1026 raw_dummy3.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1027 auto cmds = CompileCommandDB(app.data); 1028 1029 // trusted: constructing a path in memory which is never used for writing. 1030 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1031 1032 auto found = cmds.find(abs_path ~ "/dir2/file3.cpp"); 1033 assert(found.length == 1); 1034 1035 found.toString.shouldEqual(format("%s/dir2 1036 file3.cpp 1037 %s/dir2/file3.cpp 1038 g++ -Idir1 -c -o binary file3.cpp 1039 ", abs_path, abs_path)); 1040 } 1041 1042 @("shall extract arguments, file, directory and output with absolute paths") 1043 unittest { 1044 auto app = appender!(CompileCommand[])(); 1045 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1046 auto cmds = CompileCommandDB(app.data); 1047 1048 // trusted: constructing a path in memory which is never used for writing. 1049 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1050 1051 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1052 assert(found.length == 1); 1053 1054 found.toString.shouldEqual(format("%s/dir2 1055 file3.cpp 1056 %s/dir2/file3.cpp 1057 file3.o 1058 %s/dir2/file3.o 1059 g++ -Idir1 -c -o binary file3.cpp 1060 ", abs_path, abs_path, abs_path)); 1061 } 1062 1063 @("shall be the compiler flags derived from the arguments attribute") 1064 unittest { 1065 auto app = appender!(CompileCommand[])(); 1066 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1067 auto cmds = CompileCommandDB(app.data); 1068 1069 // trusted: constructing a path in memory which is never used for writing. 1070 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1071 1072 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.cpp")); 1073 assert(found.length == 1); 1074 1075 found[0].parseFlag(defaultCompilerFilter).cflags.shouldEqual([ 1076 "-I", buildPath(abs_path, "dir2", "dir1") 1077 ]); 1078 } 1079 1080 @("shall find the entry based on an output match") 1081 unittest { 1082 auto app = appender!(CompileCommand[])(); 1083 raw_dummy4.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1084 auto cmds = CompileCommandDB(app.data); 1085 1086 // trusted: constructing a path in memory which is never used for writing. 1087 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1088 1089 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1090 assert(found.length == 1); 1091 1092 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1093 } 1094 1095 @("shall parse the compilation database when *arguments* is a json list") 1096 unittest { 1097 auto app = appender!(CompileCommand[])(); 1098 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1099 auto cmds = CompileCommandDB(app.data); 1100 1101 // trusted: constructing a path in memory which is never used for writing. 1102 auto abs_path = () @trusted { return getcwd() ~ "/path"; }(); 1103 1104 auto found = cmds.find(buildPath(abs_path, "dir2", "file3.o")); 1105 assert(found.length == 1); 1106 1107 (cast(string) found[0].absoluteFile).shouldEqual(buildPath(abs_path, "dir2", "file3.cpp")); 1108 } 1109 1110 @("shall parse the compilation database and find a match via the glob pattern") 1111 unittest { 1112 import std.path : baseName; 1113 1114 auto app = appender!(CompileCommand[])(); 1115 raw_dummy5.parseCommands(CompileDbFile("path/compilation_db.json"), app); 1116 auto cmds = CompileCommandDB(app.data); 1117 1118 auto found = cmds.find("*/dir2/file3.cpp"); 1119 assert(found.length == 1); 1120 1121 found[0].absoluteFile.baseName.shouldEqual("file3.cpp"); 1122 } 1123 1124 @("shall extract filepath from includes correctly when there is spaces in the path") 1125 unittest { 1126 auto cmd = toCompileCommand("/home", "file.cpp", [ 1127 "-I", `"dir with spaces"`, "-I", `\"dir with spaces\"` 1128 ], AbsoluteCompileDbDirectory("/home".Path.AbsolutePath), null); 1129 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1130 pargs.cflags.shouldEqual([ 1131 "-I", "/home/dir with spaces", "-I", "/home/dir with spaces" 1132 ]); 1133 pargs.includes.shouldEqual([ 1134 "/home/dir with spaces", "/home/dir with spaces" 1135 ]); 1136 } 1137 1138 @("shall handle path with spaces, both as separate string and combined with backslash") 1139 unittest { 1140 auto cmd = toCompileCommand("/project", "file.cpp", [ 1141 "-I", `"separate dir/with space"`, "-I", `\"separate dir/with space\"`, 1142 `-I"combined dir/with space"`, `-I\"combined dir/with space\"`, 1143 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1144 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1145 pargs.cflags.shouldEqual([ 1146 "-I", "/project/separate dir/with space", "-I", 1147 "/project/separate dir/with space", "-I", 1148 "/project/combined dir/with space", "-I", 1149 "/project/combined dir/with space" 1150 ]); 1151 pargs.includes.shouldEqual([ 1152 "/project/separate dir/with space", "/project/separate dir/with space", 1153 "/project/combined dir/with space", "/project/combined dir/with space" 1154 ]); 1155 } 1156 1157 @("shall handle path with consecutive spaces") 1158 unittest { 1159 auto cmd = toCompileCommand("/project", "file.cpp", 1160 [ 1161 `-I"one space/lots of space"`, 1162 `-I\"one space/lots of space\"`, `-I`, 1163 `"one space/lots of space"`, `-I`, 1164 `\"one space/lots of space\"`, 1165 ], AbsoluteCompileDbDirectory("/project".Path.AbsolutePath), null); 1166 auto pargs = cmd.get.parseFlag(defaultCompilerFilter); 1167 pargs.cflags.shouldEqual([ 1168 "-I", "/project/one space/lots of space", "-I", 1169 "/project/one space/lots of space", "-I", 1170 "/project/one space/lots of space", "-I", 1171 "/project/one space/lots of space", 1172 ]); 1173 pargs.includes.shouldEqual([ 1174 "/project/one space/lots of space", 1175 "/project/one space/lots of space", 1176 "/project/one space/lots of space", 1177 "/project/one space/lots of space" 1178 ]); 1179 }