1use std::collections::BTreeSet;
17use std::path::{Path, PathBuf};
18
19use serde::Deserialize;
20
21use super::{BuildConfiguration, CppBuild};
22
23#[derive(Debug, thiserror::Error)]
25pub enum CompileCommandsError {
26 #[error("compile_commands.json is {actual_bytes} bytes, exceeding the {max_bytes}-byte limit")]
28 TooLarge {
29 actual_bytes: u64,
31 max_bytes: u64,
33 },
34 #[error("reading compile_commands.json: {0}")]
36 Read(#[source] std::io::Error),
37 #[error("parsing compile_commands.json: {0}")]
39 Parse(#[source] serde_json::Error),
40}
41
42#[derive(Debug, Deserialize)]
43struct RawEntry {
44 file: String,
45 directory: Option<String>,
46 #[serde(default)]
49 arguments: Option<Vec<String>>,
50 #[serde(default)]
52 command: Option<String>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct CompileEntry {
58 pub file: PathBuf,
61 pub directory: Option<PathBuf>,
63 pub arguments: Vec<String>,
69}
70
71impl CompileEntry {
72 #[must_use]
78 pub fn build(&self) -> CppBuild {
79 CppBuild::from_command_in_directory(&self.arguments, &self.file, self.directory.as_deref())
80 }
81
82 #[must_use]
88 pub fn selector_fields(&self) -> (String, Option<String>, Vec<String>) {
89 let normalize = |path: &Path| {
90 crate::paths::canonical(path)
91 .unwrap_or_else(|_| path.to_path_buf())
92 .display()
93 .to_string()
94 };
95 (
96 normalize(&self.file),
97 self.directory.as_deref().map(normalize),
98 self.arguments.clone(),
99 )
100 }
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct CompileCommands {
106 pub entries: Vec<CompileEntry>,
108 pub content_hash: Option<String>,
114}
115
116impl CompileCommands {
117 pub fn read(path: &Path) -> Result<Self, CompileCommandsError> {
124 let text = std::fs::read_to_string(path).map_err(CompileCommandsError::Read)?;
125 Self::parse(&text)
126 }
127
128 pub fn read_with_limit(path: &Path, max_bytes: u64) -> Result<Self, CompileCommandsError> {
136 let metadata = std::fs::metadata(path).map_err(CompileCommandsError::Read)?;
137 if metadata.len() > max_bytes {
138 return Err(CompileCommandsError::TooLarge {
139 actual_bytes: metadata.len(),
140 max_bytes,
141 });
142 }
143 let text = std::fs::read_to_string(path).map_err(CompileCommandsError::Read)?;
144 if text.len() as u64 > max_bytes {
145 return Err(CompileCommandsError::TooLarge {
146 actual_bytes: text.len() as u64,
147 max_bytes,
148 });
149 }
150 Self::parse(&text)
151 }
152
153 fn parse(text: &str) -> Result<Self, CompileCommandsError> {
154 let raw: Vec<RawEntry> = serde_json::from_str(text).map_err(CompileCommandsError::Parse)?;
155 let mut seen = BTreeSet::new();
156 let mut entries = Vec::new();
157 for entry in raw {
158 let directory = entry.directory.map(PathBuf::from);
159 let file_path = PathBuf::from(&entry.file);
160 let resolved = match (&directory, file_path.is_relative()) {
161 (Some(dir), true) => dir.join(&file_path),
162 _ => file_path,
163 };
164 let arguments = entry.arguments.unwrap_or_else(|| {
165 entry
166 .command
167 .as_deref()
168 .map(split_command)
169 .unwrap_or_default()
170 });
171 if seen.insert((resolved.clone(), arguments.clone())) {
172 entries.push(CompileEntry {
173 file: resolved,
174 directory,
175 arguments,
176 });
177 }
178 }
179 Ok(Self {
180 entries,
181 content_hash: Some(super::build_config::content_hash(text)),
182 })
183 }
184
185 #[must_use]
187 pub fn translation_unit_count(&self) -> usize {
188 self.entries.len()
189 }
190
191 #[must_use]
197 pub fn source_file_count(&self) -> usize {
198 self.entries
199 .iter()
200 .map(|entry| &entry.file)
201 .collect::<BTreeSet<_>>()
202 .len()
203 }
204
205 #[must_use]
212 pub fn build_partitions(&self) -> std::collections::BTreeMap<String, Vec<&CompileEntry>> {
213 let mut partitions = std::collections::BTreeMap::new();
214 for entry in &self.entries {
215 let build = BuildConfiguration::Cpp(Box::new(entry.build()));
216 partitions
217 .entry(build.fingerprint())
218 .or_insert_with(Vec::new)
219 .push(entry);
220 }
221 partitions
222 }
223}
224
225fn split_command(command: &str) -> Vec<String> {
232 let mut arguments = Vec::new();
233 let mut current = String::new();
234 let mut started = false;
235 let mut quote: Option<char> = None;
236 let mut characters = command.chars();
237 while let Some(character) = characters.next() {
238 match (character, quote) {
239 ('\\', Some('\'')) => current.push('\\'),
240 ('\\', _) => {
241 if let Some(escaped) = characters.next() {
242 current.push(escaped);
243 }
244 }
245 ('\'' | '"', None) => {
246 quote = Some(character);
247 started = true;
248 }
249 (c, Some(open)) if c == open => quote = None,
250 (c, None) if c.is_whitespace() => {
251 if started || !current.is_empty() {
252 arguments.push(std::mem::take(&mut current));
253 started = false;
254 }
255 }
256 (c, _) => current.push(c),
257 }
258 }
259 if started || !current.is_empty() {
260 arguments.push(current);
261 }
262 arguments
263}
264
265#[cfg(test)]
266#[allow(clippy::unwrap_used, clippy::expect_used)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn reads_entries_and_resolves_relative_paths() {
272 let dir = tempfile::tempdir().unwrap();
273 let path = dir.path().join("compile_commands.json");
274 std::fs::write(
275 &path,
276 r#"[
277 {"directory": "/work/build", "file": "../src/a.c", "command": "cc a.c"},
278 {"directory": "/work/build", "file": "/abs/b.c", "command": "cc b.c"}
279 ]"#,
280 )
281 .unwrap();
282 let db = CompileCommands::read(&path).unwrap();
283 assert_eq!(db.translation_unit_count(), 2);
284 assert_eq!(db.entries[0].file, PathBuf::from("/work/build/../src/a.c"));
285 assert_eq!(db.entries[1].file, PathBuf::from("/abs/b.c"));
286 }
287
288 #[test]
289 fn duplicate_translation_units_are_registered_once() {
290 let dir = tempfile::tempdir().unwrap();
291 let path = dir.path().join("compile_commands.json");
292 std::fs::write(
293 &path,
294 r#"[
295 {"directory": "/w", "file": "/w/a.c"},
296 {"directory": "/w", "file": "/w/a.c"}
297 ]"#,
298 )
299 .unwrap();
300 let db = CompileCommands::read(&path).unwrap();
301 assert_eq!(db.translation_unit_count(), 1);
302 }
303
304 #[test]
308 fn one_file_compiled_two_ways_is_two_translation_units() {
309 let dir = tempfile::tempdir().unwrap();
310 let path = dir.path().join("compile_commands.json");
311 std::fs::write(
312 &path,
313 r#"[
314 {"directory": "/w", "file": "/w/a.c", "arguments": ["cc", "-c", "/w/a.c"]},
315 {"directory": "/w", "file": "/w/a.c",
316 "arguments": ["cc", "-DWIDE=1", "-c", "/w/a.c"]}
317 ]"#,
318 )
319 .unwrap();
320 let db = CompileCommands::read(&path).unwrap();
321 assert_eq!(db.translation_unit_count(), 2);
322 assert_eq!(db.source_file_count(), 1);
324 }
325
326 #[test]
327 fn commands_partition_by_build_settings_not_source_path() {
328 let dir = tempfile::tempdir().unwrap();
329 let path = dir.path().join("compile_commands.json");
330 std::fs::write(
331 &path,
332 r#"[
333 {"directory": "/w", "file": "/w/a.cpp", "arguments": ["clang++", "-DNARROW", "-c", "/w/a.cpp"]},
334 {"directory": "/w", "file": "/w/b.cpp", "arguments": ["clang++", "-DNARROW", "-c", "/w/b.cpp"]},
335 {"directory": "/w", "file": "/w/a.cpp", "arguments": ["clang++", "-DWIDE", "-c", "/w/a.cpp"]}
336 ]"#,
337 )
338 .unwrap();
339 let db = CompileCommands::read(&path).unwrap();
340 let partitions = db.build_partitions();
341 assert_eq!(partitions.len(), 2);
342 let mut sizes: Vec<usize> = partitions.values().map(Vec::len).collect();
343 sizes.sort_unstable();
344 assert_eq!(sizes, [1, 2]);
345 assert!(partitions.values().any(|entries| {
346 entries
347 .iter()
348 .all(|entry| entry.build().defines() == ["NARROW"])
349 }));
350 assert!(partitions.values().any(|entries| {
351 entries
352 .iter()
353 .all(|entry| entry.build().defines() == ["WIDE"])
354 }));
355 }
356
357 #[test]
360 fn relative_source_arguments_do_not_split_an_otherwise_shared_build() {
361 let dir = tempfile::tempdir().unwrap();
362 let source_dir = dir.path().join("src");
363 std::fs::create_dir_all(&source_dir).unwrap();
364 std::fs::write(source_dir.join("first.cpp"), "int first() { return 1; }\n").unwrap();
365 std::fs::write(
366 source_dir.join("second.cpp"),
367 "int second() { return 2; }\n",
368 )
369 .unwrap();
370 let path = dir.path().join("compile_commands.json");
371 let directory = serde_json::to_string(&source_dir.display().to_string()).unwrap();
375 std::fs::write(
376 &path,
377 format!(
378 r#"[
379 {{"directory": {directory}, "file": "first.cpp", "arguments": ["clang++", "-std=c++20", "-c", "first.cpp"]}},
380 {{"directory": {directory}, "file": "second.cpp", "arguments": ["clang++", "-std=c++20", "-c", "second.cpp"]}}
381 ]"#
382 ),
383 )
384 .unwrap();
385
386 let db = CompileCommands::read(&path).unwrap();
387 let partitions = db.build_partitions();
388 assert_eq!(partitions.len(), 1);
389 assert_eq!(partitions.values().next().map(Vec::len), Some(2));
390 }
391
392 #[test]
393 fn a_recorded_command_line_is_split_the_way_a_shell_would_split_it() {
394 let dir = tempfile::tempdir().unwrap();
395 let path = dir.path().join("compile_commands.json");
396 std::fs::write(
397 &path,
398 r#"[{"directory": "/w", "file": "/w/a.c",
399 "command": "cc -I\"/w/inc dir\" -DTEXT='a b' -c /w/a.c"}]"#,
400 )
401 .unwrap();
402 let db = CompileCommands::read(&path).unwrap();
403 assert_eq!(
404 db.entries[0].arguments,
405 vec!["cc", "-I/w/inc dir", "-DTEXT=a b", "-c", "/w/a.c"]
406 );
407 }
408
409 #[test]
412 fn the_database_is_identified_by_what_it_says() {
413 let dir = tempfile::tempdir().unwrap();
414 let one = dir.path().join("one.json");
415 let other = dir.path().join("other.json");
416 std::fs::write(&one, r#"[{"directory": "/w", "file": "/w/a.c"}]"#).unwrap();
417 std::fs::write(&other, r#"[{"directory": "/w", "file": "/w/b.c"}]"#).unwrap();
418 let one = CompileCommands::read(&one).unwrap();
419 let other = CompileCommands::read(&other).unwrap();
420 assert!(one.content_hash.is_some());
421 assert_ne!(one.content_hash, other.content_hash);
422 }
423
424 #[test]
425 fn unrelated_database_entries_do_not_change_an_existing_partition_identity() {
426 let one = CompileCommands::parse(
427 r#"[{"directory":"/w","file":"/w/a.c","arguments":["cc","-DVALUE=1","-c","/w/a.c"]}]"#,
428 )
429 .unwrap();
430 let expanded = CompileCommands::parse(
431 r#"[
432 {"directory":"/w","file":"/w/a.c","arguments":["cc","-DVALUE=1","-c","/w/a.c"]},
433 {"directory":"/w","file":"/w/unrelated.c","arguments":["cc","-DVALUE=2","-c","/w/unrelated.c"]}
434 ]"#,
435 )
436 .unwrap();
437
438 let original = BuildConfiguration::Cpp(Box::new(one.entries[0].build())).fingerprint();
439 let unchanged =
440 BuildConfiguration::Cpp(Box::new(expanded.entries[0].build())).fingerprint();
441 assert_eq!(original, unchanged);
442 assert_ne!(one.content_hash, expanded.content_hash);
443 }
444
445 #[test]
446 fn malformed_json_is_an_error() {
447 let dir = tempfile::tempdir().unwrap();
448 let path = dir.path().join("compile_commands.json");
449 std::fs::write(&path, "not json").unwrap();
450 assert!(matches!(
451 CompileCommands::read(&path),
452 Err(CompileCommandsError::Parse(_))
453 ));
454 }
455
456 #[test]
457 fn a_database_over_the_size_limit_is_rejected_before_parsing() {
458 let dir = tempfile::tempdir().unwrap();
459 let path = dir.path().join("compile_commands.json");
460 std::fs::write(&path, "[{}]").unwrap();
461
462 assert!(matches!(
463 CompileCommands::read_with_limit(&path, 2),
464 Err(CompileCommandsError::TooLarge {
465 actual_bytes: 4,
466 max_bytes: 2,
467 })
468 ));
469 }
470}