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, DbCompiler = Compiler;
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, FileName fname,
42         ref const CompileCommandFilter flag_filter, const string[] extra_flags,
43         const DbCompiler user_compiler = DbCompiler.init) @trusted {
44     import std.algorithm : filter;
45     import std.file : exists;
46     import std.path : baseName;
47     import std.typecons : Yes;
48 
49     import cpptooling.analyzer.clang.check_parse_result : hasParseErrors, 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, user_compiler);
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.get.original = entry;
77             r.get.derived = entry;
78             r.get.derived.file = found.get;
79             r.get.derived.absoluteFile = CompileCommand.AbsoluteFileName(entry.directory,
80                     found.get);
81             return r;
82         }
83     }
84 
85     return r;
86 }
87 
88 /// Find flags for fname by searching in the compilation DB.
89 Nullable!SearchResult findFlags(ref CompileCommandDB compdb, FileName fname, const string[] flags,
90         ref const CompileCommandFilter flag_filter, const DbCompiler user_compiler = DbCompiler
91         .init) {
92     import std.file : exists;
93     import std.path : baseName;
94 
95     import dextool.compilation_db : appendOrError;
96 
97     typeof(return) rval;
98 
99     auto db_search_result = compdb.appendOrError(flags, fname, flag_filter);
100     if (!db_search_result.isNull) {
101         rval = SearchResult(db_search_result.get.flags, db_search_result.get.absoluteFile);
102         logger.trace(rval.get.flags);
103         return rval;
104     }
105 
106     logger.warningf(`Analyzing all files in the compilation DB for one that has an '#include "%s"'`,
107             fname.baseName);
108 
109     auto sres = compdb.findCompileCommandFromIncludes(fname, flag_filter, flags);
110     if (sres.isNull) {
111         logger.error("Unable to find any compiler flags for: ", fname);
112         return rval;
113     }
114 
115     // check if the file from the user is directly accessable on the filesystem.
116     // in such a case assume that the located file is the one the user want to parse.
117     // otherwise derive it from the compile command DB.
118     auto p = AbsolutePath(fname);
119 
120     if (!exists(p)) {
121         logger.tracef("Unable to locate '%s' on the filesystem", p);
122         p = sres.get.derived.absoluteFile;
123         logger.tracef("Using the filename from the compile DB instead '%s'", p);
124     }
125 
126     logger.warningf(`Using compiler flags derived from '%s' because it has an '#include' for '%s'`,
127             sres.get.original.absoluteFile, sres.get.derived.absoluteFile);
128 
129     rval = SearchResult(flags ~ sres.get.derived.parseFlag(flag_filter, user_compiler), p);
130     // the user may want to see the flags but usually uninterested
131     logger.trace(rval.get.flags);
132 
133     return rval;
134 }