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
273impl CppCompileContext {
274 pub fn tu_language(&self, source_path: &Path) -> CompiledLanguage {
286 if let Some(language) = self.explicit_language {
287 return language;
288 }
289 if let Some(language) = self.std_language {
290 return language;
291 }
292 if self
293 .driver_basename
294 .as_deref()
295 .is_some_and(|driver| driver.ends_with("++"))
296 {
297 return CompiledLanguage::Cpp;
298 }
299 if source_path
300 .extension()
301 .and_then(|extension| extension.to_str())
302 == Some("c")
303 {
304 CompiledLanguage::C
305 } else {
306 CompiledLanguage::Cpp
307 }
308 }
309
310 fn external_include_roots<'a>(
311 &'a self,
312 workspace_root: &'a Path,
313 quoted: bool,
314 ) -> impl Iterator<Item = &'a Path> + 'a {
315 self.include_search_roots.iter().filter_map(move |root| {
316 (quoted || root.kind != CppIncludeSearchRootKind::Quote)
317 .then_some(root)
318 .filter(|root| {
319 root.kind == CppIncludeSearchRootKind::System
320 || !root.path.starts_with(workspace_root)
321 })
322 .map(|root| root.path.as_path())
323 })
324 }
325
326 fn resolve_external_include(
327 &self,
328 workspace_root: &Path,
329 including_file: &Path,
330 include: &Path,
331 quoted: bool,
332 ) -> Option<(PathBuf, PathBuf)> {
333 if include.is_absolute()
334 || (!quoted
335 && include
336 .components()
337 .any(|component| !matches!(component, Component::Normal(_))))
338 || (quoted
339 && include.components().any(|component| {
340 matches!(component, Component::RootDir | Component::Prefix(_))
341 }))
342 {
343 return None;
344 }
345
346 let workspace_root = canonical_or_normalized_path(workspace_root);
347 if quoted {
348 let including_directory = including_file.parent()?;
349 if let Ok(candidate) = including_directory.join(include).canonicalize()
350 && candidate.is_file()
351 {
352 if candidate.clone().normalize().starts_with(&workspace_root) {
355 return None;
356 }
357 if let Some(root) = self
358 .external_include_roots(&workspace_root, true)
359 .filter_map(|root| {
360 let root = root.canonicalize().ok()?;
361 candidate.starts_with(&root).then_some(root)
362 })
363 .next()
364 {
365 return Some((root, candidate));
366 }
367 return None;
368 }
369 }
370
371 self.external_include_roots(&workspace_root, quoted)
372 .filter_map(|root| {
373 let root = root.canonicalize().ok()?;
374 let candidate = root.join(include).canonicalize().ok()?;
375 (candidate.starts_with(&root) && candidate.is_file()).then_some((root, candidate))
376 })
377 .next()
378 }
379}
380
381#[derive(Debug, Deserialize)]
382struct CompilationDatabaseEntry {
383 directory: PathBuf,
384 file: PathBuf,
385 arguments: Option<Vec<String>>,
386 command: Option<String>,
387}
388
389impl CompilationDatabaseEntry {
390 fn source_path(&self, workspace_root: &Path) -> Option<PathBuf> {
391 absolute_path(
392 &command_directory(workspace_root, &self.directory)?,
393 &self.file,
394 )
395 }
396
397 fn compile_context(&self, workspace_root: &Path) -> Option<CppCompileContext> {
398 let arguments = match &self.arguments {
399 Some(arguments) if !arguments.is_empty() => arguments.clone(),
400 Some(_) => return None,
401 None => shlex::split(self.command.as_deref()?)?,
402 };
403 parse_compile_arguments(
404 &command_directory(workspace_root, &self.directory)?,
405 &arguments,
406 )
407 }
408}
409
410fn command_directory(workspace_root: &Path, directory: &Path) -> Option<PathBuf> {
411 absolute_path(workspace_root, directory)
412}
413
414fn parse_compile_arguments(directory: &Path, arguments: &[String]) -> Option<CppCompileContext> {
415 if arguments.is_empty() {
416 return None;
417 }
418
419 let driver_basename = driver_basename(&arguments[0]);
420 let mut project_include_roots = Vec::new();
421 let mut system_include_roots = Vec::new();
422 let mut forced_includes = Vec::new();
423 let mut defined_macros = HashSet::default();
424 let mut include_search_roots = Vec::new();
425 let mut explicit_language = None;
426 let mut std_language = None;
427 let mut index = 1;
428 while index < arguments.len() {
429 let argument = &arguments[index];
430 match argument.as_str() {
431 "-x" => {
432 if let Some(language) = language_from_x_value(arguments.get(index + 1)?) {
433 explicit_language = Some(language);
434 }
435 index += 2;
436 }
437 "/TC" => {
438 explicit_language = Some(CompiledLanguage::C);
439 index += 1;
440 }
441 "/TP" => {
442 explicit_language = Some(CompiledLanguage::Cpp);
443 index += 1;
444 }
445 "-I" | "/I" => {
446 let path = argument_path(directory, arguments.get(index + 1)?)?;
447 project_include_roots.push(path.clone());
448 include_search_roots.push(CppIncludeSearchRoot {
449 path,
450 kind: CppIncludeSearchRootKind::Project,
451 });
452 index += 2;
453 }
454 "-iquote" => {
455 let path = argument_path(directory, arguments.get(index + 1)?)?;
456 project_include_roots.push(path.clone());
457 include_search_roots.push(CppIncludeSearchRoot {
458 path,
459 kind: CppIncludeSearchRootKind::Quote,
460 });
461 index += 2;
462 }
463 "-isystem" | "/external:I" | "/imsvc" => {
464 let path = argument_path(directory, arguments.get(index + 1)?)?;
465 system_include_roots.push(path.clone());
466 include_search_roots.push(CppIncludeSearchRoot {
467 path,
468 kind: CppIncludeSearchRootKind::System,
469 });
470 index += 2;
471 }
472 "-include" => {
473 forced_includes.push(argument_path(directory, arguments.get(index + 1)?)?);
474 index += 2;
475 }
476 "-D" => {
477 defined_macros.insert(macro_name(arguments.get(index + 1)?)?);
478 index += 2;
479 }
480 "-U" => {
485 defined_macros.remove(¯o_name(arguments.get(index + 1)?)?);
486 index += 2;
487 }
488 _ => {
489 if let Some(path) = argument
490 .strip_prefix("/external:I")
491 .or_else(|| argument.strip_prefix("/imsvc"))
492 {
493 let path = argument_path(directory, path)?;
494 system_include_roots.push(path.clone());
495 include_search_roots.push(CppIncludeSearchRoot {
496 path,
497 kind: CppIncludeSearchRootKind::System,
498 });
499 } else if let Some(path) = argument
500 .strip_prefix("-I")
501 .or_else(|| argument.strip_prefix("/I"))
502 {
503 let path = argument_path(directory, path)?;
504 project_include_roots.push(path.clone());
505 include_search_roots.push(CppIncludeSearchRoot {
506 path,
507 kind: CppIncludeSearchRootKind::Project,
508 });
509 } else if let Some(path) = argument.strip_prefix("-iquote") {
510 let path = argument_path(directory, path)?;
511 project_include_roots.push(path.clone());
512 include_search_roots.push(CppIncludeSearchRoot {
513 path,
514 kind: CppIncludeSearchRootKind::Quote,
515 });
516 } else if let Some(path) = argument.strip_prefix("-isystem") {
517 let path = argument_path(directory, path)?;
518 system_include_roots.push(path.clone());
519 include_search_roots.push(CppIncludeSearchRoot {
520 path,
521 kind: CppIncludeSearchRootKind::System,
522 });
523 } else if let Some(definition) = argument.strip_prefix("-D") {
524 defined_macros.insert(macro_name(definition)?);
525 } else if let Some(name) = argument.strip_prefix("-U") {
526 defined_macros.remove(¯o_name(name)?);
527 } else if let Some(value) = argument.strip_prefix("-std=") {
528 std_language = Some(if value.contains("++") {
529 CompiledLanguage::Cpp
530 } else {
531 CompiledLanguage::C
532 });
533 } else if let Some(value) = argument.strip_prefix("-x")
534 && let Some(language) = language_from_x_value(value)
535 {
536 explicit_language = Some(language);
537 }
538 index += 1;
539 }
540 }
541 }
542
543 Some(CppCompileContext {
544 project_include_roots,
545 system_include_roots,
546 forced_includes,
547 defined_macros,
548 include_search_roots,
549 driver_basename,
550 explicit_language,
551 std_language,
552 })
553}
554
555fn driver_basename(driver: &str) -> Option<String> {
560 Path::new(driver)
561 .file_stem()
562 .and_then(|stem| stem.to_str())
563 .map(str::to_owned)
564}
565
566fn language_from_x_value(value: &str) -> Option<CompiledLanguage> {
570 let family = value
571 .strip_suffix("-header")
572 .or_else(|| value.strip_suffix("-cpp-output"))
573 .unwrap_or(value);
574 match family {
575 "c" => Some(CompiledLanguage::C),
576 "c++" => Some(CompiledLanguage::Cpp),
577 _ => None,
578 }
579}
580
581fn argument_path(directory: &Path, raw: &str) -> Option<PathBuf> {
582 if raw.is_empty() {
583 return None;
584 }
585 absolute_path(directory, Path::new(raw))
586}
587
588fn absolute_path(directory: &Path, path: &Path) -> Option<PathBuf> {
589 let path = if path.is_absolute() {
590 path.to_path_buf()
591 } else {
592 directory.join(path)
593 }
594 .normalize();
595 path.is_absolute().then_some(path)
596}
597
598fn canonical_or_normalized_path(path: &Path) -> PathBuf {
604 let path = path.to_path_buf().normalize();
605 let mut missing_tail: Vec<OsString> = Vec::new();
606 let mut current = path.as_path();
607 loop {
608 if let Ok(canonical) = current.canonicalize() {
609 let mut result = canonical.normalize();
610 for component in missing_tail.iter().rev() {
611 result.push(component);
612 }
613 return result.normalize();
614 }
615 let Some(name) = current.file_name() else {
616 return path;
617 };
618 missing_tail.push(name.to_owned());
619 let Some(parent) = current.parent() else {
620 return path;
621 };
622 current = parent;
623 }
624}
625
626fn macro_name(definition: &str) -> Option<String> {
627 let end = definition.find('=').unwrap_or(definition.len());
628 let name = &definition[..end];
629 (!name.is_empty()).then(|| name.to_string())
630}
631
632#[cfg(test)]
633mod tests {
634 use super::{CompiledLanguage, CppCompileContexts, CppExternalIncludeResolution};
635 use brokk_bifrost_core::analyzer::project::{FilesystemProject, Project, TestProject};
636 use brokk_bifrost_core::analyzer::{Language, ProjectFile};
637 use brokk_bifrost_test_macros::hardwon;
638 use std::path::{Path, PathBuf};
639
640 fn context(arguments: &[&str]) -> super::CppCompileContext {
644 let arguments = arguments
645 .iter()
646 .map(|argument| argument.to_string())
647 .collect::<Vec<_>>();
648 super::parse_compile_arguments(Path::new("/workspace"), &arguments)
649 .expect("non-empty argument vector parses")
650 }
651
652 fn project_with_database(database: Option<&str>) -> (tempfile::TempDir, TestProject) {
653 let temp = tempfile::tempdir().expect("temp dir");
654 let root = temp.path().canonicalize().expect("canonical root");
655 ProjectFile::new(root.clone(), "src/main.cpp")
656 .write("int main() { return 0; }")
657 .expect("source");
658 if let Some(database) = database {
659 ProjectFile::new(root.clone(), "compile_commands.json")
660 .write(database)
661 .expect("database");
662 }
663 (temp, TestProject::new(root, Language::Cpp))
664 }
665
666 #[test]
667 fn arguments_entry_collects_include_paths_and_macro_names() {
668 let (_temp, project) = project_with_database(Some(
669 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-iquotequotes","-isystem","system-include","-DDEBUG=1","-D","FEATURE=a=b","-c","src/main.cpp"]}]"#,
670 ));
671 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
672 let contexts = CppCompileContexts::load(&project);
673 let [context] = contexts.contexts_for(&file) else {
674 panic!("one matching context");
675 };
676
677 assert_eq!(
678 vec![
679 project.root_path().join("include"),
680 project.root_path().join("quotes"),
681 ],
682 context.project_include_roots
683 );
684 assert_eq!(
685 vec![project.root_path().join("system-include")],
686 context.system_include_roots
687 );
688 assert!(context.defined_macros.contains("DEBUG"));
689 assert!(context.defined_macros.contains("FEATURE"));
690 assert!(!context.defined_macros.contains("FEATURE=a"));
691 }
692
693 #[test]
694 fn quoted_command_entry_is_tokenized_without_executing_it() {
695 let (_temp, project) = project_with_database(Some(
696 r#"[{"directory":".","file":"src/main.cpp","command":"clang++ -I 'project include' -DNAME=\\\"two words\\\" -c src/main.cpp"}]"#,
697 ));
698 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
699 let contexts = CppCompileContexts::load(&project);
700 let [context] = contexts.contexts_for(&file) else {
701 panic!("one matching context");
702 };
703
704 assert_eq!(
705 vec![project.root_path().join("project include")],
706 context.project_include_roots
707 );
708 assert!(context.defined_macros.contains("NAME"));
709 }
710
711 #[test]
712 #[hardwon("#2011: conditional C visibility ignored command-order macro state")]
713 fn undefine_flags_apply_in_command_order() {
714 let context = context(&[
715 "cc",
716 "-DKEPT",
717 "-DCANCELLED",
718 "-U",
719 "CANCELLED",
720 "-UREVIVED",
721 "-DREVIVED",
722 "-UNEVER_DEFINED",
723 ]);
724 assert!(context.defined_macros.contains("KEPT"));
725 assert!(context.defined_macros.contains("REVIVED"));
726 assert!(!context.defined_macros.contains("CANCELLED"));
727 assert!(!context.defined_macros.contains("NEVER_DEFINED"));
728 }
729
730 #[test]
731 fn a_file_compiled_in_two_configurations_keeps_both() {
732 let (_temp, project) = project_with_database(Some(
733 r#"[
734 {"directory":".","file":"src/main.cpp","arguments":["clang++","-c","src/main.cpp"]},
735 {"directory":".","file":"src/main.cpp","arguments":["clang++","-DOTHER","-c","src/main.cpp"]},
736 {"directory":".","file":"src/other.cpp","arguments":["clang++","-c","src/other.cpp"]}
737 ]"#,
738 ));
739 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
740 let contexts = CppCompileContexts::load(&project);
741 let candidates = contexts.contexts_for(&file);
742
743 assert_eq!(2, candidates.len());
746 assert!(candidates[0].defined_macros.is_empty());
747 assert!(candidates[1].defined_macros.contains("OTHER"));
748 }
749
750 #[test]
751 fn repeated_identical_entries_are_one_configuration() {
752 let (_temp, project) = project_with_database(Some(
753 r#"[
754 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]},
755 {"directory":".","file":"src/main.cpp","arguments":["clang++","-I","include","-c","src/main.cpp"]}
756 ]"#,
757 ));
758 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
759 let contexts = CppCompileContexts::load(&project);
760
761 assert_eq!(1, contexts.contexts_for(&file).len());
764 }
765
766 #[test]
767 fn missing_workspace_sources_are_canonical_bounded_and_deterministic() {
768 let (_temp, project) = project_with_database(None);
769 let root = project.root_path().to_path_buf();
770 ProjectFile::new(root.clone(), "src/present.cpp")
771 .write("int present() { return 0; }")
772 .expect("present source");
773 ProjectFile::new(root.clone(), "src/present.c")
774 .write("int present_c(void) { return 0; }")
775 .expect("present C source");
776 ProjectFile::new(root.clone(), "include/present.hin")
777 .write("int generated_declaration(void);")
778 .expect("present C header template");
779
780 let mut entries = vec![serde_json::json!({
781 "directory": ".",
782 "file": "src/./present.cpp",
783 "arguments": ["clang++", "-c", "src/present.cpp"]
784 })];
785 entries.push(serde_json::json!({
786 "directory": ".",
787 "file": "src/present.c",
788 "arguments": ["clang", "-c", "src/present.c"]
789 }));
790 entries.push(serde_json::json!({
791 "directory": ".",
792 "file": "include/present.hin",
793 "arguments": ["clang", "-x", "c-header", "include/present.hin"]
794 }));
795 entries.extend(
796 (0..(super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3)).map(|index| {
797 serde_json::json!({
798 "directory": ".",
799 "file": format!("generated/./unit-{index:02}.cpp"),
800 "arguments": ["clang++", "-c", format!("generated/unit-{index:02}.cpp")]
801 })
802 }),
803 );
804 entries.push(serde_json::json!({
807 "directory": "generated",
808 "file": "../generated/unit-00.cpp",
809 "arguments": ["clang++", "-c", "../generated/unit-00.cpp"]
810 }));
811 ProjectFile::new(root.clone(), "compile_commands.json")
812 .write(serde_json::to_string(&entries).expect("database JSON"))
813 .expect("database");
814
815 let contexts = CppCompileContexts::load(&project);
816 let listed = project
817 .analyzable_files(Language::Cpp)
818 .expect("C++ listing");
819 let coverage = contexts.missing_workspace_sources(&root, &listed);
820
821 assert_eq!(
822 super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE + 3,
823 coverage.missing_workspace_source_count()
824 );
825 assert_eq!(
826 (0..super::MAX_MISSING_WORKSPACE_SOURCE_SAMPLE)
827 .map(|index| Path::new("generated").join(format!("unit-{index:02}.cpp")))
828 .collect::<Vec<_>>(),
829 coverage.missing_workspace_source_sample()
830 );
831 assert!(
832 !coverage
833 .missing_workspace_source_sample()
834 .contains(&PathBuf::from("src/present.cpp"))
835 );
836
837 let reversed = entries.into_iter().rev().collect::<Vec<_>>();
838 ProjectFile::new(root.clone(), "compile_commands.json")
839 .write(serde_json::to_string(&reversed).expect("reversed database JSON"))
840 .expect("reversed database");
841 let reversed_coverage =
842 CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
843 assert_eq!(coverage, reversed_coverage);
844 }
845
846 #[test]
847 fn missing_workspace_sources_excludes_outside_root_and_ignored_files() {
848 let temp = tempfile::tempdir().expect("workspace root");
849 let root = temp.path().canonicalize().expect("canonical root");
850 let outside = tempfile::tempdir().expect("outside root");
851 let outside_source = outside
852 .path()
853 .canonicalize()
854 .expect("canonical outside root")
855 .join("outside.cpp")
856 .to_string_lossy()
857 .into_owned();
858 ProjectFile::new(root.clone(), "src/present.cpp")
859 .write("int present() { return 0; }")
860 .expect("present source");
861 ProjectFile::new(root.clone(), "ignored.cpp")
862 .write("int ignored() { return 0; }")
863 .expect("ignored source");
864 ProjectFile::new(root.clone(), ".bifrostignore")
865 .write("ignored.cpp\n")
866 .expect("ignore file");
867 let database = serde_json::json!([
868 {
869 "directory": ".",
870 "file": "src/../src/present.cpp",
871 "arguments": ["clang++", "-c", "src/present.cpp"]
872 },
873 {
874 "directory": ".",
875 "file": "ignored.cpp",
876 "arguments": ["clang++", "-c", "ignored.cpp"]
877 },
878 {
879 "directory": ".",
880 "file": outside_source.clone(),
881 "arguments": ["clang++", "-c", outside_source]
882 }
883 ]);
884 ProjectFile::new(root.clone(), "compile_commands.json")
885 .write(database.to_string())
886 .expect("database");
887 let project = FilesystemProject::new(root.clone()).expect("filesystem project");
888 let listed = project
889 .analyzable_files(Language::Cpp)
890 .expect("C++ listing");
891 assert!(
892 listed
893 .iter()
894 .any(|file| file.rel_path() == Path::new("src/present.cpp"))
895 );
896 assert!(
897 !listed
898 .iter()
899 .any(|file| file.rel_path() == Path::new("ignored.cpp"))
900 );
901
902 let coverage = CppCompileContexts::load(&project).missing_workspace_sources(&root, &listed);
903 assert_eq!(1, coverage.missing_workspace_source_count());
904 assert_eq!(
905 &[PathBuf::from("ignored.cpp")],
906 coverage.missing_workspace_source_sample()
907 );
908 }
909
910 #[test]
911 fn a_quoted_include_checks_the_including_directory_before_external_roots() {
912 let external = tempfile::tempdir().expect("external root");
913 let external_root = external
914 .path()
915 .canonicalize()
916 .expect("canonical external root");
917 std::fs::write(external_root.join("vendor.hpp"), "class External {};\n")
918 .expect("external vendor header");
919 std::fs::write(external_root.join("from-search.hpp"), "class Search {};\n")
920 .expect("external search header");
921 let database = serde_json::json!([{
922 "directory": ".",
923 "file": "src/main.cpp",
924 "arguments": [
925 "clang++",
926 "-I",
927 external_root,
928 "-c",
929 "src/main.cpp"
930 ]
931 }])
932 .to_string();
933 let (_temp, project) = project_with_database(Some(&database));
934 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
935 let source_path = file.abs_path();
936 let source_directory = source_path.parent().expect("source directory");
937 std::fs::create_dir_all(source_directory).expect("source directory");
938 std::fs::write(source_directory.join("vendor.hpp"), "class Local {};\n")
939 .expect("local vendor header");
940 let contexts = CppCompileContexts::load(&project);
941
942 assert_eq!(
943 CppExternalIncludeResolution::Undeclared,
944 contexts.resolve_external_include(
945 &file,
946 &file.abs_path(),
947 std::path::Path::new("vendor.hpp"),
948 true,
949 )
950 );
951 assert!(matches!(
952 contexts.resolve_external_include(
953 &file,
954 &file.abs_path(),
955 std::path::Path::new("from-search.hpp"),
956 true,
957 ),
958 CppExternalIncludeResolution::Declared { .. }
959 ));
960 assert!(matches!(
961 contexts.resolve_external_include(
962 &file,
963 &file.abs_path(),
964 std::path::Path::new("vendor.hpp"),
965 false,
966 ),
967 CppExternalIncludeResolution::Declared { .. }
968 ));
969 }
970
971 #[test]
972 fn configurations_must_agree_on_the_external_header() {
973 let first = tempfile::tempdir().expect("first root");
974 let second = tempfile::tempdir().expect("second root");
975 let first = first.path().canonicalize().expect("canonical first root");
976 let second = second.path().canonicalize().expect("canonical second root");
977 std::fs::write(first.join("vector"), "namespace std { class vector {}; }")
978 .expect("first header");
979 std::fs::write(second.join("vector"), "namespace std { class vector {}; }")
980 .expect("second header");
981 let database = serde_json::json!([
982 {
983 "directory": ".",
984 "file": "src/main.cpp",
985 "arguments": ["clang++", "-isystem", first, "-c", "src/main.cpp"]
986 },
987 {
988 "directory": ".",
989 "file": "src/main.cpp",
990 "arguments": ["clang++", "-isystem", second, "-c", "src/main.cpp"]
991 }
992 ])
993 .to_string();
994 let (_temp, project) = project_with_database(Some(&database));
995 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
996
997 assert_eq!(
998 CppExternalIncludeResolution::Conflicting,
999 CppCompileContexts::load(&project)
1000 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1001 );
1002 }
1003
1004 #[test]
1005 fn external_include_cannot_escape_its_declared_root() {
1006 let external = tempfile::tempdir().expect("external root");
1007 let root = external.path().canonicalize().expect("canonical root");
1008 std::fs::create_dir_all(root.join("include")).expect("include directory");
1009 std::fs::write(root.join("outside.hpp"), "class Outside {};").expect("outside header");
1010 let database = serde_json::json!([{
1011 "directory": ".",
1012 "file": "src/main.cpp",
1013 "arguments": [
1014 "clang++",
1015 "-isystem",
1016 root.join("include"),
1017 "-c",
1018 "src/main.cpp"
1019 ]
1020 }])
1021 .to_string();
1022 let (_temp, project) = project_with_database(Some(&database));
1023 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1024
1025 assert_eq!(
1026 CppExternalIncludeResolution::Undeclared,
1027 CppCompileContexts::load(&project)
1028 .resolve_external_angle_include(&file, std::path::Path::new("../outside.hpp"))
1029 );
1030 assert_eq!(
1031 CppExternalIncludeResolution::Undeclared,
1032 CppCompileContexts::load(&project)
1033 .resolve_external_angle_include(&file, &root.join("outside.hpp"))
1034 );
1035 }
1036
1037 #[test]
1038 fn msvc_project_and_system_include_flags_preserve_search_order() {
1039 let (_temp, project) = project_with_database(Some(
1040 r#"[{"directory":".","file":"src/main.cpp","arguments":["cl.exe","/I","vendor/include","/external:Ifake-system/include","/imsvc","toolchain/include","/c","src/main.cpp"]}]"#,
1041 ));
1042 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1043 let contexts = CppCompileContexts::load(&project);
1044 let [context] = contexts.contexts_for(&file) else {
1045 panic!("one MSVC compile context");
1046 };
1047
1048 assert_eq!(1, context.project_include_roots.len());
1049 assert_eq!(2, context.system_include_roots.len());
1050 }
1051
1052 #[test]
1053 fn driver_basename_ending_in_plus_plus_selects_cpp_over_a_c_extension() {
1054 let cx = context(&["clang++", "-c", "file.c"]);
1055 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1056
1057 let cx = context(&["arm-none-eabi-gcc", "-c", "file.c"]);
1060 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1061
1062 let cx = context(&["arm-none-eabi-g++", "-c", "file.h"]);
1064 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.h")));
1065 }
1066
1067 #[test]
1068 fn driver_exe_suffix_is_stripped_before_the_plus_plus_check() {
1069 let cx = context(&["clang++.exe", "-c", "file.c"]);
1070 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1071 }
1072
1073 #[test]
1074 fn std_family_outranks_the_driver_basename() {
1075 let cx = context(&["g++", "-std=gnu11", "-c", "file.c"]);
1078 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1079
1080 let cx = context(&["gcc", "-std=c++17", "-c", "file.cc"]);
1081 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1082
1083 let cx = context(&["gcc", "-std=c17", "-c", "file.cc"]);
1084 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cc")));
1085
1086 let cx = context(&["gcc", "-std=gnu++20", "-c", "file.c"]);
1087 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1088 }
1089
1090 #[test]
1091 fn explicit_x_split_form_outranks_std_and_driver() {
1092 let cx = context(&["clang", "-x", "c", "-std=c++17", "-c", "file.cpp"]);
1093 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1094 }
1095
1096 #[test]
1097 fn explicit_x_glued_form_is_recognized() {
1098 let cx = context(&["gcc", "-xc++", "-c", "file.c"]);
1099 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1100
1101 let cx = context(&["g++", "-xc", "-c", "file.cpp"]);
1102 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1103 }
1104
1105 #[test]
1106 fn explicit_x_on_a_c_file_overrides_the_extension() {
1107 let cx = context(&["gcc", "-x", "c++", "-c", "file.c"]);
1110 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1111 }
1112
1113 #[test]
1114 fn msvc_tc_and_tp_force_the_language_regardless_of_extension() {
1115 let cx = context(&["cl.exe", "/TC", "/c", "file.cpp"]);
1116 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.cpp")));
1117
1118 let cx = context(&["cl.exe", "/TP", "/c", "file.c"]);
1119 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.c")));
1120 }
1121
1122 #[test]
1123 fn missing_or_malformed_database_has_no_context() {
1124 let (_temp, project) = project_with_database(None);
1125 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1126 assert!(
1127 CppCompileContexts::load(&project)
1128 .contexts_for(&file)
1129 .is_empty()
1130 );
1131
1132 let (_temp, project) = project_with_database(Some("not json"));
1133 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1134 assert!(
1135 CppCompileContexts::load(&project)
1136 .contexts_for(&file)
1137 .is_empty()
1138 );
1139 }
1140
1141 #[test]
1142 fn an_explicit_system_root_declares_an_angle_include() {
1143 let (_temp, project) = project_with_database(Some(
1144 r#"[{"directory":".","file":"src/main.cpp","arguments":["clang++","-isystem","fake-system/include","-c","src/main.cpp"]}]"#,
1145 ));
1146 let header = ProjectFile::new(
1147 project.root_path().to_path_buf(),
1148 "fake-system/include/vector",
1149 );
1150 header
1151 .write("namespace std { class vector {}; }")
1152 .expect("header");
1153 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1154
1155 assert_eq!(
1156 CppExternalIncludeResolution::Declared {
1157 root: project
1158 .root_path()
1159 .join("fake-system/include")
1160 .canonicalize()
1161 .expect("canonical root"),
1162 header: header.abs_path().canonicalize().expect("canonical header"),
1163 },
1164 CppCompileContexts::load(&project)
1165 .resolve_external_angle_include(&file, std::path::Path::new("vector"))
1166 );
1167 }
1168
1169 #[test]
1170 fn an_external_project_root_declares_but_a_workspace_project_root_does_not() {
1171 let external = tempfile::tempdir().expect("external root");
1172 let external_root = external
1173 .path()
1174 .canonicalize()
1175 .expect("canonical external root");
1176 std::fs::write(external_root.join("vendor.hpp"), "class Vendor {};")
1177 .expect("external header");
1178 let database = serde_json::json!([{
1179 "directory": ".",
1180 "file": "src/main.cpp",
1181 "arguments": [
1182 "clang++",
1183 "-I",
1184 external_root,
1185 "-I",
1186 "include",
1187 "-c",
1188 "src/main.cpp"
1189 ]
1190 }])
1191 .to_string();
1192 let (_temp, project) = project_with_database(Some(&database));
1193 ProjectFile::new(project.root_path().to_path_buf(), "include/local.hpp")
1194 .write("class Local {};")
1195 .expect("local header");
1196 let file = ProjectFile::new(project.root_path().to_path_buf(), "src/main.cpp");
1197 let contexts = CppCompileContexts::load(&project);
1198
1199 assert!(matches!(
1200 contexts.resolve_external_angle_include(&file, std::path::Path::new("vendor.hpp")),
1201 CppExternalIncludeResolution::Declared { .. }
1202 ));
1203 assert_eq!(
1204 CppExternalIncludeResolution::Undeclared,
1205 contexts.resolve_external_angle_include(&file, std::path::Path::new("local.hpp"))
1206 );
1207 }
1208
1209 #[test]
1210 fn default_by_extension_c_is_c_and_everything_else_is_cpp() {
1211 let cx = context(&["cc", "-c", "file.c"]);
1212 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1213
1214 let cx = context(&["cc", "-c", "file.cc"]);
1215 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cc")));
1216 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.hpp")));
1217 let cx = context(&["cl.exe", "/c", "file.c"]);
1218 assert_eq!(CompiledLanguage::C, cx.tu_language(Path::new("file.c")));
1219
1220 let cx = context(&["cl.exe", "/c", "file.cpp"]);
1221 assert_eq!(CompiledLanguage::Cpp, cx.tu_language(Path::new("file.cpp")));
1222 }
1223
1224 #[test]
1225 #[hardwon("#6929: a Windows verbatim prefix made an in-workspace sibling external")]
1226 fn quoted_sibling_external_header_respects_workspace_boundary() {
1227 let outer = tempfile::tempdir().unwrap();
1230 let outer_root = outer.path().canonicalize().unwrap();
1231 let workspace = outer_root.join("workspace");
1232 let external = outer_root.join("external");
1233 std::fs::create_dir_all(&workspace).unwrap();
1234 std::fs::create_dir_all(&external).unwrap();
1235 for directory in [&workspace, &external] {
1236 std::fs::write(directory.join("caller.hpp"), "#include \"sibling.hpp\"\n").unwrap();
1237 std::fs::write(directory.join("sibling.hpp"), "struct S {};\n").unwrap();
1238 }
1239 let cx = super::parse_compile_arguments(
1242 &workspace,
1243 &[
1244 "clang++".into(),
1245 "-I".into(),
1246 outer_root.to_string_lossy().into_owned(),
1247 ],
1248 )
1249 .unwrap();
1250 assert_eq!(
1251 cx.resolve_external_include(
1252 &workspace,
1253 &workspace.join("caller.hpp"),
1254 Path::new("sibling.hpp"),
1255 true
1256 ),
1257 None
1258 );
1259 assert_eq!(
1260 cx.resolve_external_include(
1261 &workspace,
1262 &external.join("caller.hpp"),
1263 Path::new("sibling.hpp"),
1264 true
1265 ),
1266 Some((outer_root, external.join("sibling.hpp")))
1267 );
1268 }
1269}