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