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 std.typecons : Nullable;
15 import logger = std.experimental.logger;
16 
17 import dextool.compilation_db : SearchResult, CompileCommandDB,
18     CompileCommandFilter, CompileCommand, parseFlag;
19 import dextool.type : FileName, AbsolutePath;
20 
21 @safe:
22 
23 private struct IncludeResult {
24     /// The entry that had an #include with the desired file
25     CompileCommand original;
26 
27     /// The compile command derived from the original with adjusted file and
28     /// absoluteFile.
29     CompileCommand derived;
30 }
31 
32 /** Find a CompileCommand that in any way have an `#include` which pull in fname.
33  *
34  * This is useful to find the flags needed to parse a header file which is used
35  * by the implementation.
36  *
37  * Note that the context will be expanded with the flags.
38  *
39  * Returns: The first CompileCommand object which _probably_ has the flags needed to parse fname.
40  */
41 Nullable!IncludeResult findCompileCommandFromIncludes(ref CompileCommandDB compdb,
42         FileName fname, ref const CompileCommandFilter flag_filter, const string[] extra_flags) {
43     import std.algorithm : filter;
44     import std.file : exists;
45     import std.path : baseName;
46     import std.typecons : Yes;
47 
48     import cpptooling.analyzer.clang.check_parse_result : hasParseErrors,
49         logDiagnostic;
50     import cpptooling.analyzer.clang.context : ClangContext;
51     import cpptooling.analyzer.clang.include_visitor : hasInclude;
52 
53     auto ctx = ClangContext(Yes.useInternalHeaders, Yes.prependParamSyntaxOnly);
54 
55     string find_file = fname.baseName;
56 
57     bool isMatch(string include) {
58         return find_file == include.baseName;
59     }
60 
61     Nullable!IncludeResult r;
62 
63     foreach (entry; compdb.filter!(a => exists(a.absoluteFile))) {
64         auto flags = extra_flags ~ entry.parseFlag(flag_filter);
65         auto translation_unit = ctx.makeTranslationUnit(entry.absoluteFile, flags);
66 
67         if (translation_unit.hasParseErrors) {
68             logger.warningf("Skipping '%s' because of compilation errors", entry.absoluteFile);
69             logDiagnostic(translation_unit);
70             continue;
71         }
72 
73         auto found = translation_unit.cursor.hasInclude!isMatch();
74         if (!found.isNull) {
75             r = IncludeResult();
76             r.original = entry;
77             r.derived = entry;
78             r.derived.file = found.get;
79             r.derived.absoluteFile = CompileCommand.AbsoluteFileName(entry.directory, found.get);
80             return r;
81         }
82     }
83 
84     return r;
85 }
86 
87 /// Find flags for fname by searching in the compilation DB.
88 Nullable!SearchResult findFlags(ref CompileCommandDB compdb, FileName fname,
89         const string[] flags, ref const CompileCommandFilter flag_filter) {
90     import std.file : exists;
91     import std.path : baseName;
92     import std..string : join;
93 
94     import dextool.compilation_db : appendOrError;
95 
96     typeof(return) rval;
97 
98     auto db_search_result = compdb.appendOrError(flags, fname, flag_filter);
99     if (!db_search_result.isNull) {
100         rval = SearchResult(db_search_result.cflags, db_search_result.absoluteFile);
101         logger.trace("Compiler flags: ", rval.cflags.join(" "));
102         return rval;
103     }
104 
105     logger.warningf(`Analyzing all files in the compilation DB for one that has an '#include "%s"'`,
106             fname.baseName);
107 
108     auto sres = compdb.findCompileCommandFromIncludes(fname, flag_filter, flags);
109     if (sres.isNull) {
110         logger.error("Unable to find any compiler flags for: ", fname);
111         return rval;
112     }
113 
114     // check if the file from the user is directly accessable on the filesystem.
115     // in such a case assume that the located file is the one the user want to parse.
116     // otherwise derive it from the compile command DB.
117     auto p = AbsolutePath(fname);
118 
119     if (!exists(p)) {
120         logger.tracef("Unable to locate '%s' on the filesystem", p);
121         p = sres.derived.absoluteFile;
122         logger.tracef("Using the filename from the compile DB instead '%s'", p);
123     }
124 
125     logger.warningf(`Using compiler flags derived from '%s' because it has an '#include' for '%s'`,
126             sres.original.absoluteFile, sres.derived.absoluteFile);
127 
128     rval = SearchResult(flags ~ sres.derived.parseFlag(flag_filter), p);
129     // the user may want to see the flags but usually uninterested
130     logger.trace("Compiler flags: ", rval.cflags.join(" "));
131 
132     return rval;
133 }