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