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 }