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 }