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