1use brokk_bifrost_core::analyzer::ProjectFile;
8use brokk_bifrost_core::analyzer::project::Project;
9use brokk_bifrost_core::hash::{HashMap, HashSet};
10use brokk_bifrost_core::path_normalization::NormalizePath;
11use serde::Deserialize;
12use std::collections::BTreeSet;
13use std::ffi::OsString;
14use std::path::{Component, Path, PathBuf};
15
16const COMPILATION_DATABASE_PATH: &str = "compile_commands.json";
17
18const MAX_MISSING_WORKSPACE_SOURCE_SAMPLE: usize = 32;
23
24pub fn is_cpp_compile_context_input(file: &ProjectFile) -> bool {
27 file.rel_path() == Path::new(COMPILATION_DATABASE_PATH)
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct CppCompileContext {
36 pub project_include_roots: Vec<PathBuf>,
37 pub system_include_roots: Vec<PathBuf>,
38 pub forced_includes: Vec<PathBuf>,
39 pub defined_macros: HashSet<String>,
40 include_search_roots: Vec<CppIncludeSearchRoot>,
41 driver_basename: Option<String>,
45 explicit_language: Option<CompiledLanguage>,
48 std_language: Option<CompiledLanguage>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum CompiledLanguage {
58 C,
59 Cpp,
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68enum CppIncludeSearchRootKind {
69 Project,
70 Quote,
71 System,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75struct CppIncludeSearchRoot {
76 path: PathBuf,
77 kind: CppIncludeSearchRootKind,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub enum CppExternalIncludeResolution {
83 MissingCompileContext,
85 Undeclared,
87 Conflicting,
89 Declared { root: PathBuf, header: PathBuf },
91}
92
93#[derive(Debug, Clone, Default, PartialEq, Eq)]
103pub struct CppCompileDatabaseCoverage {
104 missing_workspace_source_count: usize,
105 missing_workspace_source_sample: Vec<PathBuf>,
106}
107
108impl CppCompileDatabaseCoverage {
109 pub fn missing_workspace_source_count(&self) -> usize {
112 self.missing_workspace_source_count
113 }
114
115 pub fn missing_workspace_source_sample(&self) -> &[PathBuf] {
117 &self.missing_workspace_source_sample
118 }
119}
120
121#[derive(Debug, Default)]
122pub struct CppCompileContexts {
123 by_source: HashMap<PathBuf, Vec<CppCompileContext>>,
124 database_sources: HashSet<PathBuf>,
130}
131
132impl CppCompileContexts {
133 pub fn load(project: &dyn Project) -> Self {
134 let workspace_root = canonical_or_normalized_path(project.root());
135 let database_path = workspace_root.join(COMPILATION_DATABASE_PATH);
136 let Ok(database) = std::fs::read_to_string(database_path) else {
137 return Self::default();
138 };
139 let Ok(entries) = serde_json::from_str::<Vec<CompilationDatabaseEntry>>(&database) else {
140 return Self::default();
141 };
142
143 let mut by_source: HashMap<PathBuf, Vec<CppCompileContext>> = HashMap::default();
144 let mut database_sources = HashSet::default();
145 for entry in entries {
146 let Some(source) = entry.source_path(&workspace_root) else {
147 continue;
148 };
149 let source_identity = canonical_or_normalized_path(&source);
150 if source_identity.strip_prefix(&workspace_root).is_err() {
151 continue;
152 }
153 database_sources.insert(source_identity.clone());
154 let Some(context) = entry.compile_context(&workspace_root) else {
155 continue;
156 };
157 let candidates = by_source.entry(source_identity).or_default();
163 if !candidates.contains(&context) {
164 candidates.push(context);
165 }
166 }
167 Self {
168 by_source,
169 database_sources,
170 }
171 }
172
173 pub fn missing_workspace_sources<'a>(
183 &self,
184 workspace_root: &Path,
185 workspace_files: impl IntoIterator<Item = &'a ProjectFile>,
186 ) -> CppCompileDatabaseCoverage {
187 let workspace_root = canonical_or_normalized_path(workspace_root);
188 let listed_sources = workspace_files
189 .into_iter()
190 .map(|file| canonical_or_normalized_path(&file.abs_path()))
191 .collect::<HashSet<_>>();
192 let mut missing_count = 0usize;
193 let mut sample = BTreeSet::new();
194 for source in &self.database_sources {
195 let Some(relative) = (|| {
196 let relative = source.strip_prefix(&workspace_root).ok()?;
197 (!relative.as_os_str().is_empty() && !listed_sources.contains(source))
198 .then(|| relative.to_path_buf().normalize())
199 })() else {
200 continue;
201 };
202 missing_count = missing_count.saturating_add(1);
203 sample.insert(relative);
204 if sample.len() > MAX_MISSING_WORKSPACE_SOURCE_SAMPLE {
205 sample.pop_last();
206 }
207 }
208 CppCompileDatabaseCoverage {
209 missing_workspace_source_count: missing_count,
210 missing_workspace_source_sample: sample.into_iter().collect(),
211 }
212 }
213
214 pub fn contexts_for(&self, file: &ProjectFile) -> &[CppCompileContext] {
221 self.by_source
222 .get(&canonical_or_normalized_path(&file.abs_path()))
223 .map_or(&[], Vec::as_slice)
224 }
225
226 pub fn resolve_external_angle_include(
234 &self,
235 file: &ProjectFile,
236 include: &Path,
237 ) -> CppExternalIncludeResolution {
238 self.resolve_external_include(file, &file.abs_path(), include, false)
239 }
240
241 pub fn resolve_external_include(
249 &self,
250 file: &ProjectFile,
251 including_file: &Path,
252 include: &Path,
253 quoted: bool,
254 ) -> CppExternalIncludeResolution {
255 let contexts = self.contexts_for(file);
256 let Some(first_context) = contexts.first() else {
257 return CppExternalIncludeResolution::MissingCompileContext;
258 };
259 let first =
260 first_context.resolve_external_include(file.root(), including_file, include, quoted);
261 if contexts.iter().skip(1).any(|context| {
262 context.resolve_external_include(file.root(), including_file, include, quoted) != first
263 }) {
264 return CppExternalIncludeResolution::Conflicting;
265 }
266 match first {
267 Some((root, header)) => CppExternalIncludeResolution::Declared { root, header },
268 None => CppExternalIncludeResolution::Undeclared,
269 }
270 }
271
272 pub fn external_angle_include_roots(&self, workspace_root: &Path) -> Vec<PathBuf> {
279 let mut roots = self
280 .by_source
281 .values()
282 .flatten()
283 .flat_map(|context| context.external_angle_include_roots(workspace_root))
284 .map(Path::to_path_buf)
285 .collect::<Vec<_>>();
286 roots.sort();
287 roots.dedup();
288 roots
289 }
290}
291
292impl CppCompileContext {
293 pub fn tu_language(&self, source_path: &Path) -> CompiledLanguage {
305 if let Some(language) = self.explicit_language {
306 return language;
307 }
308 if let Some(language) = self.std_language {
309 return language;
310 }
311 if self
312 .driver_basename
313 .as_deref()
314 .is_some_and(|driver| driver.ends_with("++"))
315 {
316 return CompiledLanguage::Cpp;
317 }
318 if source_path
319 .extension()
320 .and_then(|extension| extension.to_str())
321 == Some("c")
322 {
323 CompiledLanguage::C
324 } else {
325 CompiledLanguage::Cpp
326 }
327 }
328
329 pub fn external_angle_include_roots<'a>(
331 &'a self,
332 workspace_root: &'a Path,
333 ) -> impl Iterator<Item = &'a Path> + 'a {
334 self.external_include_roots(workspace_root, false)
335 }
336
337 fn external_include_roots<'a>(
338 &'a self,
339 workspace_root: &'a Path,
340 quoted: bool,
341 ) -> impl Iterator<Item = &'a Path> + 'a {
342 self.include_search_roots.iter().filter_map(move |root| {
343 (quoted || root.kind != CppIncludeSearchRootKind::Quote)
344 .then_some(root)
345 .filter(|root| {
346 root.kind == CppIncludeSearchRootKind::System
347 || !root.path.starts_with(workspace_root)
348 })
349 .map(|root| root.path.as_path())
350 })
351 }
352
353 fn resolve_external_include(
354 &self,
355 workspace_root: &Path,
356 including_file: &Path,
357 include: &Path,
358 quoted: bool,
359 ) -> Option<(PathBuf, PathBuf)> {
360 if include.is_absolute()
361 || (!quoted
362 && include
363 .components()
364 .any(|component| !matches!(component, Component::Normal(_))))
365 || (quoted
366 && include.components().any(|component| {
367 matches!(component, Component::RootDir | Component::Prefix(_))
368 }))
369 {
370 return None;
371 }
372
373 let workspace_root = canonical_or_normalized_path(workspace_root);
374 if quoted {
375 let including_directory = including_file.parent()?;
376 if let Ok(candidate) = including_directory.join(include).canonicalize()
377 && candidate.is_file()
378 {
379 if candidate.starts_with(&workspace_root) {
380 return None;
381 }
382 if let Some(root) = self
383 .external_include_roots(&workspace_root, true)
384 .filter_map(|root| {
385 let root = root.canonicalize().ok()?;
386 candidate.starts_with(&root).then_some(root)
387 })
388 .next()
389 {
390 return Some((root, candidate));
391 }
392 return None;
393 }
394 }
395
396 self.external_include_roots(&workspace_root, quoted)
397 .filter_map(|root| {
398 let root = root.canonicalize().ok()?;
399 let candidate = root.join(include).canonicalize().ok()?;
400 (candidate.starts_with(&root) && candidate.is_file()).then_some((root, candidate))
401 })
402 .next()
403 }
404}
405
406#[derive(Debug, Deserialize)]
407struct CompilationDatabaseEntry {
408 directory: PathBuf,
409 file: PathBuf,
410 arguments: Option<Vec<String>>,
411 command: Option<String>,
412}
413
414impl CompilationDatabaseEntry {
415 fn source_path(&self, workspace_root: &Path) -> Option<PathBuf> {
416 absolute_path(
417 &command_directory(workspace_root, &self.directory)?,
418 &self.file,
419 )
420 }
421
422 fn compile_context(&self, workspace_root: &Path) -> Option<CppCompileContext> {
423 let arguments = match &self.arguments {
424 Some(arguments) if !arguments.is_empty() => arguments.clone(),
425 Some(_) => return None,
426 None => shlex::split(self.command.as_deref()?)?,
427 };
428 parse_compile_arguments(
429 &command_directory(workspace_root, &self.directory)?,
430 &arguments,
431 )
432 }
433}
434
435fn command_directory(workspace_root: &Path, directory: &Path) -> Option<PathBuf> {
436 absolute_path(workspace_root, directory)
437}
438
439fn parse_compile_arguments(directory: &Path, arguments: &[String]) -> Option<CppCompileContext> {
440 if arguments.is_empty() {
441 return None;
442 }
443
444 let driver_basename = driver_basename(&arguments[0]);
445 let mut project_include_roots = Vec::new();
446 let mut system_include_roots = Vec::new();
447 let mut forced_includes = Vec::new();
448 let mut defined_macros = HashSet::default();
449 let mut include_search_roots = Vec::new();
450 let mut explicit_language = None;
451 let mut std_language = None;
452 let mut index = 1;
453 while index < arguments.len() {
454 let argument = &arguments[index];
455 match argument.as_str() {
456 "-x" => {
457 if let Some(language) = language_from_x_value(arguments.get(index + 1)?) {
458 explicit_language = Some(language);
459 }
460 index += 2;
461 }
462 "/TC" => {
463 explicit_language = Some(CompiledLanguage::C);
464 index += 1;
465 }
466 "/TP" => {
467 explicit_language = Some(CompiledLanguage::Cpp);
468 index += 1;
469 }
470 "-I" | "/I" => {
471 let path = argument_path(directory, arguments.get(index + 1)?)?;
472 project_include_roots.push(path.clone());
473 include_search_roots.push(CppIncludeSearchRoot {
474 path,
475 kind: CppIncludeSearchRootKind::Project,
476 });
477 index += 2;
478 }
479 "-iquote" => {
480 let path = argument_path(directory, arguments.get(index + 1)?)?;
481 project_include_roots.push(path.clone());
482 include_search_roots.push(CppIncludeSearchRoot {
483 path,
484 kind: CppIncludeSearchRootKind::Quote,
485 });
486 index += 2;
487 }
488 "-isystem" | "/external:I" | "/imsvc" => {
489 let path = argument_path(directory, arguments.get(index + 1)?)?;
490 system_include_roots.push(path.clone());
491 include_search_roots.push(CppIncludeSearchRoot {
492 path,
493 kind: CppIncludeSearchRootKind::System,
494 });
495 index += 2;
496 }
497 "-include" => {
498 forced_includes.push(argument_path(directory, arguments.get(index + 1)?)?);
499 index += 2;
500 }
501 "-D" => {
502 defined_macros.insert(macro_name(arguments.get(index + 1)?)?);
503 index += 2;
504 }
505 "-U" => {
510 defined_macros.remove(¯o_name(arguments.get(index + 1)?)?);
511 index += 2;
512 }
513 _ => {
514 if let Some(path) = argument
515 .strip_prefix("/external:I")
516 .or_else(|| argument.strip_prefix("/imsvc"))
517 {
518 let path = argument_path(directory, path)?;
519 system_include_roots.push(path.clone());
520 include_search_roots.push(CppIncludeSearchRoot {
521 path,
522 kind: CppIncludeSearchRootKind::System,
523 });
524 } else if let Some(path) = argument
525 .strip_prefix("-I")
526 .or_else(|| argument.strip_prefix("/I"))
527 {
528 let path = argument_path(directory, path)?;
529 project_include_roots.push(path.clone());
530 include_search_roots.push(CppIncludeSearchRoot {
531 path,
532 kind: CppIncludeSearchRootKind::Project,
533 });
534 } else if let Some(path) = argument.strip_prefix("-iquote") {
535 let path = argument_path(directory, path)?;
536 project_include_roots.push(path.clone());
537 include_search_roots.push(CppIncludeSearchRoot {
538 path,
539 kind: CppIncludeSearchRootKind::Quote,
540 });
541 } else if let Some(path) = argument.strip_prefix("-isystem") {
542 let path = argument_path(directory, path)?;
543 system_include_roots.push(path.clone());
544 include_search_roots.push(CppIncludeSearchRoot {
545 path,
546 kind: CppIncludeSearchRootKind::System,
547 });
548 } else if let Some(definition) = argument.strip_prefix("-D") {
549 defined_macros.insert(macro_name(definition)?);
550 } else if let Some(name) = argument.strip_prefix("-U") {
551 defined_macros.remove(¯o_name(name)?);
552 } else if let Some(value) = argument.strip_prefix("-std=") {
553 std_language = Some(if value.contains("++") {
554 CompiledLanguage::Cpp
555 } else {
556 CompiledLanguage::C
557 });
558 } else if let Some(value) = argument.strip_prefix("-x")
559 && let Some(language) = language_from_x_value(value)
560 {
561 explicit_language = Some(language);
562 }
563 index += 1;
564 }
565 }
566 }
567
568 Some(CppCompileContext {
569 project_include_roots,
570 system_include_roots,
571 forced_includes,
572 defined_macros,
573 include_search_roots,
574 driver_basename,
575 explicit_language,
576 std_language,
577 })
578}
579
580fn driver_basename(driver: &str) -> Option<String> {
585 Path::new(driver)
586 .file_stem()
587 .and_then(|stem| stem.to_str())
588 .map(str::to_owned)
589}
590
591fn language_from_x_value(value: &str) -> Option<CompiledLanguage> {
595 let family = value
596 .strip_suffix("-header")
597 .or_else(|| value.strip_suffix("-cpp-output"))
598 .unwrap_or(value);
599 match family {
600 "c" => Some(CompiledLanguage::C),
601 "c++" => Some(CompiledLanguage::Cpp),
602 _ => None,
603 }
604}
605
606fn argument_path(directory: &Path, raw: &str) -> Option<PathBuf> {
607 if raw.is_empty() {
608 return None;
609 }
610 absolute_path(directory, Path::new(raw))
611}
612
613fn absolute_path(directory: &Path, path: &Path) -> Option<PathBuf> {
614 let path = if path.is_absolute() {
615 path.to_path_buf()
616 } else {
617 directory.join(path)
618 }
619 .normalize();
620 path.is_absolute().then_some(path)
621}
622
623fn canonical_or_normalized_path(path: &Path) -> PathBuf {
629 let path = path.to_path_buf().normalize();
630 let mut missing_tail: Vec<OsString> = Vec::new();
631 let mut current = path.as_path();
632 loop {
633 if let Ok(canonical) = current.canonicalize() {
634 let mut result = canonical.normalize();
635 for component in missing_tail.iter().rev() {
636 result.push(component);
637 }
638 return result.normalize();
639 }
640 let Some(name) = current.file_name() else {
641 return path;
642 };
643 missing_tail.push(name.to_owned());
644 let Some(parent) = current.parent() else {
645 return path;
646 };
647 current = parent;
648 }
649}
650
651fn macro_name(definition: &str) -> Option<String> {
652 let end = definition.find('=').unwrap_or(definition.len());
653 let name = &definition[..end];
654 (!name.is_empty()).then(|| name.to_string())
655}
656
657#[cfg(test)]
658mod tests {
659 use super::{CompiledLanguage, CppCompileContexts, CppExternalIncludeResolution};
660 use brokk_bifrost_core::analyzer::project::{FilesystemProject, Project, TestProject};
661 use brokk_bifrost_core::analyzer::{Language, ProjectFile};
662 use brokk_bifrost_test_macros::hardwon;
663 use std::path::{Path, PathBuf};
664
665 fn context(arguments: &[&str]) -> super::CppCompileContext {
669 let arguments = arguments
670 .iter()
671 .map(|argument| argument.to_string())
672 .collect::<Vec<_>>();
673 super::parse_compile_arguments(Path::new("/workspace"), &arguments)
674 .expect("non-empty argument vector parses")
675 }
676
677 fn project_with_database(database: Option<&str>) -> (tempfile::TempDir, TestProject) {
678 let temp = tempfile::tempdir().expect("temp dir");
679 let root = temp.path().canonicalize().expect("canonical root");
680 ProjectFile::new(root.clone(), "src/main.cpp")
681 .write("int main() { return 0; }")
682 .expect("source");
683 if let Some(database) = database {
684 ProjectFile::new(root.clone(), "compile_commands.json")
685 .write(database)
686 .expect("database");
687 }
688 (temp, TestProject::new(root, Language::Cpp))
689 }
690
691 #[test]
692 fn arguments_entry_collects_include_paths_and_macro_names() {
693 let (_temp, project) = project_with_database(Some(
694 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-iquotequotes","-isystem","system-include","-DDEBUG=1","-D","FEATURE","-c","src/main.cpp"]}]"#,
695 ));
696 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
697 let contexts = CppCompileContexts::load(&project);
698 let [context] = contexts.contexts_for(&file) else {
699 panic!("one matching context");
700 };
701
702 assert_eq!(
703 vec![
704 project.root_path().join("include"),
705 project.root_path().join("quotes"),
706 ],
707 context.project_include_roots
708 );
709 assert_eq!(
710 vec![project.root_path().join("system-include")],
711 context.system_include_roots
712 );
713 assert!(context.defined_macros.contains("DEBUG"));
714 assert!(context.defined_macros.contains("FEATURE"));
715 }
716
717 #[test]
718 fn quoted_command_entry_is_tokenized_without_executing_it() {
719 let (_temp, project) = project_with_database(Some(
720 r#"[{"directory":".","file":"src/main.cpp","command":"clang++ -I 'project include' -DNAME=\\\"two words\\\" -c src/main.cpp"}]"#,
721 ));
722 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
723 let contexts = CppCompileContexts::load(&project);
724 let [context] = contexts.contexts_for(&file) else {
725 panic!("one matching context");
726 };
727
728 assert_eq!(
729 vec![project.root_path().join("project include")],
730 context.project_include_roots
731 );
732 assert!(context.defined_macros.contains("NAME"));
733 }
734
735 #[test]
736 #[hardwon("#2011: conditional C visibility ignored command-order macro state")]
737 fn undefine_flags_apply_in_command_order() {
738 let context = context(&[
739 "cc",
740 "-DKEPT",
741 "-DCANCELLED",
742 "-U",
743 "CANCELLED",
744 "-UREVIVED",
745 "-DREVIVED",
746 "-UNEVER_DEFINED",
747 ]);
748 assert!(context.defined_macros.contains("KEPT"));
749 assert!(context.defined_macros.contains("REVIVED"));
750 assert!(!context.defined_macros.contains("CANCELLED"));
751 assert!(!context.defined_macros.contains("NEVER_DEFINED"));
752 }
753
754 #[test]
755 fn a_file_compiled_in_two_configurations_keeps_both() {
756 let (_temp, project) = project_with_database(Some(
757 r#"[
758 {"directory":".","file":"src/main.cpp","arguments":["clang++","-c","src/main.cpp"]},
759 {"directory":".","file":"src/main.cpp","arguments":["clang++","-DOTHER","-c","src/main.cpp"]},
760 {"directory":".","file":"src/other.cpp","arguments":["clang++","-c","src/other.cpp"]}
761 ]"#,
762 ));
763 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
764 let contexts = CppCompileContexts::load(&project);
765 let candidates = contexts.contexts_for(&file);
766
767 assert_eq!(2, candidates.len());
770 assert!(candidates[0].defined_macros.is_empty());
771 assert!(candidates[1].defined_macros.contains("OTHER"));
772 }
773
774 #[test]
775 fn repeated_identical_entries_are_one_configuration() {
776 let (_temp, project) = project_with_database(Some(
777 r#"[
778 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]},
779 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]}
780 ]"#,
781 ));
782 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
783 let contexts = CppCompileContexts::load(&project);
784
785 assert_eq!(1, contexts.contexts_for(&file).len());
788 }
789
790 #[test]
791 fn missing_workspace_sources_are_canonical_bounded_and_deterministic() {
792 let (_temp, project) = project_with_database(None);
793 let root = project.root_path().to_path_buf();
794 ProjectFile::new(root.clone(), "src/present.cpp")
795 .write("int present() { return 0; }")
796 .expect("present source");
797 ProjectFile::new(root.clone(), "src/present.c")
798 .write("int present_c(void) { return 0; }")
799 .expect("present C source");
800 ProjectFile::new(root.clone(), "include/present.hin")
801 .write("int generated_declaration(void);")
802 .expect("present C header template");
803
804 let mut entries = vec![serde_json::json!({
805 "directory": ".",
806 "file": "src/./present.cpp",
807 "arguments": ["clang++", "-c", "src/present.cpp"]
808 })];
809 entries.push(serde_json::json!({
810 "directory": ".",
811 "file": "src/present.c",
812 "arguments": ["clang", "-c", "src/present.c"]
813 }));
814 entries.push(serde_json::json!({
815 "directory": ".",
816 "file": "include/present.hin",
817 "arguments": ["clang", "-x", "c-header", "include/present.hin"]
818 }));
819 entries.extend(
820 (0..(super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3)).map(|index| {
821 serde_json::json!({
822 "directory": ".",
823 "file": format!("generated/./unit-{index:02}.cpp"),
824 "arguments": ["clang++", "-c", format!("generated/unit-{index:02}.cpp")]
825 })
826 }),
827 );
828 entries.push(serde_json::json!({
831 "directory": "generated",
832 "file": "../generated/unit-00.cpp",
833 "arguments": ["clang++", "-c", "../generated/unit-00.cpp"]
834 }));
835 ProjectFile::new(root.clone(), "compile_commands.json")
836 .write(serde_json::to_string(&entries).expect("database JSON"))
837 .expect("database");
838
839 let contexts = CppCompileContexts::load(&project);
840 let listed = project
841 .analyzable_files(Language::Cpp)
842 .expect("C++ listing");
843 let coverage = contexts.missing_workspace_sources(&root, &listed);
844
845 assert_eq!(
846 super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3,
847 coverage.missing_workspace_source_count()
848 );
849 assert_eq!(
850 (0..super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE)
851 .map(|index| Path::new("generated").join(format!("unit-{index:02}.cpp")))
852 .collect::<Vec<_>>(),
853 coverage.missing_workspace_source_sample()
854 );
855 assert!(
856 !coverage
857 .missing_workspace_source_sample()
858 .contains(&PathBuf::from("src/present.cpp"))
859 );
860
861 let reversed = entries.into_iter().rev().collect::<Vec<_>>();
862 ProjectFile::new(root.clone(), "compile_commands.json")
863 .write(serde_json::to_string(&reversed).expect("reversed database JSON"))
864 .expect("reversed database");
865 let reversed_coverage =
866 CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
867 assert_eq!(coverage, reversed_coverage);
868 }
869
870 #[test]
871 fn missing_workspace_sources_excludes_outside_root_and_ignored_files() {
872 let temp = tempfile::tempdir().expect("workspace root");
873 let root = temp.path().canonicalize().expect("canonical root");
874 let outside = tempfile::tempdir().expect("outside root");
875 let outside_source = outside
876 .path()
877 .canonicalize()
878 .expect("canonical outside root")
879 .join("outside.cpp")
880 .to_string_lossy()
881 .into_owned();
882 ProjectFile::new(root.clone(), "src/present.cpp")
883 .write("int present() { return 0; }")
884 .expect("present source");
885 ProjectFile::new(root.clone(), "ignored.cpp")
886 .write("int ignored() { return 0; }")
887 .expect("ignored source");
888 ProjectFile::new(root.clone(), ".bifrostignore")
889 .write("ignored.cpp\n")
890 .expect("ignore file");
891 let database = serde_json::json!([
892 {
893 "directory": ".",
894 "file": "src/../src/present.cpp",
895 "arguments": ["clang++", "-c", "src/present.cpp"]
896 },
897 {
898 "directory": ".",
899 "file": "ignored.cpp",
900 "arguments": ["clang++", "-c", "ignored.cpp"]
901 },
902 {
903 "directory": ".",
904 "file": outside_source.clone(),
905 "arguments": ["clang++", "-c", outside_source]
906 }
907 ]);
908 ProjectFile::new(root.clone(), "compile_commands.json")
909 .write(database.to_string())
910 .expect("database");
911 let project = FilesystemProject::new(root.clone()).expect("filesystem project");
912 let listed = project
913 .analyzable_files(Language::Cpp)
914 .expect("C++ listing");
915 assert!(
916 listed
917 .iter()
918 .any(|file| file.rel_path() == Path::new("src/present.cpp"))
919 );
920 assert!(
921 !listed
922 .iter()
923 .any(|file| file.rel_path() == Path::new("ignored.cpp"))
924 );
925
926 let coverage = CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
927 assert_eq!(1, coverage.missing_workspace_source_count());
928 assert_eq!(
929 &[PathBuf::from("ignored.cpp")],
930 coverage.missing_workspace_source_sample()
931 );
932 }
933
934 #[test]
935 fn a_quoted_include_checks_the_including_directory_before_external_roots() {
936 let external = tempfile::tempdir().expect("external root");
937 let external_root = external
938 .path()
939 .canonicalize()
940 .expect("canonical external root");
941 std::fs::write(external_root.join("vendor.hpp"), "class External {};\n")
942 .expect("external vendor header");
943 std::fs::write(external_root.join("from-search.hpp"), "class Search {};\n")
944 .expect("external search header");
945 let database = serde_json::json!([{
946 "directory": ".",
947 "file": "src/main.cpp",
948 "arguments": [
949 "clang++",
950 "-I",
951 external_root,
952 "-c",
953 "src/main.cpp"
954 ]
955 }])
956 .to_string();
957 let (_temp, project) = project_with_database(Some(&database));
958 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
959 let source_path = file.abs_path();
960 let source_directory = source_path.parent().expect("source directory");
961 std::fs::create_dir_all(source_directory).expect("source directory");
962 std::fs::write(source_directory.join("vendor.hpp"), "class Local {};\n")
963 .expect("local vendor header");
964 let contexts = CppCompileContexts::load(&project);
965
966 assert_eq!(
967 CppExternalIncludeResolution::Undeclared,
968 contexts.resolve_external_include(
969 &file,
970 &file.abs_path(),
971 std::path::Path::new("vendor.hpp"),
972 true,
973 )
974 );
975 assert!(matches!(
976 contexts.resolve_external_include(
977 &file,
978 &file.abs_path(),
979 std::path::Path::new("from-search.hpp"),
980 true,
981 ),
982 CppExternalIncludeResolution::Declared { .. }
983 ));
984 assert!(matches!(
985 contexts.resolve_external_include(
986 &file,
987 &file.abs_path(),
988 std::path::Path::new("vendor.hpp"),
989 false,
990 ),
991 CppExternalIncludeResolution::Declared { .. }
992 ));
993 }
994
995 #[test]
996 fn configurations_must_agree_on_the_external_header() {
997 let first = tempfile::tempdir().expect("first root");
998 let second = tempfile::tempdir().expect("second root");
999 let first = first.path().canonicalize().expect("canonical first root");
1000 let second = second.path().canonicalize().expect("canonical second root");
1001 std::fs::write(first.join("vector"), "namespace std { class vector {}; }")
1002 .expect("first header");
1003 std::fs::write(second.join("vector"), "namespace std { class vector {}; }")
1004 .expect("second header");
1005 let database = serde_json::json!([
1006 {
1007 "directory": ".",
1008 "file": "src/main.cpp",
1009 "arguments": ["clang++", "-isystem", first, "-c", "src/main.cpp"]
1010 },
1011 {
1012 "directory": ".",
1013 "file": "src/main.cpp",
1014 "arguments": ["clang++", "-isystem", second, "-c", "src/main.cpp"]
1015 }
1016 ])
1017 .to_string();
1018 let (_temp, project) = project_with_database(Some(&database));
1019 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1020
1021 assert_eq!(
1022 CppExternalIncludeResolution::Conflicting,
1023 CppCompileContexts::load(&project)
1024 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1025 );
1026 }
1027
1028 #[test]
1029 fn external_include_cannot_escape_its_declared_root() {
1030 let external = tempfile::tempdir().expect("external root");
1031 let root = external.path().canonicalize().expect("canonical root");
1032 std::fs::create_dir_all(root.join("include")).expect("include directory");
1033 std::fs::write(root.join("outside.hpp"), "class Outside {};").expect("outside header");
1034 let database = serde_json::json!([{
1035 "directory": ".",
1036 "file": "src/main.cpp",
1037 "arguments": [
1038 "clang++",
1039 "-isystem",
1040 root.join("include"),
1041 "-c",
1042 "src/main.cpp"
1043 ]
1044 }])
1045 .to_string();
1046 let (_temp, project) = project_with_database(Some(&database));
1047 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1048
1049 assert_eq!(
1050 CppExternalIncludeResolution::Undeclared,
1051 CppCompileContexts::load(&project)
1052 .resolve_external_angle_include(&file, std::path::Path::new("../outside.hpp"))
1053 );
1054 assert_eq!(
1055 CppExternalIncludeResolution::Undeclared,
1056 CppCompileContexts::load(&project)
1057 .resolve_external_angle_include(&file, &root.join("outside.hpp"))
1058 );
1059 }
1060
1061 #[test]
1062 fn msvc_project_and_system_include_flags_preserve_search_order() {
1063 let (_temp, project) = project_with_database(Some(
1064 r#"[{"directory":".","file":"src/main.cpp","arguments":["cl.exe","/I","vendor/include","/external:Ifake-system/include","/imsvc","toolchain/include","/c","src/main.cpp"]}]"#,
1065 ));
1066 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1067 let contexts = CppCompileContexts::load(&project);
1068 let [context] = contexts.contexts_for(&file) else {
1069 panic!("one MSVC compile context");
1070 };
1071
1072 assert_eq!(1, context.project_include_roots.len());
1073 assert_eq!(2, context.system_include_roots.len());
1074 }
1075
1076 #[test]
1077 fn driver_basename_ending_in_plus_plus_selects_cpp_over_a_c_extension() {
1078 let cx = context(&["clang++", "-c", "file.c"]);
1079 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1080
1081 let cx = context(&["arm-none-eabi-gcc", "-c", "file.c"]);
1084 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1085
1086 let cx = context(&["arm-none-eabi-g++", "-c", "file.h"]);
1088 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.h")));
1089 }
1090
1091 #[test]
1092 fn driver_exe_suffix_is_stripped_before_the_plus_plus_check() {
1093 let cx = context(&["clang++.exe", "-c", "file.c"]);
1094 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1095 }
1096
1097 #[test]
1098 fn std_family_outranks_the_driver_basename() {
1099 let cx = context(&["g++", "-std=gnu11", "-c", "file.c"]);
1102 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1103
1104 let cx = context(&["gcc", "-std=c++17", "-c", "file.cc"]);
1105 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1106
1107 let cx = context(&["gcc", "-std=c17", "-c", "file.cc"]);
1108 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cc")));
1109
1110 let cx = context(&["gcc", "-std=gnu++20", "-c", "file.c"]);
1111 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1112 }
1113
1114 #[test]
1115 fn explicit_x_split_form_outranks_std_and_driver() {
1116 let cx = context(&["clang", "-x", "c", "-std=c++17", "-c", "file.cpp"]);
1117 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1118 }
1119
1120 #[test]
1121 fn explicit_x_glued_form_is_recognized() {
1122 let cx = context(&["gcc", "-xc++", "-c", "file.c"]);
1123 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1124
1125 let cx = context(&["g++", "-xc", "-c", "file.cpp"]);
1126 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1127 }
1128
1129 #[test]
1130 fn explicit_x_on_a_c_file_overrides_the_extension() {
1131 let cx = context(&["gcc", "-x", "c++", "-c", "file.c"]);
1134 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1135 }
1136
1137 #[test]
1138 fn msvc_tc_and_tp_force_the_language_regardless_of_extension() {
1139 let cx = context(&["cl.exe", "/TC", "/c", "file.cpp"]);
1140 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1141
1142 let cx = context(&["cl.exe", "/TP", "/c", "file.c"]);
1143 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1144 }
1145
1146 #[test]
1147 fn missing_or_malformed_database_has_no_context() {
1148 let (_temp, project) = project_with_database(None);
1149 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1150 assert!(
1151 CppCompileContexts::load(&project)
1152 .contexts_for(&file)
1153 .is_empty()
1154 );
1155
1156 let (_temp, project) = project_with_database(Some("not json"));
1157 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1158 assert!(
1159 CppCompileContexts::load(&project)
1160 .contexts_for(&file)
1161 .is_empty()
1162 );
1163 }
1164
1165 #[test]
1166 fn an_explicit_system_root_declares_an_angle_include() {
1167 let (_temp, project) = project_with_database(Some(
1168 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","fake-system/include","-c","src/main.cpp"]}]"#,
1169 ));
1170 let header = ProjectFile::new(
1171 project.root_path().to_path_buf(),
1172 "fake-system/include/vector",
1173 );
1174 header
1175 .write("namespace std { class vector {}; }")
1176 .expect("header");
1177 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1178
1179 assert_eq!(
1180 CppExternalIncludeResolution::Declared {
1181 root: project
1182 .root_path()
1183 .join("fake-system/include")
1184 .canonicalize()
1185 .expect("canonical root"),
1186 header: header.abs_path().canonicalize().expect("canonical header"),
1187 },
1188 CppCompileContexts::load(&project)
1189 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1190 );
1191 }
1192
1193 #[test]
1194 fn an_external_project_root_declares_but_a_workspace_project_root_does_not() {
1195 let external = tempfile::tempdir().expect("external root");
1196 let external_root = external
1197 .path()
1198 .canonicalize()
1199 .expect("canonical external root");
1200 std::fs::write(external_root.join("vendor.hpp"), "class Vendor {};")
1201 .expect("external header");
1202 let database = serde_json::json!([{
1203 "directory": ".",
1204 "file": "src/main.cpp",
1205 "arguments": [
1206 "clang++",
1207 "-I",
1208 external_root,
1209 "-I",
1210 "include",
1211 "-c",
1212 "src/main.cpp"
1213 ]
1214 }])
1215 .to_string();
1216 let (_temp, project) = project_with_database(Some(&database));
1217 ProjectFile::new(project.root_path().to_path_buf(), "include/local.hpp")
1218 .write("class Local {};")
1219 .expect("local header");
1220 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1221 let contexts = CppCompileContexts::load(&project);
1222
1223 assert!(matches!(
1224 contexts.resolve_external_angle_include(&file, std::path::Path::new("vendor.hpp")),
1225 CppExternalIncludeResolution::Declared { .. }
1226 ));
1227 assert_eq!(
1228 CppExternalIncludeResolution::Undeclared,
1229 contexts.resolve_external_angle_include(&file, std::path::Path::new("local.hpp"))
1230 );
1231 }
1232
1233 #[test]
1234 fn default_by_extension_c_is_c_and_everything_else_is_cpp() {
1235 let cx = context(&["cc", "-c", "file.c"]);
1236 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1237
1238 let cx = context(&["cc", "-c", "file.cc"]);
1239 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1240 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.hpp")));
1241 let cx = context(&["cl.exe", "/c", "file.c"]);
1242 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1243
1244 let cx = context(&["cl.exe", "/c", "file.cpp"]);
1245 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cpp")));
1246 }
1247}