1 /// Workspace-d component that provide import paths and errors from a 2 /// compile_commands.json file generated by a build system. 3 /// See https://clang.llvm.org/docs/JSONCompilationDatabase.html 4 module workspaced.com.ccdb; 5 6 import std.exception; 7 import std.file; 8 import std.json; 9 import std.stdio; 10 11 import workspaced.api; 12 13 import containers.hashset; 14 15 import dub.internal.vibecompat.core.log; 16 17 @component("ccdb") 18 class ClangCompilationDatabaseComponent : ComponentWrapper 19 { 20 mixin DefaultComponentWrapper; 21 22 protected void load() 23 { 24 logDebug("loading ccdb component"); 25 26 if (config.get!bool("ccdb", "registerImportProvider", true)) 27 importPathProvider = &imports; 28 if (config.get!bool("ccdb", "registerStringImportProvider", true)) 29 stringImportPathProvider = &stringImports; 30 if (config.get!bool("ccdb", "registerImportFilesProvider", false)) 31 importFilesProvider = &fileImports; 32 if (config.get!bool("ccdb", "registerProjectVersionsProvider", true)) 33 projectVersionsProvider = &versions; 34 if (config.get!bool("ccdb", "registerDebugSpecificationsProvider", true)) 35 debugSpecificationsProvider = &debugVersions; 36 37 try 38 { 39 if (config.get!string("ccdb", null)) 40 { 41 const dbPath = config.get!string("ccdb", "dbPath"); 42 if (!dbPath) 43 { 44 throw new Exception("ccdb.dbPath is not provided"); 45 } 46 loadDb(dbPath); 47 } 48 } 49 catch (Exception e) 50 { 51 stderr.writeln("Clang-DB Error (ignored): ", e); 52 } 53 } 54 55 private void loadDb(string filename) 56 { 57 import std.algorithm : each, filter, map; 58 import std.array : array; 59 60 string jsonString = cast(string) assumeUnique(read(filename)); 61 auto json = parseJSON(jsonString); 62 // clang db can be quite large (e.g. 100 k lines of JSON data on large projects) 63 // we release memory when possible to avoid having at the same time more than 64 // two represention of the same data 65 jsonString = null; 66 67 HashSet!string imports; 68 HashSet!string stringImports; 69 HashSet!string fileImports; 70 HashSet!string versions; 71 HashSet!string debugVersions; 72 73 json.array 74 .map!(jv => CompileCommand.fromJson(jv)) 75 .filter!(cc => cc.isValid) 76 .each!(cc => 77 cc.feedOptions(imports, stringImports, fileImports, versions, debugVersions) 78 ); 79 80 _importPaths = imports[].array; 81 _stringImportPaths = stringImports[].array; 82 _importFiles = fileImports[].array; 83 _versions = versions[].array; 84 _debugVersions = debugVersions[].array; 85 } 86 87 /// Lists all import paths 88 string[] imports() @property nothrow 89 { 90 return _importPaths; 91 } 92 93 /// Lists all string import paths 94 string[] stringImports() @property nothrow 95 { 96 return _stringImportPaths; 97 } 98 99 /// Lists all import paths to files 100 string[] fileImports() @property nothrow 101 { 102 return _importFiles; 103 } 104 105 /// Lists the currently defined versions 106 string[] versions() @property nothrow 107 { 108 return _versions; 109 } 110 111 /// Lists the currently defined debug versions (debug specifications) 112 string[] debugVersions() @property nothrow 113 { 114 return _debugVersions; 115 } 116 117 private: 118 119 string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; 120 } 121 122 private struct CompileCommand 123 { 124 string directory; 125 string file; 126 string[] args; 127 string output; 128 129 static CompileCommand fromJson(JSONValue json) 130 { 131 import std.algorithm : map; 132 import std.array : array; 133 134 CompileCommand cc; 135 136 cc.directory = json["directory"].str; 137 cc.file = json["file"].str; 138 139 if (auto args = "arguments" in json) 140 { 141 cc.args = args.array.map!(jv => jv.str).array; 142 } 143 else if (auto cmd = "command" in json) 144 { 145 cc.args = unescapeCommand(cmd.str); 146 } 147 else 148 { 149 throw new Exception( 150 "Either 'arguments' or 'command' missing from Clang compilation database"); 151 } 152 153 if (auto o = "output" in json) 154 { 155 cc.output = o.str; 156 } 157 158 return cc; 159 } 160 161 @property bool isValid() const 162 { 163 import std.algorithm : endsWith; 164 165 if (args.length <= 1) 166 return false; 167 if (!file.endsWith(".d")) 168 return false; 169 return true; 170 } 171 172 void feedOptions( 173 ref HashSet!string imports, 174 ref HashSet!string stringImports, 175 ref HashSet!string fileImports, 176 ref HashSet!string versions, 177 ref HashSet!string debugVersions) 178 { 179 import std.algorithm : startsWith; 180 181 enum importMark = "-I"; // optional = 182 enum stringImportMark = "-J"; // optional = 183 enum fileImportMark = "-i="; 184 enum versionMark = "-version="; 185 enum debugMark = "-debug="; 186 187 foreach (arg; args) 188 { 189 const mark = arg.startsWith( 190 importMark, stringImportMark, fileImportMark, versionMark, debugMark 191 ); 192 193 switch (mark) 194 { 195 case 0: 196 break; 197 case 1: 198 case 2: 199 if (arg.length == 2) 200 break; // ill-formed flag, we don't need to care here 201 const st = arg[2] == '=' ? 3 : 2; 202 const path = getPath(arg[st .. $]); 203 if (mark == 1) 204 imports.put(path); 205 else 206 stringImports.put(path); 207 break; 208 case 3: 209 fileImports.put(getPath(arg[fileImportMark.length .. $])); 210 break; 211 case 4: 212 versions.put(getPath(arg[versionMark.length .. $])); 213 break; 214 case 5: 215 debugVersions.put(getPath(arg[debugMark.length .. $])); 216 break; 217 default: 218 break; 219 } 220 } 221 } 222 223 string getPath(string filename) 224 { 225 import std.path : absolutePath; 226 227 return absolutePath(filename, directory); 228 } 229 } 230 231 private string[] unescapeCommand(string cmd) 232 { 233 string[] result; 234 string current; 235 236 bool inquot; 237 bool escapeNext; 238 239 foreach (dchar c; cmd) 240 { 241 if (escapeNext) 242 { 243 escapeNext = false; 244 if (c != '"') 245 { 246 current ~= '\\'; 247 } 248 current ~= c; 249 continue; 250 } 251 252 switch (c) 253 { 254 case '\\': 255 escapeNext = true; 256 break; 257 case '"': 258 inquot = !inquot; 259 break; 260 case ' ': 261 if (inquot) 262 { 263 current ~= ' '; 264 } 265 else 266 { 267 result ~= current; 268 current = null; 269 } 270 break; 271 default: 272 current ~= c; 273 break; 274 } 275 } 276 277 if (current.length) 278 { 279 result ~= current; 280 } 281 return result; 282 } 283 284 @("unescapeCommand") 285 unittest 286 { 287 const cmd = `"ldc2" "-I=..\foo\src" -I="..\with \" and space" "-m64" ` ~ 288 `-of=foo/libfoo.a.p/src_foo_bar.d.obj -c ../foo/src/foo/bar.d`; 289 290 const cmdArgs = unescapeCommand(cmd); 291 292 const args = [ 293 "ldc2", "-I=..\\foo\\src", "-I=..\\with \" and space", "-m64", 294 "-of=foo/libfoo.a.p/src_foo_bar.d.obj", "-c", "../foo/src/foo/bar.d", 295 ]; 296 297 assert(cmdArgs == args); 298 }