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