1 /**
2 Copyright: Copyright (c) 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 This file contains helpers for interactive with the clang abstractions.
11 */
12 module dextool.clang;
13 
14 import logger = std.experimental.logger;
15 import std.algorithm : filter, map;
16 import std.array : appender, array, empty;
17 import std.file : exists, getcwd;
18 import std.path : baseName, buildPath;
19 import std.typecons : Nullable, Yes;
20 
21 import dextool.compilation_db : CompileCommandDB, LimitFileRange, ParsedCompileCommand,
22     ParseFlags, CompileCommandFilter, CompileCommand, parseFlag, DbCompiler = Compiler;
23 import dextool.type : Path, AbsolutePath;
24 
25 @safe:
26 
27 struct IncludeResult {
28     /// The entry that had an #include with the desired file
29     ParsedCompileCommand original;
30 
31     /// The compile command derived from the original with adjusted file and
32     /// absoluteFile.
33     ParsedCompileCommand derived;
34 }
35 
36 /// Find the path on the filesystem where f exists as if the compiler search for the file.
37 Nullable!AbsolutePath findFile(Path f, ParseFlags.Include[] includes, AbsolutePath dir) {
38     typeof(return) rval;
39 
40     foreach (a; includes.map!(a => buildPath(cast(string) dir, a, f))
41             .filter!(a => exists(a))) {
42         rval = AbsolutePath(Path(a));
43         break;
44     }
45     return rval;
46 }
47 
48 /** Find a CompileCommand that in any way have an `#include` which pull in fname.
49  *
50  * This is useful to find the flags needed to parse a header file which is used
51  * by the implementation.
52  *
53  * Note that the context will be expanded with the flags.
54  *
55  * Returns: The first CompileCommand object which _probably_ has the flags needed to parse fname.
56  */
57 Nullable!IncludeResult findCompileCommandFromIncludes(ParsedCompileCommand[] compdb, Path fname) @trusted {
58     import libclang_ast.check_parse_result : hasParseErrors, logDiagnostic;
59     import libclang_ast.context : ClangContext;
60     import libclang_ast.include_visitor : hasInclude;
61 
62     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
63 
64     string find_file = fname.baseName;
65 
66     bool isMatch(string include) {
67         return find_file == include.baseName;
68     }
69 
70     typeof(return) r;
71 
72     foreach (entry; compdb.filter!(a => exists(a.cmd.absoluteFile))) {
73         auto translation_unit = ctx.makeTranslationUnit(entry.cmd.absoluteFile,
74                 entry.flags.completeFlags);
75 
76         if (translation_unit.hasParseErrors) {
77             logger.infof("Skipping '%s' because of compilation errors", entry.cmd.absoluteFile);
78             logDiagnostic(translation_unit);
79             continue;
80         }
81 
82         auto found = translation_unit.cursor.hasInclude!isMatch();
83         if (!found.isNull) {
84             r = IncludeResult.init;
85             r.get.original = entry;
86             r.get.derived = entry;
87             r.get.derived.cmd.file = found.get;
88             auto onDisc = findFile(found.get, entry.flags.includes, entry.cmd.directory);
89             if (onDisc.isNull) {
90                 // wild guess
91                 r.get.derived.cmd.absoluteFile = AbsolutePath(Path(buildPath(entry.cmd.directory,
92                         found.get)));
93             } else {
94                 r.get.derived.cmd.absoluteFile = onDisc.get;
95             }
96 
97             return r;
98         }
99     }
100 
101     return r;
102 }
103 
104 /** Try and find matching compiler flags for the missing files.
105  *
106  * Returns: an updated LimitFileRange where those that where found have been moved from `missingFiles` to `commands`.
107  */
108 auto reduceMissingFiles(LimitFileRange lfr, ParsedCompileCommand[] db) {
109     import std.algorithm : canFind;
110 
111     if (db.empty || lfr.isMissingFilesEmpty)
112         return lfr;
113 
114     auto found = appender!(string[])();
115     auto newCmds = appender!(ParsedCompileCommand[])();
116 
117     foreach (f; lfr.missingFiles) {
118         logger.infof(`Analyzing all files in the compilation DB for one that has an '#include "%s"'`,
119                 f.baseName);
120 
121         auto res = findCompileCommandFromIncludes(db, Path(f));
122         if (res.isNull) {
123             continue;
124         }
125 
126         logger.infof(`Using compiler flags derived from '%s' because it has an '#include' for '%s'`,
127                 res.get.original.cmd.absoluteFile, res.get.derived.cmd.absoluteFile);
128 
129         ParsedCompileCommand cmd = res.get.derived;
130 
131         // check if the file from the user is directly accessable on the
132         // filesystem. In such a case assume that the located file is the one
133         // the user want to parse. Otherwise derive it from the compile command
134         // DB.
135         if (exists(f)) {
136             cmd.cmd.file = Path(f);
137             cmd.cmd.absoluteFile = AbsolutePath(f);
138         } else {
139             logger.tracef("Unable to locate '%s' on the filesystem", f);
140             logger.tracef("Using the filename from the compile DB instead '%s'",
141                     cmd.cmd.absoluteFile);
142         }
143 
144         newCmds.put(cmd);
145         found.put(f);
146     }
147 
148     lfr.commands ~= newCmds.data;
149     lfr.missingFiles = lfr.missingFiles.filter!(a => !found.data.canFind(a)).array;
150     return lfr;
151 }