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 }