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